diff --git a/scripts/sold-client-ports.py b/scripts/sold-client-ports.py index 94a3768..2a0bc5a 100755 --- a/scripts/sold-client-ports.py +++ b/scripts/sold-client-ports.py @@ -1,27 +1,39 @@ #!/usr/bin/env python3 """Switch the FIFA client's Blaze ports between PRODUCTION and STAGING, reversibly. -The staging sold experiment needs the client to talk to the staging Blaze (which -advertises the staging UTAS base). The ONLY client change required is the two Blaze -port lines in `openfut.cfg`; `host` and `https_port` are left alone. +The staging sold experiment needs the client to talk to the staging Blaze, which is +what advertises the staging UTAS base. Two files are involved and the distinction +matters, because getting it wrong silently runs the whole experiment against +production: + + * `openfut.cfg` in the game directory is what the injected hook reads at connect + time -- but it is a DERIVED artifact. + * `~/.config/openfut-launcher/config.json` is the AUTHORITATIVE source. The + launcher reconciles openfut.cfg from it, fail-closed, immediately before every + launch. An edit to openfut.cfg alone is therefore destroyed by the next launch: + observed live, with openfut.cfg's mtime 2s before the FIFA process start and the + hook logging `blaze_redir=42127 blaze_main=42130` after it had been set to + staging. So this script owns BOTH, source first. Safety properties, in order of importance: - * The production values are recorded to a sidecar file on the client BEFORE the - first edit, and `restore` reads that sidecar rather than assuming what - production was. If the sidecar is missing, restore refuses. - * `restore` is idempotent and safe to run at any time, including after a crash. - * Every operation re-reads the file afterwards and prints it, so the result is + * Production values are recorded to a sidecar next to each file BEFORE the first + edit, and `restore` reads those sidecars rather than assuming what production + was. Missing sidecar means restore refuses. + * `restore` is idempotent and safe at any time, including after a crash. + * Every operation re-reads both files afterwards and prints them, so the result is verified rather than assumed. - * It refuses to edit while a FIFA client is running: the hook reads this file at - connect time, so changing it under a live session is unsafe. - * Only the two known keys are rewritten. Unknown lines are passed through - untouched, and a missing key is an error rather than a silent append. + * It refuses to edit while FIFA is running (the hook reads openfut.cfg at connect + time) AND while the launcher is running (the launcher holds its config in + memory and would write the stale values straight back over ours). + * Only known keys are rewritten. Unknown lines and unrelated JSON keys are passed + through untouched, and a missing key is an error rather than a silent append. python3 scripts/sold-client-ports.py show python3 scripts/sold-client-ports.py staging # 42327 / 42330 python3 scripts/sold-client-ports.py restore # back to the recorded values """ +import json import subprocess import sys @@ -30,6 +42,11 @@ CFG = "/mnt/games/FIFA 17/openfut.cfg" SIDECAR = "/mnt/games/FIFA 17/openfut.cfg.openfut-prod-ports" KEYS = ("blaze_redirector_port", "blaze_main_port") STAGING = {"blaze_redirector_port": "42327", "blaze_main_port": "42330"} +# The launcher is the real owner; openfut.cfg is regenerated from these. +LAUNCHER_CFG = "/home/alex/.config/openfut-launcher/config.json" +LAUNCHER_SIDECAR = "/home/alex/.config/openfut-launcher/config.json.openfut-prod-ports" +LAUNCHER_KEY = {"blaze_redirector_port": "openfut_blaze_redirector_port", + "blaze_main_port": "openfut_blaze_main_port"} def ssh(script): @@ -71,6 +88,52 @@ def fifa_running(): return bool(out) +def launcher_running(): + """True if openfut-launcher is up. Its `comm` is truncated by the kernel to 15 + chars ("openfut-launche"), which has bitten this project before, so match the + truncated form rather than the full name.""" + out = ssh( + "for d in /proc/[0-9]*; do c=$(cat $d/comm 2>/dev/null); " + "case \"$c\" in openfut-launche*) echo ${d#/proc/};; esac; done || true" + ).strip() + return bool(out) + + +def read_launcher(): + return ssh(f'cat "{LAUNCHER_CFG}"') + + +def launcher_ports(text): + d = json.loads(text) + return {k: str(d.get(LAUNCHER_KEY[k])) for k in KEYS} + + +def write_launcher(values): + """Rewrite only the two port keys, preserving every other setting and the file's + formatting conventions, then verify by re-reading.""" + text = read_launcher() + d = json.loads(text) + for k in KEYS: + if LAUNCHER_KEY[k] not in d: + raise SystemExit( + f"key {LAUNCHER_KEY[k]!r} absent from {LAUNCHER_CFG}; refusing to guess") + existing = ssh(f'cat "{LAUNCHER_SIDECAR}" 2>/dev/null || true').strip() + if not existing: + rec = "\n".join(f"{k}={d[LAUNCHER_KEY[k]]}" for k in KEYS) + ssh(f'cat > "{LAUNCHER_SIDECAR}" <<\'EOF\'\n{rec}\nEOF') + print(f"recorded launcher production ports to {LAUNCHER_SIDECAR}:\n{rec}") + for k, v in values.items(): + d[LAUNCHER_KEY[k]] = int(v) + body = json.dumps(d, indent=2, sort_keys=True) + ssh(f'cat > "{LAUNCHER_CFG}" <<\'EOF\'\n{body}\nEOF') + after = launcher_ports(read_launcher()) + for k, v in values.items(): + if after.get(k) != str(v): + raise SystemExit( + f"VERIFY FAILED in launcher config: {k} is {after.get(k)!r}, want {v!r}") + print(f"--- launcher config now: {after}") + + def show(): text = read_cfg() print(f"--- {CFG}") @@ -79,16 +142,30 @@ def show(): print("--- blaze ports:", {k: cur.get(k) for k in KEYS}) side = ssh(f'cat "{SIDECAR}" 2>/dev/null || true').strip() print("--- recorded production ports:", side or "(none recorded yet)") + print(f"--- {LAUNCHER_CFG} (authoritative)") + print("--- launcher blaze ports:", launcher_ports(read_launcher())) + lside = ssh(f'cat "{LAUNCHER_SIDECAR}" 2>/dev/null || true').strip() + print("--- recorded launcher production ports:", lside or "(none recorded yet)") print("--- FIFA client running:", "YES" if fifa_running() else "no") + print("--- launcher running:", "YES" if launcher_running() else "no") return cur def write_ports(values, label): if fifa_running(): raise SystemExit( - "REFUSING to edit openfut.cfg while a FIFA client is running.\n" - "The hook reads this file at connect time; exit FIFA first." + "REFUSING to edit while a FIFA client is running.\n" + "The hook reads openfut.cfg at connect time; exit FIFA first." ) + if launcher_running(): + raise SystemExit( + "REFUSING to edit while openfut-launcher is running.\n" + "The launcher holds its config in memory and reconciles openfut.cfg from\n" + "it fail-closed before every launch, so it would write the production\n" + "ports straight back over ours. Quit the launcher first." + ) + # Source of truth first, then the derived file. + write_launcher(values) text = read_cfg() cur = parse(text) for k in KEYS: