#!/usr/bin/env python3 """Serve the MIGRATED + RECLASSIFIED rehearsal copy with the candidate Rust stack and validate the wire surface. Isolated ports, copy-only state, no production or staging resource touched.""" import json import os import signal import subprocess import time import urllib.error import urllib.request R = "/home/alex/openfut-migration/rehearsal-20260822" REPO = "/home/alex/OpenFUT" CORE_PORT, HOST_PORT = 18099, 18098 H = {"X-OpenFUT-Game": "fifa17"} procs = [] def start(name, binary, env): e = dict(os.environ) e.update(env) log = open("%s/evidence/%s.log" % (R, name), "w") p = subprocess.Popen([binary], cwd=REPO, env=e, stdout=log, stderr=log, start_new_session=False) procs.append(p) return p def get(port, path): req = urllib.request.Request("http://127.0.0.1:%d%s" % (port, path), headers=H) with urllib.request.urlopen(req, timeout=25) as r: return json.loads(r.read()) try: start("rehearsal-core", R + "/artifacts/openfut-core", { "LISTEN_ADDR": "127.0.0.1:%d" % CORE_PORT, "DATABASE_URL": "sqlite://%s/work/core.db" % R, "DATA_DIR": "%s/openfut-core/data" % REPO, "OPENFUT_CONTENT_PACKS": "%s/emit/content/fifa17-production-cards.json" % R, "RUST_LOG": "warn", }) time.sleep(6) start("rehearsal-host", R + "/artifacts/openfut-utas-host", { "OPENFUT_UTAS_HOST_ADDR": "127.0.0.1:%d" % HOST_PORT, "OPENFUT_UTAS_PYTHON_URL": "http://127.0.0.1:19999", # deliberately dead "OPENFUT_CORE_URL": "http://127.0.0.1:%d" % CORE_PORT, "OPENFUT_FIFA17_TABLES_DIR": "%s/fifa17-recon/data/tables" % REPO, "OPENFUT_FIFA17_CATALOG": "%s/emit/content/fifa17-production-catalog.json" % R, "OPENFUT_IDENTITY_STORE": "%s/work/identity.json" % R, "OPENFUT_PERSONA_ID": "33068179", "OPENFUT_MARKET_DB": "%s/work/market.db" % R, "OPENFUT_PILE_DB": "%s/work/pile.db" % R, "OPENFUT_FIFA17_DISCARD_TABLE": "1", "RUST_LOG": "warn", }) time.sleep(6) checks = {} club = get(HOST_PORT, "/ut/game/fifa17/club?type=player&start=0&count=5") checks["club_total"] = club.get("totalResults") checks["club_first_discard"] = (club.get("itemData") or [{}])[0].get("discardValue") checks["club_first_rating"] = (club.get("itemData") or [{}])[0].get("rating") umi = get(HOST_PORT, "/ut/game/fifa17/userMassInfo") checks["squad_slots"] = len(((umi.get("userInfo") or {}).get("squad") or umi.get("squad") or {}).get("players", []) or []) for cat in ("contracts", "development", "fitness"): d = get(HOST_PORT, "/ut/game/fifa17/club/consumables/" + cat) st = d.get("itemData") or [] checks["consumables_" + cat] = (len(st), sum(s.get("count", 0) for s in st)) sq = get(HOST_PORT, "/ut/game/fifa17/squad/0") checks["squad_players"] = len((sq.get("squad") or sq).get("players") or []) print(json.dumps(checks, indent=2, sort_keys=True)) # The consumable apply is Rust-owned and MUTATES, so the rehearsal must not # send one that would succeed: a two-target body is refused (400 # apply_batch_unsupported) before anything is resolved or written. That # refusal is only reachable if the candidate host CLAIMS the route -- a 502 # means it fell through to the (dead) upstream, i.e. the cutover is missing. print("\n=== the candidate must OWN the consumable apply ===") req = urllib.request.Request( "http://127.0.0.1:%d/ut/game/fifa17/item/resource/5001004" % HOST_PORT, data=b'{"apply":[{"id":100000003},{"id":100000004}]}', headers={"X-OpenFUT-Game": "fifa17", "Content-Type": "application/json"}, method="POST") try: with urllib.request.urlopen(req, timeout=20) as r: st, body = r.status, r.read().decode() except urllib.error.HTTPError as e: st, body = e.code, e.read().decode() print(" status=%s body=%s" % (st, body)) print(" apply route Rust-owned, nothing mutated (must be 400 " "apply_batch_unsupported): %s" % (st == 400 and "apply_batch_unsupported" in body)) finally: for p in procs: try: os.kill(p.pid, signal.SIGTERM) except ProcessLookupError: pass time.sleep(2) for p in procs: if p.poll() is None: os.kill(p.pid, signal.SIGKILL) print("\nrehearsal processes stopped")