Add FIFA17 match transition tracers
This commit is contained in:
Executable
+194
@@ -0,0 +1,194 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Trace FIFA17 ACTION_ADVANCE dispatch after MATCH_CREATED delivery.
|
||||||
|
|
||||||
|
Stage one (`match_transition_trace.py`) proves the HTTP response, deserializer,
|
||||||
|
and FUT_CREATE_MATCH_DP delivery. This stage uses that provider callback to
|
||||||
|
capture the screen key, then records every nested global UI dispatch for that
|
||||||
|
same screen until the positive-control FUT_GET_MATCH_KITS_DP arrives. It also
|
||||||
|
captures the low-level create event and final native-to-UI provider bridge.
|
||||||
|
|
||||||
|
The generated GDB program uses hardware-assisted execution breakpoints only.
|
||||||
|
It never writes client memory and never drives game input.
|
||||||
|
|
||||||
|
match_advance_trace.py [pid] [--output PATH]
|
||||||
|
match_advance_trace.py --print-script [pid]
|
||||||
|
match_advance_trace.py --selftest
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
import match_transition_trace as transition
|
||||||
|
|
||||||
|
FIFA_MODULE = "FIFA17.exe"
|
||||||
|
PINNED_FIFA_SHA256 = "29c31cef12b0c3c2a7305220617c7b4fa139ab76b8c857851bdbe88987962899"
|
||||||
|
GLOBAL_UI_DISPATCH_RVA = 0x80D1070
|
||||||
|
CREATE_MATCH_CONTROLLER_RVA = 0xBF950
|
||||||
|
PROVIDER_BRIDGE_CALL_RVA = 0x1A4D41
|
||||||
|
|
||||||
|
|
||||||
|
def module_mapping(pid: int, module: str) -> tuple[int, str]:
|
||||||
|
with open(f"/proc/{pid}/maps", encoding="utf-8") as handle:
|
||||||
|
for line in handle:
|
||||||
|
fields = line.split(maxsplit=5)
|
||||||
|
path = fields[5].rstrip() if len(fields) == 6 else ""
|
||||||
|
if not path.endswith(module):
|
||||||
|
continue
|
||||||
|
return int(fields[0].split("-", 1)[0], 16), path
|
||||||
|
raise RuntimeError(f"{module} is not mapped in PID {pid}")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_file(path: str, expected: str, label: str) -> None:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with open(path, "rb") as handle:
|
||||||
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
actual = digest.hexdigest()
|
||||||
|
if actual != expected:
|
||||||
|
raise RuntimeError(f"unsupported {label}: sha256={actual}; expected={expected}")
|
||||||
|
|
||||||
|
|
||||||
|
def trace_addresses(cards_base: int, fifa_base: int) -> dict[str, int]:
|
||||||
|
return {
|
||||||
|
"provider": cards_base + transition.PROVIDER_DISPATCH_RVA,
|
||||||
|
"global_dispatch": fifa_base + GLOBAL_UI_DISPATCH_RVA,
|
||||||
|
"controller": cards_base + CREATE_MATCH_CONTROLLER_RVA,
|
||||||
|
"bridge": cards_base + PROVIDER_BRIDGE_CALL_RVA,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_gdb_script(pid: int, cards_base: int, fifa_base: int, output: str) -> str:
|
||||||
|
if any(character in output for character in "\n\r"):
|
||||||
|
raise ValueError("output path cannot contain a newline")
|
||||||
|
address = trace_addresses(cards_base, fifa_base)
|
||||||
|
return f"""set pagination off
|
||||||
|
set confirm off
|
||||||
|
set print thread-events off
|
||||||
|
set breakpoint always-inserted on
|
||||||
|
set logging file {output}
|
||||||
|
set logging overwrite on
|
||||||
|
set logging redirect off
|
||||||
|
set logging enabled on
|
||||||
|
handle SIGSEGV nostop noprint pass
|
||||||
|
handle SIGILL nostop noprint pass
|
||||||
|
handle SIGFPE nostop noprint pass
|
||||||
|
handle SIGPIPE nostop noprint pass
|
||||||
|
handle SIGALRM nostop noprint pass
|
||||||
|
handle SIGUSR1 nostop noprint pass
|
||||||
|
handle SIGUSR2 nostop noprint pass
|
||||||
|
|
||||||
|
attach {pid}
|
||||||
|
set $screen_key = 0
|
||||||
|
set $target_seen = 0
|
||||||
|
|
||||||
|
hbreak *0x{address['provider']:x}
|
||||||
|
condition 1 $edx == 0x{transition.FUT_CREATE_MATCH_DP:x} || $edx == 0x{transition.FUT_GET_MATCH_KITS_DP:x}
|
||||||
|
commands
|
||||||
|
silent
|
||||||
|
if $edx == 0x{transition.FUT_CREATE_MATCH_DP:x}
|
||||||
|
set $screen_key = $r8
|
||||||
|
set $target_seen = 1
|
||||||
|
end
|
||||||
|
python import time; print("ADVTRACE epoch_ns=%d mono_ns=%d PROVIDER" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||||
|
printf "thread=%d provider=%#x screen_key=%p controller=%p caller=%p\\n", $_thread, $edx, $r8, $rcx, *(void**)$rsp
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
hbreak *0x{address['global_dispatch']:x}
|
||||||
|
condition 2 $target_seen != 0 && $r8 == $screen_key
|
||||||
|
commands
|
||||||
|
silent
|
||||||
|
python import time; print("ADVTRACE epoch_ns=%d mono_ns=%d GLOBAL_DISPATCH" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||||
|
printf "thread=%d id=%#x screen_key=%p registry=%p caller=%p\\n", $_thread, $rdx, $r8, $rcx, *(void**)$rsp
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
hbreak *0x{address['controller']:x}
|
||||||
|
condition 3 $edx == 0x7546
|
||||||
|
commands
|
||||||
|
silent
|
||||||
|
python import time; print("ADVTRACE epoch_ns=%d mono_ns=%d CREATE_MATCH_CONTROLLER" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||||
|
printf "thread=%d event=%#x controller=%p caller=%p\\n", $_thread, $edx, $rcx, *(void**)$rsp
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
hbreak *0x{address['bridge']:x}
|
||||||
|
condition 4 $edi == 0x{transition.FUT_CREATE_MATCH_DP:x} || $edi == 0x{transition.FUT_GET_MATCH_KITS_DP:x}
|
||||||
|
commands
|
||||||
|
silent
|
||||||
|
python import time; print("ADVTRACE epoch_ns=%d mono_ns=%d PROVIDER_BRIDGE" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||||
|
printf "thread=%d provider=%#x target=%p bridge=%p callback=%p\\n", $_thread, $edi, $rsi, $rbx, *(void**)(*(void**)$rbx+0x48)
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
printf "ADVTRACE ARMED pid={pid} provider=0x{address['provider']:x} global=0x{address['global_dispatch']:x} controller=0x{address['controller']:x} bridge=0x{address['bridge']:x}\\n"
|
||||||
|
continue
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def selftest() -> None:
|
||||||
|
address = trace_addresses(0x180000000, 0x140000000)
|
||||||
|
assert address == {
|
||||||
|
"provider": 0x1801A4CD0,
|
||||||
|
"global_dispatch": 0x1480D1070,
|
||||||
|
"controller": 0x1800BF950,
|
||||||
|
"bridge": 0x1801A4D41,
|
||||||
|
}
|
||||||
|
script = build_gdb_script(28804, 0x180000000, 0x140000000, "/tmp/advance.log")
|
||||||
|
assert script.count("hbreak *") == 4
|
||||||
|
assert f"$edx == 0x{transition.FUT_CREATE_MATCH_DP:x}" in script
|
||||||
|
assert f"$edx == 0x{transition.FUT_GET_MATCH_KITS_DP:x}" in script
|
||||||
|
assert "$r8 == $screen_key" in script
|
||||||
|
assert "CREATE_MATCH_CONTROLLER" in script
|
||||||
|
assert "PROVIDER_BRIDGE" in script
|
||||||
|
assert "set *(" not in script
|
||||||
|
print("match_advance_trace selftest: PASS")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("pid", nargs="?", type=int)
|
||||||
|
parser.add_argument("--output")
|
||||||
|
parser.add_argument("--print-script", action="store_true")
|
||||||
|
parser.add_argument("--selftest", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.selftest:
|
||||||
|
selftest()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
pid = args.pid or transition.find_pid()
|
||||||
|
if not pid:
|
||||||
|
print("FIFA17.exe not found", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
try:
|
||||||
|
cards_base, cards_path = transition.cards_mapping(pid)
|
||||||
|
transition.validate_cards(cards_path)
|
||||||
|
fifa_base, fifa_path = module_mapping(pid, FIFA_MODULE)
|
||||||
|
validate_file(fifa_path, PINNED_FIFA_SHA256, FIFA_MODULE)
|
||||||
|
output = args.output or f"/tmp/fifa17-match-advance-{pid}.log"
|
||||||
|
script = build_gdb_script(pid, cards_base, fifa_base, output)
|
||||||
|
except (OSError, RuntimeError, ValueError) as error:
|
||||||
|
print(error, file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
if args.print_script:
|
||||||
|
print(script, end="")
|
||||||
|
return 0
|
||||||
|
if not shutil.which("gdb"):
|
||||||
|
print("gdb not found", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
script_path = f"/tmp/fifa17-match-advance-{pid}.gdb"
|
||||||
|
with open(script_path, "w", encoding="utf-8") as handle:
|
||||||
|
handle.write(script)
|
||||||
|
os.execvp("gdb", ["gdb", "-q", "-nx", "-x", script_path])
|
||||||
|
return 127
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Executable
+233
@@ -0,0 +1,233 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Trace the FIFA17 create-match publish boundary with hardware breakpoints.
|
||||||
|
|
||||||
|
The tracer covers the client-local path after POST /match:
|
||||||
|
|
||||||
|
response callback -> deserializer -> controller event 0x7546
|
||||||
|
-> FUT_CREATE_MATCH_DP 0x7563
|
||||||
|
|
||||||
|
FUT_GET_MATCH_KITS_DP 0x7565 is captured as the positive control through the
|
||||||
|
same native dispatcher. The generated GDB program uses only hardware-assisted
|
||||||
|
execution breakpoints. It never writes client memory and never drives game
|
||||||
|
input.
|
||||||
|
|
||||||
|
match_transition_trace.py [pid] [--output PATH]
|
||||||
|
match_transition_trace.py --print-script [pid]
|
||||||
|
match_transition_trace.py --selftest
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import glob
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
CARDS_MODULE = "CardsDLL_Win64_retail.dll"
|
||||||
|
PINNED_CARDS_SHA256 = "4706a881ae1fc7b5769fd810b25a868d29d2b16a8e65a7513436327ef645573c"
|
||||||
|
RESPONSE_CALLBACK_RVA = 0x114D90
|
||||||
|
DESERIALIZE_SUCCESS_RVA = 0x118940
|
||||||
|
CREATE_MATCH_CONTROLLER_RVA = 0xBF950
|
||||||
|
PROVIDER_DISPATCH_RVA = 0x1A4CD0
|
||||||
|
CREATE_MATCH_CONTROLLER_EVENT = 0x7546
|
||||||
|
FUT_CREATE_MATCH_DP = 0x7563
|
||||||
|
FUT_GET_MATCH_KITS_DP = 0x7565
|
||||||
|
|
||||||
|
|
||||||
|
def find_pid() -> int | None:
|
||||||
|
found = []
|
||||||
|
for directory in glob.glob("/proc/[0-9]*"):
|
||||||
|
try:
|
||||||
|
with open(os.path.join(directory, "comm"), encoding="utf-8") as handle:
|
||||||
|
if handle.read().strip() != "FIFA17.exe":
|
||||||
|
continue
|
||||||
|
pid = int(os.path.basename(directory))
|
||||||
|
with open(os.path.join(directory, "statm"), encoding="utf-8") as handle:
|
||||||
|
resident_pages = int(handle.read().split()[1])
|
||||||
|
found.append((resident_pages, pid))
|
||||||
|
except (OSError, ValueError, IndexError):
|
||||||
|
continue
|
||||||
|
return max(found)[1] if found else None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cards_mapping(lines) -> tuple[int, str]:
|
||||||
|
for line in lines:
|
||||||
|
fields = line.split(maxsplit=5)
|
||||||
|
path = fields[5].rstrip() if len(fields) == 6 else ""
|
||||||
|
if not path.endswith(CARDS_MODULE):
|
||||||
|
continue
|
||||||
|
start = int(fields[0].split("-", 1)[0], 16)
|
||||||
|
return start, path
|
||||||
|
raise RuntimeError(f"{CARDS_MODULE} is not mapped")
|
||||||
|
|
||||||
|
|
||||||
|
def cards_mapping(pid: int) -> tuple[int, str]:
|
||||||
|
with open(f"/proc/{pid}/maps", encoding="utf-8") as handle:
|
||||||
|
try:
|
||||||
|
return parse_cards_mapping(handle)
|
||||||
|
except RuntimeError as error:
|
||||||
|
raise RuntimeError(f"{error} in PID {pid}") from error
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_file(path: str) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with open(path, "rb") as handle:
|
||||||
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_cards(path: str) -> None:
|
||||||
|
actual = sha256_file(path)
|
||||||
|
if actual != PINNED_CARDS_SHA256:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"unsupported {CARDS_MODULE}: sha256={actual}; expected={PINNED_CARDS_SHA256}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def trace_addresses(base: int) -> dict[str, int]:
|
||||||
|
return {
|
||||||
|
"response": base + RESPONSE_CALLBACK_RVA,
|
||||||
|
"deserialize": base + DESERIALIZE_SUCCESS_RVA,
|
||||||
|
"controller": base + CREATE_MATCH_CONTROLLER_RVA,
|
||||||
|
"provider": base + PROVIDER_DISPATCH_RVA,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_gdb_script(pid: int, base: int, output: str) -> str:
|
||||||
|
if any(character in output for character in "\n\r"):
|
||||||
|
raise ValueError("output path cannot contain a newline")
|
||||||
|
address = trace_addresses(base)
|
||||||
|
return f"""set pagination off
|
||||||
|
set confirm off
|
||||||
|
set print thread-events off
|
||||||
|
set breakpoint always-inserted on
|
||||||
|
set logging file {output}
|
||||||
|
set logging overwrite on
|
||||||
|
set logging redirect off
|
||||||
|
set logging enabled on
|
||||||
|
handle SIGSEGV nostop noprint pass
|
||||||
|
handle SIGILL nostop noprint pass
|
||||||
|
handle SIGFPE nostop noprint pass
|
||||||
|
handle SIGPIPE nostop noprint pass
|
||||||
|
handle SIGALRM nostop noprint pass
|
||||||
|
handle SIGUSR1 nostop noprint pass
|
||||||
|
handle SIGUSR2 nostop noprint pass
|
||||||
|
|
||||||
|
attach {pid}
|
||||||
|
|
||||||
|
hbreak *0x{address['response']:x}
|
||||||
|
commands
|
||||||
|
silent
|
||||||
|
python import time; print("HWTRACE epoch_ns=%d mono_ns=%d T3_RESPONSE_CALLBACK" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||||
|
if $rdx != 0
|
||||||
|
printf "thread=%d manager=%p status_obj=%p status=%u wire_payload=%p caller=%p\\n", $_thread, $rcx, $rdx, *(unsigned int*)($rdx+0x1c), *(void**)($rdx+0x28), *(void**)$rsp
|
||||||
|
else
|
||||||
|
printf "thread=%d manager=%p status_obj=0 caller=%p\\n", $_thread, $rcx, *(void**)$rsp
|
||||||
|
end
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
hbreak *0x{address['deserialize']:x}
|
||||||
|
commands
|
||||||
|
silent
|
||||||
|
python import time; print("HWTRACE epoch_ns=%d mono_ns=%d T4_DESERIALIZE_SUCCESS" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||||
|
printf "thread=%d manager=%p payload=%p caller=%p\\n", $_thread, $rcx, $rdx, *(void**)$rsp
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
hbreak *0x{address['controller']:x}
|
||||||
|
condition 3 $edx == 0x{CREATE_MATCH_CONTROLLER_EVENT:x}
|
||||||
|
commands
|
||||||
|
silent
|
||||||
|
python import time; print("HWTRACE epoch_ns=%d mono_ns=%d T5_CREATE_MATCH_CONTROLLER" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||||
|
printf "thread=%d controller_subobject=%p event=%#x caller=%p\\n", $_thread, $rcx, $edx, *(void**)$rsp
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
hbreak *0x{address['provider']:x}
|
||||||
|
condition 4 $edx == 0x{FUT_CREATE_MATCH_DP:x} || $edx == 0x{FUT_GET_MATCH_KITS_DP:x}
|
||||||
|
commands
|
||||||
|
silent
|
||||||
|
python import time; print("HWTRACE epoch_ns=%d mono_ns=%d T6_PROVIDER_DISPATCH" % (time.time_ns(), time.monotonic_ns()), end=" ")
|
||||||
|
printf "thread=%d controller=%p provider=%#x payload=%p callback=%p\\n", $_thread, $rcx, $edx, $r8, *(void**)$rsp
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
printf "HWTRACE ARMED pid={pid} response=0x{address['response']:x} deserialize=0x{address['deserialize']:x} controller=0x{address['controller']:x} provider=0x{address['provider']:x}\\n"
|
||||||
|
continue
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def selftest() -> None:
|
||||||
|
base = 0x180000000
|
||||||
|
address = trace_addresses(base)
|
||||||
|
assert address == {
|
||||||
|
"response": 0x180114D90,
|
||||||
|
"deserialize": 0x180118940,
|
||||||
|
"controller": 0x1800BF950,
|
||||||
|
"provider": 0x1801A4CD0,
|
||||||
|
}
|
||||||
|
mapping = parse_cards_mapping(
|
||||||
|
[
|
||||||
|
"6ffffc0f0000-6ffffc0f1000 r--p 00000000 00:37 2941670 "
|
||||||
|
"/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll\n"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert mapping == (
|
||||||
|
0x6FFFFC0F0000,
|
||||||
|
"/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll",
|
||||||
|
)
|
||||||
|
script = build_gdb_script(25718, base, "/tmp/match-transition.log")
|
||||||
|
assert script.count("hbreak *") == 4
|
||||||
|
assert f"$edx == 0x{CREATE_MATCH_CONTROLLER_EVENT:x}" in script
|
||||||
|
assert f"$edx == 0x{FUT_CREATE_MATCH_DP:x}" in script
|
||||||
|
assert f"$edx == 0x{FUT_GET_MATCH_KITS_DP:x}" in script
|
||||||
|
assert "T3_RESPONSE_CALLBACK" in script
|
||||||
|
assert "T4_DESERIALIZE_SUCCESS" in script
|
||||||
|
assert "T5_CREATE_MATCH_CONTROLLER" in script
|
||||||
|
assert "T6_PROVIDER_DISPATCH" in script
|
||||||
|
assert "set *(" not in script
|
||||||
|
print("match_transition_trace selftest: PASS")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("pid", nargs="?", type=int)
|
||||||
|
parser.add_argument("--output")
|
||||||
|
parser.add_argument("--print-script", action="store_true")
|
||||||
|
parser.add_argument("--selftest", action="store_true")
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.selftest:
|
||||||
|
selftest()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
pid = args.pid or find_pid()
|
||||||
|
if not pid:
|
||||||
|
print("FIFA17.exe not found", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
try:
|
||||||
|
base, cards_path = cards_mapping(pid)
|
||||||
|
validate_cards(cards_path)
|
||||||
|
output = args.output or f"/tmp/fifa17-match-transition-{pid}.log"
|
||||||
|
script = build_gdb_script(pid, base, output)
|
||||||
|
except (OSError, RuntimeError, ValueError) as error:
|
||||||
|
print(error, file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
if args.print_script:
|
||||||
|
print(script, end="")
|
||||||
|
return 0
|
||||||
|
if not shutil.which("gdb"):
|
||||||
|
print("gdb not found", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
script_path = f"/tmp/fifa17-match-transition-{pid}.gdb"
|
||||||
|
with open(script_path, "w", encoding="utf-8") as handle:
|
||||||
|
handle.write(script)
|
||||||
|
os.execvp("gdb", ["gdb", "-q", "-nx", "-x", script_path])
|
||||||
|
return 127
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user