fbe29da05b
`pgrep -f FIFA17.exe` matched the remote shell executing it -- the SSH command line contains the literal pattern -- so fifa_running() always returned True and the port switcher could never edit openfut.cfg. It refused with "REFUSING to edit ... while a FIFA client is running" moments after FIFA had actually exited. Fail-closed, so nothing unsafe happened, but the guard was permanently stuck and blocked the A/B entirely. Now matches /proc/<pid>/comm exactly, which is the executable name: the invoking shell reads as zsh and cannot self-match, while a genuine FIFA process still does. Validated both directions with the same loop -- it found pid 36958 while FIFA was up, and reports gone once it exited. Still fail-closed on read errors. The lesson generalises: a pattern-matching process guard checked over a transport that carries the pattern in its own argv is self-satisfying, and a guard that can only ever say "yes" is not a guard.
142 lines
5.2 KiB
Python
Executable File
142 lines
5.2 KiB
Python
Executable File
#!/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/<pid>/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())
|