Files
Sylpheed/tools/re-capture/poke_control.sh
sylph-decoder 4c92f54439 re: make the one-emulator rule enforceable instead of remembered
The withdrawal last iteration was caused by `rm -f /tmp/xenia-canary.lock` --
the obvious way past a lock orphaned by kill -9, which also disables the guard
for every later launch. Three instances ended up live at once, sharing
/tmp/xenia_pad.txt and display :98, and silently confounded an input experiment.

Care is not a fix, so this is tooling. ensure_single_emulator.sh counts live
instances, stops them (plain kill, then -9, each with a bounded wait), verifies
ZERO, and only then removes the lock -- refusing loudly if any remain. The lock
is never removed before the condition it guards against is verified absent.

FOUR scripts did the bare `rm -f`, and only two were mine from today:
menu_loop_session.sh, title_draw_capture.sh, poke_control.sh and
resume_reliability.sh. So the footgun was corpus-wide rather than introduced
this session. All four now route through the guard.

The guard is controlled rather than assumed: run against a deliberately started
live instance it reports "1 instance(s) live -- stopping them", ends at 0 with
the lock cleared, and exits 0. A guard that only ever passes on an already-clean
slate would prove nothing.

It also kills by process NAME. `pkill -f xenia_canary` matches the shell running
it -- that has now cost this corpus three commands, one of them a cleanup that
died halfway and left the very instances it was meant to remove.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 16:23:59 +00:00

85 lines
3.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# POSITIVE CONTROL for the poke method: does a write to /dev/shm reach the guest?
#
# Two poke experiments produced no observable effect, and without this control
# "the game ignored it" cannot be told from "the write never arrived"
# (script-runtime-probe.md). The stick test only proves the value persisted in
# the shared-memory FILE.
#
# The control has to be something the GAME visibly reacts to. Hull is ideal:
# it lives at `player position + 0x154`, and dropping it to 1 should destroy the
# player -- an unambiguous screen change, no OCR needed.
#
# A SINGLE write is not enough: measured 2026-08-25, the game rewrote hull from
# 1 back to 1000.0f within 12 s, so one poke lands between two of the game's own
# writes and is gone before anything samples it. This HAMMERS the value in a
# tight loop for HAMMER_S seconds so it is low whenever the game looks.
#
# ship dies / GAME OVER -> the guest reads our writes, and hull is
# authoritative. The poke method is validated.
# nothing happens -> hull is a readout and the real copy is elsewhere;
# the method is still unproven, but that is a fact
# about the field rather than about the plumbing.
#
# Self-retrying, because roughly two runs in three freeze and a freeze was
# costing a whole iteration. Each attempt: boot, check the guest is animating,
# locate the player, poke, look. A freeze at any step costs a retry.
set -u
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
export PYTHONPATH=/sylph-home/.local/lib/python3.12/site-packages
SD="$(cd "$(dirname "$0")" && pwd)"; export SD
ATTEMPTS="${1:-3}"
alive_and_moving(){ python3 -c "
import sys; sys.path.insert(0,'$SD')
import frozen; d,_=frozen.frozen(5.0); sys.exit(1 if d else 0)"; }
for a in $(seq 1 "$ATTEMPTS"); do
echo "=== attempt $a/$ATTEMPTS ($(date +%T))"
pkill -9 -x xenia_canary 2>/dev/null; pkill -9 -f '[p]ilot.py' 2>/dev/null; sleep 2
. "$(dirname "${BASH_SOURCE[0]}")/ensure_single_emulator.sh"
ensure_single_emulator || exit 3
"$SD/launch_mission.sh" fly >/tmp/pc-boot.log 2>&1 || { echo " boot failed"; continue; }
if ! alive_and_moving; then echo " guest already frozen after boot"; continue; fi
pos=""
for t in 1 2 3; do
python3 "$SD/pad.py" set "rt=1" >/dev/null 2>&1; sleep 4
python3 "$SD/pad.py" clear >/dev/null 2>&1
pos=$(timeout 120 python3 "$SD/entities2.py" self 0x130 2>/dev/null \
| sed -n 's/.*pos va \(0x[0-9a-fA-F]*\).*/\1/p' | head -1)
[ -n "$pos" ] && break
echo " locate attempt $t: no player"
done
[ -n "$pos" ] || { echo " could not locate the player"; continue; }
if ! alive_and_moving; then echo " froze during locate"; continue; fi
hull=$((pos + 0x154))
echo " player pos $pos, hull $(printf '0x%X' $hull)"
before=$(python3 "$SD/gpoke.py" r32 "$hull" 1 2>/dev/null | tail -1)
echo " hull before: $before"
screenshot /tmp/pc-before.png >/dev/null 2>&1
HAMMER_S="${HAMMER_S:-15}"
echo " hammering hull=1 for ${HAMMER_S}s..."
python3 - "$hull" "$HAMMER_S" <<'PY'
import os, struct, sys, time
sys.path.insert(0, os.environ.get('SD', '.'))
import gmem
va = int(sys.argv[1]); secs = float(sys.argv[2])
off = gmem.va_to_off(va)
n = 0
with open(gmem.mem_path(), 'r+b', buffering=0) as f:
t0 = time.time()
while time.time() - t0 < secs:
f.seek(off); f.write(struct.pack('>I', 1)); n += 1
print(' wrote hull=1 %d times' % n)
PY
screenshot /tmp/pc-after.png >/dev/null 2>&1
echo " hull after : $(python3 "$SD/gpoke.py" r32 "$hull" 1 2>/dev/null | tail -1)"
echo " screen before: $(python3 "$SD/screen_id.py" /tmp/pc-before.png | head -1)"
echo " screen after : $(python3 "$SD/screen_id.py" /tmp/pc-after.png | head -1)"
python3 "$SD/frozen.py" --pair /tmp/pc-before.png /tmp/pc-after.png 2>&1 | tail -1
echo "CONTROL RAN"; exit 0
done
echo "CONTROL DID NOT RUN in $ATTEMPTS attempts"; exit 1