qemu: review pass over the rig driver and boot script
Four review passes with fixes between them (flare-edge's flow-framework review, 2026-09-09). qmp.py: drive() split out of a 330-line dispatcher, every verb guarded so a raising verb records a fatal row instead of ending the run, the shot path sanitised, the rs485 and wait verbs judged through shared helpers; imgtools.py: a bench subcommand for phash/structural timings and a colour probe that samples instead of scanning the frame; ui-drive.sh: the boot poll no longer walks every pixel per tick and the simulator's control socket path is passed as one word. Offline tests: 8. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3G6m9Aw5RyVY4ZowtKzEj
This commit is contained in:
+71
-10
@@ -47,7 +47,7 @@ while [ $# -gt 0 ]; do
|
||||
--refs) REFS_FILE="${2:?--refs needs a path}"; shift 2 ;;
|
||||
--rs485-devices) RS485_DEVICES="${2:?--rs485-devices needs ADDR:SLUG[,...]}"; shift 2 ;;
|
||||
--) shift; ARGS+=("$@"); break ;;
|
||||
-*) echo "FATAL: unknown option '$1' (usage: $0 [--seed FILE] [--refs FILE] <zImage> <script> [out-dir])" >&2; exit 1 ;;
|
||||
-*) echo "FATAL: unknown option '$1' (usage: $0 [--seed FILE] [--refs FILE] [--rs485-devices LIST] <zImage> <script> [out-dir])" >&2; exit 1 ;;
|
||||
*) ARGS+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
@@ -56,7 +56,7 @@ SCRIPT="${ARGS[1]:-}"
|
||||
OUTDIR="${ARGS[2]:-$QDIR/out/ui-drive}"
|
||||
|
||||
if [ -z "$ZIMAGE" ] || [ ! -f "$ZIMAGE" ]; then
|
||||
echo "FATAL: usage: $0 [--seed FILE] [--refs FILE] <zImage> <script> [out-dir]: the virt.fragment kernel variant" >&2
|
||||
echo "FATAL: usage: $0 [--seed FILE] [--refs FILE] [--rs485-devices LIST] <zImage> <script> [out-dir]: the virt.fragment kernel variant" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$SCRIPT" ] || [ ! -f "$SCRIPT" ]; then
|
||||
@@ -100,12 +100,26 @@ export OUT="$RUN_OUT"
|
||||
export BUSYBOX="${BUSYBOX:-$QDIR/out/busybox-armv7l}"
|
||||
QEMU_PID=""
|
||||
SIM_PIDS=""
|
||||
# SIGTERM first (lets qemu/socat/mbsim.py close their sockets and pty
|
||||
# cleanly), then SIGKILL if that didn't take: a process wedged on a full pty
|
||||
# buffer or an uninterruptible I/O wait under host load must not outlive this
|
||||
# script, or a long batch (flow-run-all.sh --jobs N) slowly accumulates
|
||||
# orphaned simulator/VM processes competing with the runs still in flight.
|
||||
reap() {
|
||||
local p="$1"
|
||||
kill "$p" 2>/dev/null || return 0
|
||||
for _i in $(seq 1 20); do
|
||||
kill -0 "$p" 2>/dev/null || return 0
|
||||
sleep 0.1
|
||||
done
|
||||
kill -KILL "$p" 2>/dev/null || true
|
||||
}
|
||||
cleanup() {
|
||||
for p in $SIM_PIDS; do kill "$p" 2>/dev/null || true; done
|
||||
for p in $SIM_PIDS; do reap "$p"; done
|
||||
if [ -n "$QEMU_PID" ]; then
|
||||
python3 "$HERE/qmp.py" "$WORK/qmp.sock" quit 2>/dev/null || true
|
||||
sleep 1
|
||||
kill "$QEMU_PID" 2>/dev/null || true
|
||||
reap "$QEMU_PID"
|
||||
fi
|
||||
cp "$WORK/console.log" "$OUTDIR/console.log" 2>/dev/null || true
|
||||
cp "$WORK/mbsim.log" "$OUTDIR/mbsim.log" 2>/dev/null || true
|
||||
@@ -160,16 +174,46 @@ if [ -n "$RS485_DEVICES" ]; then
|
||||
[ -n "${FLARE_EDGE:-}" ] && [ -f "$FLARE_EDGE/tools/modbus-sim/mbsim.py" ] || {
|
||||
echo "FATAL: --rs485-devices needs FLARE_EDGE to point at a flare-edge checkout (mbsim.py)" >&2; exit 1; }
|
||||
for _i in $(seq 1 50); do [ -S "$WORK/rs.sock" ] && break; sleep 0.1; done
|
||||
[ -S "$WORK/rs.sock" ] || {
|
||||
echo "FATAL: rs485 bus socket never appeared at $WORK/rs.sock (qemu's --rs485 chardev never came up)" >&2
|
||||
tail -25 "$WORK/console.log" >&2
|
||||
exit 1
|
||||
}
|
||||
socat "UNIX-CONNECT:$WORK/rs.sock" "PTY,link=$WORK/rs.pty,raw,echo=0" > "$WORK/socat.log" 2>&1 &
|
||||
SIM_PIDS="$SIM_PIDS $!"
|
||||
for _i in $(seq 1 50); do [ -e "$WORK/rs.pty" ] && break; sleep 0.1; done
|
||||
socat_pid=$!
|
||||
SIM_PIDS="$SIM_PIDS $socat_pid"
|
||||
# Break out the moment socat dies rather than always spending the full
|
||||
# poll budget: a socat that never links the pty is usually already gone
|
||||
# (bad UNIX-CONNECT target, no pty node available), and kill -0 catches
|
||||
# that in one tick instead of five seconds.
|
||||
for _i in $(seq 1 50); do
|
||||
[ -e "$WORK/rs.pty" ] && break
|
||||
kill -0 "$socat_pid" 2>/dev/null || break
|
||||
sleep 0.1
|
||||
done
|
||||
[ -e "$WORK/rs.pty" ] || {
|
||||
echo "FATAL: rs485 socat never created rs.pty (see $WORK/socat.log)" >&2
|
||||
cat "$WORK/socat.log" >&2
|
||||
exit 1
|
||||
}
|
||||
dev_args=()
|
||||
IFS=',' read -r -a _devs <<< "$RS485_DEVICES"
|
||||
for d in "${_devs[@]}"; do dev_args+=(--device "$d"); done
|
||||
# --control is the runtime lever: qmp.py's `rs485 silence|restore ADDR`
|
||||
# verb talks to it, so a flow can take a device off the bus mid-run.
|
||||
python3 "$FLARE_EDGE/tools/modbus-sim/mbsim.py" --port "$WORK/rs.pty" --control "$WORK/rs.ctl" "${dev_args[@]}" > "$WORK/mbsim.log" 2>&1 &
|
||||
SIM_PIDS="$SIM_PIDS $!"
|
||||
mbsim_pid=$!
|
||||
SIM_PIDS="$SIM_PIDS $mbsim_pid"
|
||||
for _i in $(seq 1 50); do
|
||||
[ -S "$WORK/rs.ctl" ] && break
|
||||
kill -0 "$mbsim_pid" 2>/dev/null || break
|
||||
sleep 0.1
|
||||
done
|
||||
{ [ -S "$WORK/rs.ctl" ] && kill -0 "$mbsim_pid" 2>/dev/null; } || {
|
||||
echo "FATAL: rs485 simulator (mbsim.py) never came up (see $WORK/mbsim.log)" >&2
|
||||
cat "$WORK/mbsim.log" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "== rs485 simulator: $RS485_DEVICES on $WORK/rs.pty"
|
||||
fi
|
||||
|
||||
@@ -189,15 +233,32 @@ grep -aq 'init: starting warden-ui' "$WORK/console.log" || {
|
||||
# A started process is not a rendered frame. Poll screendumps until the panel
|
||||
# stops being a single flat colour, on a bounded deadline: TCG renders CPU-bound
|
||||
# and a loaded host can be arbitrarily slow, so this is never a fixed sleep.
|
||||
#
|
||||
# The count only has to clear a fixed threshold (32), not be exact, so this
|
||||
# samples every 32nd pixel instead of all ~518K (720x720) of them, and bails
|
||||
# the moment the threshold clears instead of always finishing the scan. A
|
||||
# still-flat splash reads identically under any stride -- every sampled pixel
|
||||
# is the same colour either way, so sampling can never manufacture a false
|
||||
# "ready" -- and a rendered frame has enough variety that a sparse sample
|
||||
# clears 32 distinct colours just as reliably. This loop runs on the same
|
||||
# host CPU the comment above names as the bottleneck, on every poll tick of
|
||||
# every flow's boot, so the per-tick cost matters; a plain byte loop here also
|
||||
# avoids adding a Pillow dependency to a boot check that today has none (see
|
||||
# qmp.py's own imgtools/Pillow deferral -- a plain nav/tap flow must keep
|
||||
# booting on a host without Pillow installed).
|
||||
echo "== waiting for the first real frame"
|
||||
deadline=$((SECONDS + 180))
|
||||
ready=0
|
||||
while [ $SECONDS -lt $deadline ]; do
|
||||
python3 "$HERE/qmp.py" "$WORK/qmp.sock" screendump "$WORK/probe.ppm" 2>/dev/null || { sleep 2; continue; }
|
||||
colors="$(python3 -c "
|
||||
import sys
|
||||
d=open('$WORK/probe.ppm','rb').read()
|
||||
print(len(set(d[i:i+3] for i in range(15, len(d), 3))))
|
||||
d = open('$WORK/probe.ppm', 'rb').read()
|
||||
colors = set()
|
||||
for i in range(15, len(d), 32 * 3):
|
||||
colors.add(d[i:i+3])
|
||||
if len(colors) > 32:
|
||||
break
|
||||
print(len(colors))
|
||||
" 2>/dev/null || echo 0)"
|
||||
[ "${colors:-0}" -gt 32 ] && { ready=1; break; }
|
||||
sleep 3
|
||||
|
||||
Reference in New Issue
Block a user