parked relay: a supervisor that keeps it alive + a REMOTE restart the operator can actually reach
Completes the travelling-operator deployment. The relay stays parked on the
home machine (so no player ever edits join.bat) and the console dials in from
anywhere -- but until now two things needed hands on that machine: bringing the
relay back when it died, and restarting it (the only way to clear a wedge or
pick up a roster resize). Both are covered now.
tools/btrelay_park.py -- the supervisor:
* runs the relay with cwd=content\ , which is what pins WHICH
operator_secret.txt is live (the trap documented last commit);
* relaunches on ANY exit, with 2/5/15/30/60s backoff when a relay dies inside
20s, so a permanent fault (port taken, missing egg) cannot become a spin;
* rotates content\parked_relay.log to .1 first, so the dead generation's
evidence survives the relaunch that replaces it;
* Ctrl-C stops it for good. NOT a Windows service on purpose: session 0
would hide the window an operator wants to tail.
tools/park_relay.cmd -- double-click to park; a shortcut to it in shell:startup
gives start-after-reboot with no admin rights.
`restart` on the control port (btconsole.py): sets restart_requested, the run
loop returns, relay_main exits RESTART_EXIT_CODE=42 and the supervisor relaunches
-- re-reading the egg. REFUSED while a mission is running (launches_sent >= 2
and not stop_sent): bouncing then would drop every pod out of a live round.
Local stdin gets the same command for parity.
btoperator.py: Restart Session in REMOTE mode now sends `restart` and reconnects
6s later instead of just tearing down our own link -- the old behaviour looked
like "Restart does nothing" against a parked relay. (QTimer had to be imported;
it was missing, which py_compile does not catch -- it would have been a runtime
NameError on the first click.)
VERIFIED by scratchpad/test_parked_supervisor.py, 12/12, and the full
test_remote_console_e2e.py re-run green afterwards (15/15):
* kill the relay -> a NEW pid is serving again, and the old log is kept as .1;
* remote `restart` -> ACKed, new pid, egg re-read, serving again;
* the supervisor distinguishes the two -- "operator requested a restart" vs
the crash/backoff path -- proven from its own log, not assumed;
* the mid-mission guard is present and wired to launches_sent/stop_sent.
Two harness defects fixed while proving it, both mine: a check written with
`or True` that could never fail (replaced with two real assertions against the
supervisor's log), and a recv that treated the relay's correct
close-after-restart as a ConnectionResetError failure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
05f1ffb194
commit
05fdd319d6
@@ -316,6 +316,8 @@ CONSOLE_MSG_MAX = 65536
|
||||
CONSOLE_INBUF_MAX = 1 << 20
|
||||
|
||||
CONTROL_PORT_OFFSET = 7 # control port = console port + 7
|
||||
RESTART_EXIT_CODE = 42 # relay_main's exit code for `restart`
|
||||
# (btrelay_park.py relaunches on it)
|
||||
CONTROL_AUTH_TIMEOUT = 10.0 # unauthenticated sockets die after this
|
||||
CONTROL_OUTBUF_MAX = 262144 # slow-reader cutoff (bytes)
|
||||
CONTROL_SET_KEYS = {"map", "time", "weather", "scenario", "temperature",
|
||||
@@ -547,6 +549,7 @@ class Relay:
|
||||
"udp_dropped": 0, "udp_tcp_fallback": 0}
|
||||
self.stats_at = time.time() + STATS_PERIOD
|
||||
self.ctl_conns = [] # RelayControlConn list
|
||||
self.restart_requested = False # remote `restart` (parked relay)
|
||||
self.ctl_secret = control_secret()
|
||||
|
||||
# ---------------- lifecycle ----------------
|
||||
@@ -617,6 +620,15 @@ class Relay:
|
||||
self._tick_stop()
|
||||
self._tick_settle()
|
||||
self._tick_stats()
|
||||
if self.restart_requested:
|
||||
# Remote `restart` (parked-relay deployment): leave the loop so
|
||||
# relay_main can exit with RESTART_EXIT_CODE and the supervisor
|
||||
# relaunches us with a fresh egg read. This is the ONLY way a
|
||||
# travelling operator can resize the roster or recover a wedged
|
||||
# relay -- the control channel cannot do either in-process.
|
||||
print("[relay] RESTART requested by the operator -- closing "
|
||||
"for the supervisor to relaunch", flush=True)
|
||||
return
|
||||
|
||||
def _listener(self, port):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
@@ -1866,6 +1878,21 @@ class Relay:
|
||||
# out of a latched round (operator report 2026-07-25).
|
||||
print(f"[ctl] RE-ARM from {conn.name()}", flush=True)
|
||||
self._rearm_for_new_round("operator re-arm command")
|
||||
elif cmd == "restart":
|
||||
# Parked-relay deployment: the operator is not at the machine, so
|
||||
# this is their only route to a fresh relay (roster resize, or a
|
||||
# wedge nothing else clears). Refused mid-mission -- it would
|
||||
# drop every pod out of a running round.
|
||||
if self.launches_sent >= 2 and not self.stop_sent:
|
||||
conn.queue_out(b"[ctl] restart REFUSED -- a mission is "
|
||||
b"running; press End Mission first\n")
|
||||
print("[ctl] restart refused (mission running)", flush=True)
|
||||
else:
|
||||
conn.queue_out(b"[ctl] restarting -- reconnect in a few "
|
||||
b"seconds\n")
|
||||
conn.flush_out()
|
||||
print(f"[ctl] RESTART from {conn.name()}", flush=True)
|
||||
self.restart_requested = True
|
||||
elif cmd == "ping":
|
||||
conn.queue_out(b"[ctl] pong\n")
|
||||
elif cmd == "get":
|
||||
@@ -2016,6 +2043,9 @@ def relay_main(argv):
|
||||
elif command in ("rearm", "newround"):
|
||||
print("[relay] operator RE-ARM command received", flush=True)
|
||||
relay._rearm_for_new_round("operator re-arm command")
|
||||
elif command == "restart":
|
||||
print("[relay] operator RESTART command received", flush=True)
|
||||
relay.restart_requested = True
|
||||
elif command:
|
||||
print(f"[relay] unknown operator command: {command!r}",
|
||||
flush=True)
|
||||
@@ -2024,6 +2054,9 @@ def relay_main(argv):
|
||||
relay.run()
|
||||
except KeyboardInterrupt:
|
||||
print("[relay] shutting down")
|
||||
return 0
|
||||
if relay.restart_requested:
|
||||
return RESTART_EXIT_CODE # the supervisor's cue to relaunch
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user