desktop-app (RUST.md's E4) is a real crate binary a person runs, not a demo under examples/, and it needs its own argv (--ca, --link) to start at all -- neither of which the script had a way to express. --bin swaps `cargo build --example`/`target/debug/examples/NAME` for the `--bin` equivalents; $RUN_HEADLESS_ARGS is word-split into the launched binary's own argv, since no example ever needed one before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
128 lines
4.5 KiB
Bash
Executable File
128 lines
4.5 KiB
Bash
Executable File
#!/bin/sh
|
|
# Run an iris example on this machine, which has no display.
|
|
#
|
|
# ./run-headless.sh tabs [-- cargo args]
|
|
# ./run-headless.sh tabs --shot /tmp/tabs.png --seconds 4
|
|
#
|
|
# `--bin` runs a real crate binary instead of an example (E4's
|
|
# `desktop-app`, which is a window a person runs, not a demo) --
|
|
# `cargo build --bin NAME` instead of `--example NAME`, and
|
|
# `target/debug/NAME` instead of `target/debug/examples/NAME`. Its own
|
|
# argv (the CLI flags a real binary takes, as opposed to `cargo build`'s
|
|
# own flags after `--`) comes through `$RUN_HEADLESS_ARGS`, word-split on
|
|
# purpose -- an example never needed one, so there was nowhere to plumb it
|
|
# through positionally without disturbing the existing `-- cargo args`
|
|
# convention above.
|
|
#
|
|
# The VM has a virtio-gpu render node (Vulkan 1.4 through Venus, GL 4.6
|
|
# through virgl), so wgpu runs on the host's real GPU -- what is missing is
|
|
# only a compositor to give winit a surface. So: a headless sway, the same
|
|
# trick `emu` uses for the Android emulator, and `grim` to see the result.
|
|
#
|
|
# It is deliberately *not* `emu`'s compositor. sway tiles, so adding a window
|
|
# to the one an emulator is sitting in resizes that emulator's window, and a
|
|
# peer session's `emu up` could join at any moment. This one has its own
|
|
# socket and its own runtime directory and goes away with the machine.
|
|
set -eu
|
|
|
|
here=$(cd "$(dirname "$0")" && pwd)
|
|
run="${XDG_RUNTIME_DIR:-/tmp}/iris-headless"
|
|
seconds=3
|
|
shot=""
|
|
example=""
|
|
kind=example
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--shot) shot=$2; shift 2 ;;
|
|
--seconds) seconds=$2; shift 2 ;;
|
|
--bin) kind=bin; shift ;;
|
|
--) shift; break ;;
|
|
*) example=$1; shift ;;
|
|
esac
|
|
done
|
|
[ -n "$example" ] || { echo "usage: $0 NAME [--bin] [--shot PNG] [--seconds N] [-- cargo args]" >&2; exit 2; }
|
|
|
|
mkdir -p "$run"
|
|
export SWAYSOCK="$run/sway.sock"
|
|
|
|
# Named rather than left to sway's pid-based default, so a second run reuses
|
|
# this compositor instead of starting another beside it.
|
|
if ! swaymsg -t get_version >/dev/null 2>&1; then
|
|
rm -f "$SWAYSOCK"
|
|
WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 LIBSEAT_BACKEND=noop \
|
|
setsid sway -c "$here/headless.conf" >"$run/sway.log" 2>&1 &
|
|
i=0
|
|
while [ $i -lt 20 ]; do
|
|
swaymsg -t get_version >/dev/null 2>&1 && break
|
|
i=$((i + 1)); sleep 0.5
|
|
done
|
|
swaymsg -t get_version >/dev/null 2>&1 || {
|
|
echo "run-headless: compositor did not start; see $run/sway.log" >&2
|
|
exit 1
|
|
}
|
|
fi
|
|
|
|
# Asked of the compositor rather than guessed: sway takes the first free
|
|
# wayland-N, and this machine may already have one.
|
|
rm -f "$run/display"
|
|
swaymsg exec -- "sh -c 'printf %s \"\$WAYLAND_DISPLAY\" > $run/display'" >/dev/null
|
|
i=0
|
|
while [ $i -lt 20 ]; do
|
|
[ -s "$run/display" ] && break
|
|
i=$((i + 1)); sleep 0.5
|
|
done
|
|
[ -s "$run/display" ] || { echo "run-headless: could not read WAYLAND_DISPLAY" >&2; exit 1; }
|
|
WAYLAND_DISPLAY=$(cat "$run/display")
|
|
export WAYLAND_DISPLAY
|
|
|
|
echo "run-headless: $WAYLAND_DISPLAY (sway $(swaymsg -t get_version --raw | sed -n 's/.*"human_readable":"\([^"]*\)".*/\1/p'))" >&2
|
|
|
|
cd "$here"
|
|
if [ "$kind" = bin ]; then
|
|
cargo build --bin "$example" "$@" >&2
|
|
bin="$here/target/debug/$example"
|
|
else
|
|
cargo build --example "$example" "$@" >&2
|
|
bin="$here/target/debug/examples/$example"
|
|
fi
|
|
|
|
# shellcheck disable=SC2086 -- deliberately word-split: this is the
|
|
# binary's own argv, not a single path.
|
|
"$bin" ${RUN_HEADLESS_ARGS:-} >"$run/$example.log" 2>&1 &
|
|
pid=$!
|
|
trap 'kill "$pid" 2>/dev/null || true' EXIT INT TERM
|
|
|
|
# Wait for the window to be mapped rather than for a number of seconds. A
|
|
# fixed sleep took an all-black screenshot the first time this ran, when sway
|
|
# had started in the same invocation and had not composited its output yet --
|
|
# which is indistinguishable from an app that draws nothing.
|
|
i=0
|
|
while [ $i -lt 40 ]; do
|
|
kill -0 "$pid" 2>/dev/null || break
|
|
swaymsg -t get_tree --raw 2>/dev/null | grep -q "\"pid\":$pid," && break
|
|
i=$((i + 1)); sleep 0.25
|
|
done
|
|
|
|
# Then settle, for whatever the example does after its first frame.
|
|
i=0
|
|
while [ $i -lt "$((seconds * 2))" ]; do
|
|
kill -0 "$pid" 2>/dev/null || break
|
|
i=$((i + 1)); sleep 0.5
|
|
done
|
|
|
|
if kill -0 "$pid" 2>/dev/null; then
|
|
[ -n "$shot" ] && grim "$shot" && echo "run-headless: wrote $shot" >&2
|
|
kill "$pid" 2>/dev/null || true
|
|
wait "$pid" 2>/dev/null || true
|
|
status=0
|
|
else
|
|
wait "$pid" 2>/dev/null || status=$?
|
|
echo "run-headless: $example exited early (status ${status:-0})" >&2
|
|
status=${status:-1}
|
|
fi
|
|
|
|
echo "--- $example output ---" >&2
|
|
cat "$run/$example.log" >&2
|
|
exit "$status"
|