Bridge warden-ui's debug channel out of the qemu VM

The rig could drive the UI and detect one outcome: the process died. Nothing
could ask the UI what page it was on or what a tap would land on, because
that channel is a FIFO inside the guest and the initramfs is busybox-only
with no sshd. Scenarios therefore asserted nothing and screenshots went
unread.

run.sh --ctl exposes a second pci-serial port as a unix socket, the same
device the RS485 bridge already rides, listed first so it is always ttyS0.
It also puts warden.ctl on the kernel command line, and init bridges only
when that marker is present: a VM launched with --rs485 alone has a ttyS0
too, and that one is the Modbus wire. The bridge relays one command line in
and the FIFO's reply out, then a sentinel so the reader needs no timeout.

qmp.py gains the channel verbs (nav, page, stats, hit, assert_page,
assert_hit), records every step to results.jsonl as ok/fail/fatal, continues
past an assertion mismatch so one run reports every broken expectation, and
checks the console after EVERY step for the stage-2 init's EXITED line so a
crash is pinned to the step that caused it. assert_hit matches the widget's
bounding box: an icon has no usable caption and two list rows share a class,
but the geometry the UI itself resolved is exact.

The vocabulary is what tools/warden-ctl already speaks over SSH to a real
panel, so a script that runs here runs there. Verified end to end on the rig
(11/11 verbs round-tripped) and against the bench panel, where the same
commands returned byte-identical results.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013aHKWzT5EF86RFKRMtAv9n
This commit is contained in:
Noah
2026-09-07 21:54:52 -06:00
co-authored by Claude Fable 5.1
parent 3ea6c837e3
commit 8a57057053
4 changed files with 278 additions and 13 deletions
+44
View File
@@ -120,6 +120,50 @@ if [ -x /usr/bin/warden-ui ] && [ -c /dev/fb0 ]; then
) &
fi
# Control bridge: the UI's debug FIFO, reachable from OUTSIDE the VM.
#
# warden-ui answers nav/page/stats/hit on /tmp/warden-ui.ctl (warden_debug.c),
# but that FIFO lives in here and a scenario drives the VM from the host. On
# real hardware the same channel is reached over SSH (flare-edge
# tools/warden-ctl); this initramfs is busybox-only and has no sshd, so the
# equivalent seam is a second 16550 that run.sh --ctl exposes as a unix socket
# (pci-serial, the same device the RS485 bridge already rides). One command
# per line in, the FIFO's reply out, and a sentinel line so the reader knows
# the reply is complete without a timeout. The vocabulary is identical on both
# sides of that seam, which is what lets one flow script run against the sim
# and against a panel.
#
# run.sh lists the ctl port before any other pci-serial, so it is always the
# first 8250, and it says so with warden.ctl on the command line. The marker,
# not the mere presence of a ttyS0, is what arms the bridge: a VM launched
# with --rs485 alone also has a ttyS0, and that one is the Modbus wire.
if grep -qw warden.ctl /proc/cmdline && [ -c /dev/ttyS0 ]; then
ctl=/dev/ttyS0
echo "init: control bridge on $ctl"
(
# Opened ONCE, read-write, on fd 3. Reopening a serial port per line
# can block on carrier detect; one open at bridge start either works
# or fails visibly on the console. The tty stays in its default cooked
# mode: the host discards echoed input, and a whole line arrives per
# read.
exec 3<> "$ctl"
while IFS= read -r cmd <&3; do
[ -n "$cmd" ] || continue
if [ -p /tmp/warden-ui.ctl ]; then
printf '%s\n' "$cmd" > /tmp/warden-ui.ctl
# The UI polls its FIFO every 100 ms and truncates the reply
# file on each command, so a short settle then a read is the
# same protocol warden-ctl uses over SSH.
sleep 0.3
cat /tmp/warden-ui.dbg 2>/dev/null >&3
else
echo "bridge: warden-ui control FIFO not present" >&3
fi
echo "<<END>>" >&3
done
) &
fi
if grep -qw warden.shell /proc/cmdline; then
echo "warden.shell: interactive shell (exit powers off)"
setsid cttyhack sh