Files
OpenFUT/scripts/hub-diff-prod-staging.py
T
funman300 16771b0b33 test(scripts): recover the client's error text, and diff hub shapes against production
Three diagnostics from chasing a FUT error that four server-side fixes failed to
resolve, kept because the technique generalises.

client-error-string.py recovers FIFA's on-screen message from /proc/<pid>/mem,
read-only, scanning ASCII and UTF-16LE (FIFA UI strings are wide). This ended the
guessing: the dialog reads "An error occurred downloading the FUT Squad Update.
Please try again." -- a CONTENT DOWNLOAD failure, not the player's lineup. Every
squad fix before it was aimed at the wrong subsystem, because "squad update" in FIFA
means the roster update, and the server-side symptom (a squad the client would not
accept) was consistent with both readings. When the server says 200 and the client
says no, the client's own words are the cheapest evidence available and should have
been the FIRST thing recovered, not the fifth.

hub-dump.py + hub-diff-prod-staging.py diff every hub route between production
(known-good, same client accepts it) and staging, comparing key presence and JSON
types rather than values, since values legitimately differ. Result: 0 structural
differences across 14 routes, which retired the whole "a missing field breaks
bootstrap" line of investigation in one run instead of one restart at a time.

Also ruled out with evidence: cert gates ARE patched (autopatch logs
"pid 56298: PATCHED cert gates", and the gate bytes read back as the patched
patterns); the roster server serves the FUT Squad Update fine (TLS1.2
AES256-GCM-SHA384, HTTP/1.0 200, application/xml) once probed with
ALL:@SECLEVEL=0 -- a default modern context gets SSLV3_ALERT_HANDSHAKE_FAILURE and
would have been a false alarm; production and staging Blaze advertise identical
roster/POW hosts; the Blaze session is healthy and answering PINGs; and the squad
round-trips exactly through PUT/GET.
2026-08-18 04:30:08 +00:00

63 lines
2.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""Diff hub-route SHAPES between production (known-good) and staging.
Values legitimately differ (different clubs, different piles). What must not differ is
structure: a key production sends that staging omits, or a type/null mismatch, is a
candidate cause for the client rejecting FUT bootstrap.
"""
import json
import os
PROD, STAG = "/tmp/hub_prod", "/tmp/hub_stag"
def t(v):
return "null" if v is None else type(v).__name__
def shape(obj, prefix="", depth=0):
"""path -> type, descending into the FIRST element of lists (representative)."""
out = {}
if depth > 6:
return out
if isinstance(obj, dict):
for k, v in obj.items():
p = f"{prefix}.{k}" if prefix else k
out[p] = t(v)
out.update(shape(v, p, depth + 1))
elif isinstance(obj, list):
out[(prefix or "<root>") + "[]"] = "list"
if obj:
out.update(shape(obj[0], f"{prefix}[0]", depth + 1))
return out
total = 0
for name in sorted(os.listdir(PROD)):
pf, sf = os.path.join(PROD, name), os.path.join(STAG, name)
if not os.path.exists(sf):
print(f"{name}: MISSING on staging")
continue
p = json.load(open(pf))
s = json.load(open(sf))
ps, ss = shape(p.get("body")), shape(s.get("body"))
missing = [k for k in ps if k not in ss]
types = [(k, ps[k], ss[k]) for k in ps if k in ss and ps[k] != ss[k]]
nulls = [k for k in ps if k in ss and ss[k] == "null" and ps[k] != "null"]
if p.get("status") != s.get("status"):
print(f"\n### {name}: STATUS prod={p.get('status')} staging={s.get('status')}")
total += 1
if missing or types:
print(f"\n### {name}")
for k in missing:
print(f" MISSING on staging: {k:<44} prod_type={ps[k]}")
for k, a, b in types:
mark = " <-- NULL" if b == "null" else ""
print(f" TYPE {k:<48} prod={a:<7} staging={b}{mark}")
total += len(missing) + len(types)
print(f"\n=== {total} structural difference(s) across {len(os.listdir(PROD))} routes ===")
if total == 0:
print("Every hub route matches production's shape. The bootstrap failure is not a")
print("missing/mistyped field in these responses.")