fix: share reliable virtual key delivery across Linux rigs
Rust checks / windows-cross-check (push) Successful in 1m6s
Rust checks / linux (push) Failing after 5m44s

This commit is contained in:
2026-10-08 19:38:35 +02:00
parent dd016f7727
commit 178c400eeb
4 changed files with 54 additions and 15 deletions
+31
View File
@@ -466,3 +466,34 @@ The headless e2e restores keyboard focus after virtual-pointer commands and
holds virtual keys briefly: instantaneous virtual input can disappear before holds virtual keys briefly: instantaneous virtual input can disappear before
GPUI/Wayland dispatch. Keyboard actions update the editor entity directly, GPUI/Wayland dispatch. Keyboard actions update the editor entity directly,
avoiding a nested window borrow during event dispatch. avoiding a nested window borrow during event dispatch.
After merging clips and playback at `dd016f7`, the clips rig intermittently
reported “desktop did not honour Quit”. A fresh-window probe on 2026-10-08
missed 3 of 8 instantaneous `wtype` Ctrl+Q presses; two missed presses occurred
while Clipbin was focused. Temporary action/GUI-exit/worker-join markers showed
that the missed presses never reached the Quit handler. All 8 held-key attempts
reached the handler and completed shutdown. The temporary markers were removed;
no application shutdown change was needed.
Both scripts now source `scripts/rig/keys.sh`: it restores keyboard focus after
virtual-pointer input, gives the virtual keymap 200 ms to settle, and holds each
key for 100 ms. Modified keys also give the modifier 100 ms to settle. Delivery
has a 10-second timeout; each script still requires the actual desktop process
to exit successfully. Serial reruns passed with
`env PATH=/usr/bin:/bin:$HOME/.cargo/bin`:
- `scripts/validate/linux-clips.sh /tmp/clipbin-clips-quit-fixed`: H.264 retained,
360 CFR frames over a six-second aligned clip, continuous audio, zero decode
errors, original press range, bookmark protection, tiny-budget cleanup,
bookmark re-export after adjacent removal, and both Quit exits.
- `scripts/validate/linux-playback.sh /tmp/clipbin-playback-quit-fixed`: moving
preview (4.1% changed pixels), device audio RMS 0.08045, thumbnails, active
capture, seek/gap/boundary behaviour, volume/mute, frame stepping and both Quit
exits. Decode median/p95 were 2.578/4.073 ms; GPUI upload median/p95 were
0.241/0.408 ms.
Logs are `/tmp/clipbin-clips-quit-fixed.log` and
`/tmp/clipbin-playback-quit-fixed.log`; screenshots are in their corresponding
output directories. `bash -n`, `git diff --check` and `cargo fmt --check` also
passed. This follow-up changes only the Linux rig scripts and documentation;
Windows runtime and real Hyprland validation limits above remain unchanged.
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Source inside the headless rig; accepts a key and an optional modifier.
rig_key() {
local key="$1" modifier="${2:-}"
# Destroying rig-pointer's virtual seat can clear keyboard focus in Sway.
swaymsg '[app_id="dev.clipbin.Clipbin"] focus' >/dev/null
sleep .2
# Let the virtual keymap settle, then hold the key through a UI dispatch.
local args=(-s 200)
if [[ -n "${modifier}" ]]; then args+=(-M "${modifier}" -s 100); fi
args+=(-P "${key}" -s 100 -p "${key}")
if [[ -n "${modifier}" ]]; then args+=(-m "${modifier}"); fi
timeout 10 wtype "${args[@]}"
}
+3 -2
View File
@@ -10,6 +10,7 @@ export CLIPBIN_CLIPS_REPO="${repo}" CLIPBIN_CLIPS_OUT="${out}"
"${repo}/scripts/rig/headless.sh" bash <<'INNER' "${repo}/scripts/rig/headless.sh" bash <<'INNER'
set -euo pipefail set -euo pipefail
repo="${CLIPBIN_CLIPS_REPO}"; out="${CLIPBIN_CLIPS_OUT}" repo="${CLIPBIN_CLIPS_REPO}"; out="${CLIPBIN_CLIPS_OUT}"
source "${repo}/scripts/rig/keys.sh"
export XDG_DATA_HOME="${out}/data" XDG_CONFIG_HOME="${out}/config" export XDG_DATA_HOME="${out}/data" XDG_CONFIG_HOME="${out}/config"
mkdir -p "${XDG_CONFIG_HOME}/clipbin" mkdir -p "${XDG_CONFIG_HOME}/clipbin"
cat > "${XDG_CONFIG_HOME}/clipbin/settings.json" <<'JSON' cat > "${XDG_CONFIG_HOME}/clipbin/settings.json" <<'JSON'
@@ -34,12 +35,12 @@ quit_app() {
read -r qx qy qw qh < <(geometry) read -r qx qy qw qh < <(geometry)
click "$((qx+qw-30))" "$((qy+100))" click "$((qx+qw-30))" "$((qy+100))"
sleep 0.5 sleep 0.5
wtype -M ctrl -k q -m ctrl rig_key q ctrl
for attempt in $(seq 20); do for attempt in $(seq 20); do
if ! kill -0 "${app}" 2>/dev/null; then wait "${app}"; return; fi if ! kill -0 "${app}" 2>/dev/null; then wait "${app}"; return; fi
if ((attempt % 4 == 0)); then if ((attempt % 4 == 0)); then
swaymsg '[app_id="dev.clipbin.Clipbin"] focus' >/dev/null swaymsg '[app_id="dev.clipbin.Clipbin"] focus' >/dev/null
wtype -M ctrl -k q -m ctrl rig_key q ctrl
fi fi
sleep 1 sleep 1
done done
+6 -13
View File
@@ -11,6 +11,7 @@ export CLIPBIN_PLAYBACK_REPO="${repo}" CLIPBIN_PLAYBACK_OUT="${out}"
"${repo}/scripts/rig/headless.sh" bash <<'INNER' "${repo}/scripts/rig/headless.sh" bash <<'INNER'
set -euo pipefail set -euo pipefail
repo="${CLIPBIN_PLAYBACK_REPO}" out="${CLIPBIN_PLAYBACK_OUT}" repo="${CLIPBIN_PLAYBACK_REPO}" out="${CLIPBIN_PLAYBACK_OUT}"
source "${repo}/scripts/rig/keys.sh"
export XDG_DATA_HOME="${out}/data" XDG_CONFIG_HOME="${out}/config" CLIPBIN_TRACE_FRAMES=1 export XDG_DATA_HOME="${out}/data" XDG_CONFIG_HOME="${out}/config" CLIPBIN_TRACE_FRAMES=1
mkdir -p "${XDG_DATA_HOME}" "${XDG_CONFIG_HOME}" mkdir -p "${XDG_DATA_HOME}" "${XDG_CONFIG_HOME}"
# ALSA's PipeWire plugin is cpal's fallback on Linux; there is no hardware card. # ALSA's PipeWire plugin is cpal's fallback on Linux; there is no hardware card.
@@ -21,12 +22,6 @@ ctl.!default { type pipewire }
ALSA ALSA
export ALSA_CONFIG_PATH="${out}/alsa.conf" export ALSA_CONFIG_PATH="${out}/alsa.conf"
swaymsg '[title="clipbin-test-game"] fullscreen disable' > /dev/null swaymsg '[title="clipbin-test-game"] fullscreen disable' > /dev/null
send_key() {
# Destroying rig-pointer's virtual seat can clear keyboard focus in Sway.
swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null
sleep .2
timeout 10 wtype -s 200 -P "$1" -s 100 -p "$1"
}
# Timeline bounds come from GPUI's trace rather than guessed screen positions. # Timeline bounds come from GPUI's trace rather than guessed screen positions.
pointer_time() { pointer_time() {
python3 - "$1" "$2" "$3" "${XDG_DATA_HOME}" <<'PYPOINTER' python3 - "$1" "$2" "$3" "${XDG_DATA_HOME}" <<'PYPOINTER'
@@ -62,7 +57,7 @@ grim "${out}/editor-open.png"
pointer_time "${out}/active.log" 1 click | "${repo}/target/debug/rig-pointer" 1920 1080 pointer_time "${out}/active.log" 1 click | "${repo}/target/debug/rig-pointer" 1920 1080
# GPUI/Wayland needs the virtual keyboard keymap to settle and a held key; # GPUI/Wayland needs the virtual keyboard keymap to settle and a held key;
# instantaneous wtype press/release occasionally disappears before dispatch. # instantaneous wtype press/release occasionally disappears before dispatch.
send_key space rig_key space
sleep 1 sleep 1
grim "${out}/editor-playing-1.png" grim "${out}/editor-playing-1.png"
sleep 2 sleep 2
@@ -76,8 +71,7 @@ print('PASS playback left recording active and generated real thumbnails')
PY PY
kill "${game}"; wait "${game}" 2>/dev/null || true kill "${game}"; wait "${game}" 2>/dev/null || true
sleep 3 sleep 3
swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null rig_key q ctrl
timeout 10 wtype -s 150 -M ctrl -s 100 -P q -s 100 -p q -m ctrl
for _ in $(seq 20); do if ! kill -0 "${app}" 2>/dev/null; then break; fi; sleep 1; done for _ in $(seq 20); do if ! kill -0 "${app}" 2>/dev/null; then break; fi; sleep 1; done
if kill -0 "${app}" 2>/dev/null; then echo 'FAIL Ctrl+Q did not exit' >&2; exit 1; fi if kill -0 "${app}" 2>/dev/null; then echo 'FAIL Ctrl+Q did not exit' >&2; exit 1; fi
wait "${app}" wait "${app}"
@@ -110,12 +104,11 @@ grim "${out}/editor-gap.png"
pointer_time "${out}/playback.log" 1 $'scroll -1\nsleep 0.5\nscroll 1' | "${repo}/target/debug/rig-pointer" 1920 1080 pointer_time "${out}/playback.log" 1 $'scroll -1\nsleep 0.5\nscroll 1' | "${repo}/target/debug/rig-pointer" 1920 1080
pointer_time "${out}/playback.log" 2.5 click | "${repo}/target/debug/rig-pointer" 1920 1080 pointer_time "${out}/playback.log" 2.5 click | "${repo}/target/debug/rig-pointer" 1920 1080
sleep .3 sleep .3
send_key Right rig_key Right
sleep .3 sleep .3
send_key Left rig_key Left
sleep .3 sleep .3
swaymsg '[app_id="dev.clipbin.Clipbin"] focus' > /dev/null rig_key q ctrl
timeout 10 wtype -s 150 -M ctrl -s 100 -P q -s 100 -p q -m ctrl
for _ in $(seq 20); do if ! kill -0 "${app}" 2>/dev/null; then break; fi; sleep 1; done for _ in $(seq 20); do if ! kill -0 "${app}" 2>/dev/null; then break; fi; sleep 1; done
if kill -0 "${app}" 2>/dev/null; then echo 'FAIL Ctrl+Q did not exit' >&2; exit 1; fi if kill -0 "${app}" 2>/dev/null; then echo 'FAIL Ctrl+Q did not exit' >&2; exit 1; fi
wait "${app}" wait "${app}"