THE SYMPTOM: "after a game ends I push LAUNCH and nothing happens until I reset
the session entirely or reboot the console." The operator also observed the
mirror image, which is the tell: with a STABLE group, relaunching worked fine
several times in a row.
ROOT CAUSE. Two gates, both all-or-nothing, and a UI latch that agreed with
them:
* btconsole.py: the manual LAUNCH branch is guarded on `launches_sent == 0`,
but a finished mission leaves it at 2. The only paths back to 0 were
_check_launch_gate (needs EVERY seat of the last round to re-ACK) and
_maybe_reset_round (needs EVERY pod gone). A games night lives between
those -- most pods rejoin, one player closes their window. Worse, the
"not all seats filled" diagnostic sits INSIDE the `launches_sent == 0`
guard, so in exactly that state NOTHING was printed.
* btoperator.py: the LAUNCH button is gated on `not monitor.launched`, and
`launched` was cleared ONLY by the two relay lines those same gates emit
("WAITING FOR OPERATOR", "round RESET"). So the button was greyed out in
precisely the wedged state -- the click was a no-op by construction.
That is why a stable group worked (everyone re-ACKs -> gate fires) and why only
a session restart recovered (fresh relay process = fresh state).
FIXES
* An explicit operator LAUNCH is now sufficient authority to start a new
round: _rearm_for_new_round() clears the finished round's state and restores
the template roster/egg, KEEPING seats/beacons/connections, so whoever is
here stays here. Triggered on a press while a round is latched.
* New `rearm`/`newround` operator command + a Re-arm button, so recovery never
needs a session restart. Proven live over the control port.
* Every operator command is logged AT RECEIPT with the state that decides its
fate, so "did it arrive or was it ignored?" is answerable from the log.
* A stale End Mission can no longer kill the next mission: _tick_stop consumed
a stop_requested set between rounds by latching it until launches_sent hit 2,
then StopMissioning the new mission in the tick it launched -- also
indistinguishable from "launch did nothing". It is now consumed + announced
while idle. The UI's end_sent likewise reset per round, not per session
(End Mission used to work exactly once a night).
* The UI clears `launched` on "StopMission sent" and on the re-arm line, so the
button returns when the round actually ends, and only greys once a command
has really been written (a dead relay now says so instead of logging
">> sent" into the void -- _console_finished leaves console_proc non-None).
HARDENING (the "reboot the console" half)
* SO_KEEPALIVE on every accepted socket + a last_seen deadline and a reaper:
nothing detected a pod that vanished WITHOUT a FIN (sleeping laptop, dropped
Wi-Fi, killed process, NAT timeout). Such a conn kept acked=True forever,
which permanently blocked the round reset and held a phantom seat. The
reaper stands down while a mission is running.
* Every pod-facing send went blocking with NO timeout on the single-threaded
selector loop, so one wedged peer could freeze the entire relay. All sends
now go through _send_all_guarded (10s timeout, drops the peer on failure).
* _send_egg no longer calls getpeername() on a possibly-dead socket.
REGRESSION I INTRODUCED AND CAUGHT ON THE RIG: the first cut re-armed whenever
launches_sent >= 1, which also matched the NORMAL state between RunMission #1 and
#2 (launch_requested deliberately stays set across the pair). That reset the
pair mid-flight and re-released eggs every tick -- a re-arm storm that kicked
every pod into an identity-resync loop. The re-arm now additionally requires
`launch_at is None`, i.e. no launch sequence in flight. Verified: 0 REJOIN lines
and exactly 1 RE-ARM line across a full 3-mission rig session.
VERIFIED
* scratchpad/test_relay_rearm.py -- 6 groups, all passing: the exact wedge
state recovers; a stale stop is consumed while idle; staging is NOT restarted
under an impatient operator; mid-pair never re-arms; the happy path is
untouched and RunMission still reaches the pods; the reaper drops a silent
conn and keeps a live one, and stands down mid-mission.
* Live 2-pod rig (scratchpad/rig_relay.ps1 + relay_ctl.py over the control
port): two clean back-to-back missions, StopMission, round RESET, a live
`rearm`, and LAUNCH-with-nobody-present now printing why instead of nothing.
NOT reproduced end-to-end: the field wedge needs 3+ pods with staggered rejoins
(this rig's pods re-exec together, so the all-gone reset always fires). The
state itself is covered by the unit test. Awaiting live confirmation on a real
games night.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
49 lines
2.0 KiB
PowerShell
49 lines
2.0 KiB
PowerShell
# rig_relay.ps1 -- end-to-end back-to-back mission regression for the relay fix.
|
|
#
|
|
# Starts the REAL relay (btconsole.py --relay --manual-launch) plus two real pods.
|
|
# Operator commands are then issued over the relay's CONTROL PORT (1507) by
|
|
# ctl.py -- the same channel a remote operator uses, which also exercises the new
|
|
# 'rearm' command. Records only the PIDs it spawns; teardown kills exactly those.
|
|
|
|
$ErrorActionPreference = 'Continue'
|
|
$repo = 'C:\git\bt411'
|
|
$content = Join-Path $repo 'content'
|
|
$exe = Join-Path $repo 'build\Release\btl4.exe'
|
|
$scratch = 'C:\Users\EPILECTRIK\AppData\Local\Temp\claude\C--git-bt411\89ab96e9-6cf0-4555-939d-05bf3708745a\scratchpad'
|
|
$pidfile = Join-Path $scratch 'rig_relay_pids.txt'
|
|
$relaylog = Join-Path $scratch 'rig_relay.log'
|
|
|
|
$pre = @(Get-Process -Name btl4 -ErrorAction SilentlyContinue)
|
|
if ($pre.Count -gt 0) { "ABORT: btl4 already running ($($pre.Id -join ','))"; exit 1 }
|
|
|
|
Remove-Item $relaylog -ErrorAction SilentlyContinue
|
|
$pids = @()
|
|
|
|
$relay = Start-Process -FilePath 'python' `
|
|
-ArgumentList '-u', (Join-Path $repo 'tools\btconsole.py'), '--relay', '1500',
|
|
(Join-Path $content 'FOGDAY.EGG'), '--bind', '127.0.0.1', '--manual-launch' `
|
|
-WorkingDirectory $content -PassThru `
|
|
-RedirectStandardOutput $relaylog `
|
|
-RedirectStandardError (Join-Path $scratch 'rig_relay.err')
|
|
$pids += $relay.Id
|
|
Start-Sleep -Seconds 3
|
|
"relay pid=$($relay.Id) log=$relaylog"
|
|
|
|
$env:BT_START_INSIDE = '1'
|
|
$env:BT_DEV_GAUGES = '1'
|
|
$env:BT_RELAY = '127.0.0.1:1500'
|
|
$env:BT_MATCHLOG = '0'
|
|
|
|
$env:BT_LOG = 'r_a.log'; $env:BT_SELF = '127.0.0.1:1502'
|
|
$a = Start-Process -FilePath $exe -ArgumentList '-egg','FOGDAY.EGG','-net','1502' `
|
|
-WorkingDirectory $content -PassThru
|
|
$pids += $a.Id
|
|
Start-Sleep -Seconds 2
|
|
$env:BT_LOG = 'r_b.log'; $env:BT_SELF = '127.0.0.1:1602'
|
|
$b = Start-Process -FilePath $exe -ArgumentList '-egg','FOGDAY.EGG','-net','1602' `
|
|
-WorkingDirectory $content -PassThru
|
|
$pids += $b.Id
|
|
|
|
Set-Content -Path $pidfile -Value $pids
|
|
"pods: a=$($a.Id) b=$($b.Id) (pids recorded in $pidfile)"
|