#!/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. 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 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. 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 subprocess import sys CLIENT = "alex@10.10.0.105" 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"} def ssh(script): r = subprocess.run(["ssh", "-o", "BatchMode=yes", CLIENT, script], capture_output=True, text=True, timeout=60) if r.returncode != 0: raise SystemExit(f"ssh failed ({r.returncode}): {r.stderr.strip()}") return r.stdout def read_cfg(): return ssh(f'cat "{CFG}"') def parse(text): out = {} for line in text.splitlines(): if "=" in line and not line.strip().startswith("#"): k, _, v = line.partition("=") out[k.strip()] = v.strip() return out def fifa_running(): """True if a real FIFA 17 process exists on the client. Matches /proc//comm exactly rather than `pgrep -f FIFA17.exe`: the pattern form self-matched the remote shell running it (the SSH command line contains the literal string), so the guard was permanently stuck ON and could never report "not running". comm is the executable name, so the shell reads as zsh/bash and only a genuine FIFA process matches. Still fail-closed: any read error or unexpected output is treated as "running". """ out = ssh( "for d in /proc/[0-9]*; do " "[ -r \"$d/comm\" ] && [ \"$(cat $d/comm 2>/dev/null)\" = FIFA17.exe ] " "&& echo ${d#/proc/}; done || true" ).strip() return bool(out) def show(): text = read_cfg() print(f"--- {CFG}") print(text.rstrip()) cur = parse(text) 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("--- FIFA client running:", "YES" if fifa_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." ) text = read_cfg() cur = parse(text) for k in KEYS: if k not in cur: raise SystemExit(f"key {k!r} is absent from {CFG}; refusing to guess it") # Record production values once, before the first mutation, so restore never # has to assume what they were. existing_sidecar = ssh(f'cat "{SIDECAR}" 2>/dev/null || true').strip() if not existing_sidecar and label == "staging": rec = "\n".join(f"{k}={cur[k]}" for k in KEYS) ssh(f'cat > "{SIDECAR}" <<\'EOF\'\n{rec}\nEOF') print(f"recorded production ports to {SIDECAR}:\n{rec}") out = [] for line in text.splitlines(): k = line.partition("=")[0].strip() out.append(f"{k}={values[k]}" if k in values else line) body = "\n".join(out) + "\n" ssh(f'cat > "{CFG}" <<\'EOF\'\n{body.rstrip()}\nEOF') after = parse(read_cfg()) for k, v in values.items(): if after.get(k) != v: raise SystemExit(f"VERIFY FAILED: {k} is {after.get(k)!r}, expected {v!r}") print(f"--- now pointing at {label}") print(read_cfg().rstrip()) def main(): if len(sys.argv) != 2 or sys.argv[1] not in ("show", "staging", "restore"): print(__doc__) return 2 cmd = sys.argv[1] if cmd == "show": show() return 0 if cmd == "staging": write_ports(STAGING, "staging") return 0 side = ssh(f'cat "{SIDECAR}" 2>/dev/null || true').strip() if not side: raise SystemExit( f"no recorded production ports at {SIDECAR}; refusing to guess.\n" "Set them by hand and verify with `show`." ) write_ports(parse(side), "production (restored)") return 0 if __name__ == "__main__": sys.exit(main())