shutdown.sh
raw
#!/usr/bin/env bash
# Shutdown test: a guiduck application must exit cleanly when its window
# closes, on both backends.
#
# This is the one thing every other script cannot check. They drive a window
# and then tear the compositor down underneath it, so the application's own
# exit path — the one every real user takes, every time — was never run.
# What hid there was a use-after-free: `EventLoop::run_app` takes the loop by
# value, so the platform connection is gone by the time it returns, and
# anything still holding Wayland objects (the clipboard's worker thread above
# all) tore them down through a freed `wl_display`. The process died in
# `wl_proxy_destroy` *after* the application's last line of code.
#
# The window is closed by an unconsumed Escape, which is the same
# `event_loop.exit()` a title-bar close reaches — the exit path is one path.
#
# Requires: quibble + quibblectl on PATH. Exits nonzero on any failure.
set -euo pipefail
cd "$(dirname "$0")/.."
EXAMPLES=(m1_widgets m16_demo m18_demo)
for example in "${EXAMPLES[@]}"; do
cargo build -p guiduck --example "$example"
done
workdir=$(mktemp -d)
sock="$workdir/quibble.sock"
cleanup() {
quibblectl --socket "$sock" quit >/dev/null 2>&1 || true
rm -rf "$workdir"
}
trap cleanup EXIT
# A long-lived placeholder client, so the compositor outlives the applications
# under test and their exit status is ours to read. Launching them *as* the
# compositor's child would hand us the compositor's status instead.
quibble --socket "$sock" --width 400 --height 300 sleep 3600 \
>"$workdir/quibble.log" 2>&1 &
for _ in $(seq 1 50); do
quibblectl --socket "$sock" ping >/dev/null 2>&1 && break
sleep 0.2
done
display=$(grep -o 'WAYLAND_DISPLAY=wayland-[0-9]*' "$workdir/quibble.log" | head -1 | cut -d= -f2)
if [ -z "$display" ]; then
echo "FAIL: could not read the compositor's display name" >&2
exit 1
fi
fail=0
for example in "${EXAMPLES[@]}"; do
for backend in software vello; do
log="$workdir/$example-$backend.log"
status_file="$workdir/$example-$backend.status"
# `|| status=$?` rather than a bare call: `set -e` is inherited by the
# subshell, and a crashing application would otherwise kill it before
# the status is ever written — reporting "did not exit" for something
# that exited very definitely.
(
status=0
WAYLAND_DISPLAY="$display" GUIDUCK_BACKEND="$backend" \
"target/debug/examples/$example" >"$log" 2>&1 || status=$?
echo "$status" >"$status_file"
) &
runner=$!
quibblectl --socket "$sock" wait-window --timeout-ms 30000 >/dev/null
sleep 1
quibblectl --socket "$sock" key 1 press >/dev/null
quibblectl --socket "$sock" key 1 release >/dev/null
# Wait for it to go, rather than assuming a duration.
for _ in $(seq 1 100); do
[ -f "$status_file" ] && break
sleep 0.1
done
if [ ! -f "$status_file" ]; then
echo "FAIL: $example ($backend) did not exit on Escape" >&2
kill "$runner" 2>/dev/null || true
fail=1
continue
fi
status=$(cat "$status_file")
# 139 is the one this exists for: SIGSEGV during teardown.
if [ "$status" -ne 0 ]; then
echo "FAIL: $example ($backend) exited $status (139 = SIGSEGV)" >&2
tail -20 "$log" >&2
fail=1
else
echo "ok: $example ($backend) exited cleanly"
fi
done
done
if [ "$fail" -ne 0 ]; then
exit 1
fi
echo "shutdown: all examples exit cleanly on both backends"