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"