#!/usr/bin/env python3 """btrelay_park.py -- keep the relay PARKED and alive on the host machine. The travelling-operator deployment: the relay lives permanently on one machine so every player's join.bat address never changes, and the operator console dials into its control port from anywhere. For that to be true the relay has to outlive crashes, reboots and roster changes without anyone at the keyboard -- which is what this supervisor is for. python tools/btrelay_park.py [--egg OPERATOR.EGG] [--port 1500] [--manual-launch] [--once] What it does: * starts the relay with cwd = content\\ (the working directory decides which operator_secret.txt is live -- see context/operator-console.md); * relaunches it when it exits, whether that was a crash or the operator's remote `restart` (RESTART_EXIT_CODE), with a short backoff so a relay that dies instantly cannot spin the CPU; * writes everything to content\\parked_relay.log, rotating the previous run to .1 so a crash's evidence survives the relaunch; * stops cleanly on Ctrl-C, taking the relay with it. Deliberately NOT a Windows service: a service runs in session 0 and the relay is something the operator wants to see, tail and Ctrl-C. For start-at-boot, put a shortcut to tools\\park_relay.cmd in the Startup folder (shell:startup) -- no elevation, trivially removable. """ import argparse import os import subprocess import sys import time REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) CONTENT = os.path.join(REPO, "content") RESTART_EXIT_CODE = 42 # must match tools/btconsole.py # A relay that dies faster than this was not really running: back off so a # permanent fault (port in use, missing egg) cannot become a spin loop. HEALTHY_SECONDS = 20 BACKOFF = [2, 5, 15, 30, 60] def rotate(path): try: if os.path.exists(path): prev = path + ".1" if os.path.exists(prev): os.remove(prev) os.replace(path, prev) except OSError: pass # a locked log must never stop the relay def main(): ap = argparse.ArgumentParser() ap.add_argument("--egg", default="OPERATOR.EGG") ap.add_argument("--port", type=int, default=1500) ap.add_argument("--manual-launch", action="store_true", default=True, help="hold the launch for the operator (the default)") ap.add_argument("--auto-launch", dest="manual_launch", action="store_false", help="let the relay launch by itself once all pods stage") ap.add_argument("--once", action="store_true", help="do not relaunch -- run the relay a single time") args = ap.parse_args() egg = args.egg if os.path.isabs(args.egg) \ else os.path.join(CONTENT, args.egg) if not os.path.exists(egg): sys.exit("no such egg: %s" % egg) secret = os.path.join(CONTENT, "operator_secret.txt") log_path = os.path.join(CONTENT, "parked_relay.log") print("=" * 68) print(" BattleTech relay -- PARKED") print(" egg : %s" % egg) print(" ports : console %d | game %d | operator control %d" % (args.port, args.port + 1, args.port + 7)) print(" secret : %s%s" % (secret, "" if os.path.exists(secret) else " (will be generated)")) print(" log : %s (previous run kept as .1)" % log_path) print(" launch : %s" % ("operator presses LAUNCH" if args.manual_launch else "automatic")) print(" Ctrl-C here stops the relay for good; everything else") print(" (crash, or the operator's remote `restart`) relaunches it.") print("=" * 68, flush=True) cmd = [sys.executable, "-u", os.path.join(REPO, "tools", "btconsole.py"), "--relay", str(args.port), egg] if args.manual_launch: cmd.append("--manual-launch") generation = 0 strikes = 0 while True: generation += 1 rotate(log_path) started = time.time() print("[park] generation %d starting %s" % (generation, time.strftime("%H:%M:%S")), flush=True) try: with open(log_path, "w", encoding="utf-8") as log: proc = subprocess.Popen(cmd, cwd=CONTENT, stdout=log, stderr=subprocess.STDOUT) code = proc.wait() except KeyboardInterrupt: print("\n[park] Ctrl-C -- stopping the parked relay", flush=True) try: proc.terminate() except Exception: pass return 0 except OSError as e: print("[park] could not start the relay: %r" % e, flush=True) return 1 lived = time.time() - started if args.once: print("[park] --once: relay exited %d after %.0fs" % (code, lived), flush=True) return code if code == RESTART_EXIT_CODE: strikes = 0 # an ASKED-FOR restart is healthy print("[park] operator requested a restart -- relaunching " "(the egg is re-read, so roster changes take effect)", flush=True) time.sleep(1) continue # Anything else is a death: crash, killed, or a startup failure. if lived >= HEALTHY_SECONDS: strikes = 0 wait = BACKOFF[min(strikes, len(BACKOFF) - 1)] strikes += 1 print("[park] relay exited %d after %.0fs -- relaunching in %ds " "(evidence in %s.1)" % (code, lived, wait, log_path), flush=True) try: time.sleep(wait) except KeyboardInterrupt: print("\n[park] Ctrl-C -- staying down", flush=True) return 0 if __name__ == "__main__": try: sys.exit(main()) except KeyboardInterrupt: sys.exit(0)