#!/bin/sh # Run an iris example on a machine with no display. # # ./scripts/run-headless.sh tabs # ./scripts/run-headless.sh tabs --shot /tmp/tabs.png --seconds 4 # ./scripts/run-headless.sh tabs --replay taps.touch --shot /tmp/tabs.png # ./scripts/run-headless.sh app --dir ../elsewhere --mode 1080x2424@120Hz # # `--dir DIR` names the workspace to build in, defaulting to this one, so a # project that depends on iris can be run through the same rig. `--bin` runs a # crate binary rather than an example, and takes its own argv from # `$RUN_HEADLESS_ARGS`, word-split on purpose. # # `--mode` sets the output, for running something at a size other than a # desktop's -- a phone's, say. Set every run rather than only when it changes: # the compositor is reused between runs, so a default-shaped run after a # custom one would otherwise inherit the other's output and quietly screenshot # the wrong size. # # `--resize WxH@Hz` changes the output under the app once it is up, then # screenshots. A resize is its own case: what it has to match is a cold start # at that size, byte for byte, and nothing in `cargo test` can see it. # # `--replay FILE` drives a `.touch` recording into the window through # `replay-touch`, which reads it with the same parser `iris::harness` uses. A # recording is ` down|move|up ` in the output's own pixels. With # `--shot` it also writes `-before.png` from just before the gesture, # since "it moved" is a claim about two pictures. # # What it supplies is a compositor for winit to open a surface on: a headless # sway, and `grim` to screenshot it. Sway gets its own socket and runtime # directory rather than joining whatever else is running, because it tiles -- # adding a window to someone else's compositor resizes theirs. set -eu need() { command -v "$1" >/dev/null 2>&1 || { echo "run-headless: $1 is not installed ($2)" >&2 exit 127 } } need sway "the compositor an example opens its window on" need swaymsg "sway's control socket" scripts=$(cd "$(dirname "$0")" && pwd) root=$(cd "$scripts/.." && pwd) workdir="$root" cd "$root" run="${XDG_RUNTIME_DIR:-/tmp}/iris-headless" seconds=3 shot="" replay="" resize="" example="" kind=example mode=1920x1200@60Hz while [ $# -gt 0 ]; do case "$1" in --shot) shot=$2; shift 2 ;; --seconds) seconds=$2; shift 2 ;; --bin) kind=bin; shift ;; --mode) mode=$2; shift 2 ;; --resize) resize=$2; shift 2 ;; --replay) replay=$2; shift 2 ;; --dir) workdir=$(cd "$2" && pwd); shift 2 ;; --) shift; break ;; *) example=$1; shift ;; esac done [ -n "$example" ] || { echo "usage: $0 NAME [--bin] [--dir DIR] [--mode WxH@Hz] [--resize WxH@Hz] [--replay TOUCH] [--shot PNG] [--seconds N] [-- cargo args]" >&2; exit 2; } [ -z "$replay" ] || [ -f "$replay" ] || { echo "run-headless: no touch script at $replay" >&2; exit 2; } [ -z "$shot" ] || need grim "the screenshot --shot writes" 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 "$scripts/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 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 # The extent `replay-touch` positions against, so a script's coordinates are # the output's own pixels. Set beside every mode change, since a gesture # scaled against a mode the output no longer has lands somewhere else and # still looks like a run that worked. set_mode() { swaymsg output HEADLESS-1 mode "$1" >/dev/null out_w=${1%x*} out_h=${1#*x}; out_h=${out_h%@*} } set_mode "$mode" # Built before the app starts, so a compile error is not reported as a # window that failed to move. [ -z "$replay" ] || (cd "$root" && cargo build --bin replay-touch -p rig-input) >&2 cd "$workdir" if [ "$kind" = bin ]; then cargo build --bin "$example" "$@" >&2 bin="$workdir/target/debug/$example" else cargo build --example "$example" "$@" >&2 bin="$workdir/target/debug/examples/$example" fi # Deliberately word-split: this is the binary's own argv, not a single path. # shellcheck disable=SC2086 "$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 i=0 while [ $i -lt "$((seconds * 2))" ]; do kill -0 "$pid" 2>/dev/null || break i=$((i + 1)); sleep 0.5 done if [ -n "$resize" ] && kill -0 "$pid" 2>/dev/null; then set_mode "$resize" echo "run-headless: resized to $resize" >&2 sleep 2 fi if [ -n "$replay" ] && kill -0 "$pid" 2>/dev/null; then if [ -n "$shot" ]; then grim "${shot%.png}-before.png" echo "run-headless: wrote ${shot%.png}-before.png (before the gesture)" >&2 fi "$root/target/debug/replay-touch" "$out_w" "$out_h" "$replay" # A fling outlives the finger: the gesture's own last sample is not # when the list stops. Long enough for Android's spline to settle # (`FlingCalculator::duration` tops out around a second and a half). sleep 2 fi 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"