Skip to content

Commit ad4bbcf

Browse files
committed
gpt-6.1-sol: reap stalled Xvfb children after replay captures
1 parent c47cc97 commit ad4bbcf

4 files changed

Lines changed: 73 additions & 3 deletions

File tree

‎docs/RE_HANDOFF.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ Each port has its own build and runtime validation alongside the VC7 path.
3131

3232
## Native replay parity investigation
3333

34+
A fresh cold gate is in progress under `build/replay-cold-20261011/`.
35+
Cold normal/bugfix links and all 1,106 accepted authored units passed. The
36+
new 108-case suite has 46 complete retail/native pairs with equal traces.
37+
The next retail capture completed all 113,660 calculations, then failed on
38+
an Xvfb shutdown timeout. Capture cleanup now kills and reaps a stalled
39+
Xvfb child; process tests cover graceful, stalled, and completed children.
40+
Resume with the archived candidate in this batch. Merge into main and sync
41+
`port/portable-64bit` after the full replay and ordinary-demo gates pass.
42+
3443
The current playable VC7 build passes all three bundled demos against a fresh
3544
muted retail capture with ordinary pacing and rasterization: 16,080 frames,
3645
with all 22 recorded fields equal. Its executable SHA-256 is

‎scripts/capture-replay.py‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@
2020
TARGET_SHA = "330fbdbf58a710829d65277b4f312cfbb38d5448b3df523e79350b879213d924"
2121

2222

23+
def stop_process(process, timeout=5):
24+
"""Terminate and reap an owned child, killing it if shutdown stalls."""
25+
if process.poll() is not None:
26+
return
27+
process.terminate()
28+
try:
29+
process.wait(timeout=timeout)
30+
except subprocess.TimeoutExpired:
31+
process.kill()
32+
process.wait(timeout=timeout)
33+
34+
2335
def observer_config(candidate, map_path):
2436
if candidate is None:
2537
return dict(gameManager=0x0160F508, player=0x017D5EF8, rng=0x0164D520, input=0x0164D52C,
@@ -388,9 +400,7 @@ def command(command_text):
388400
shutil.copyfile(runtime / "log.txt", args.output_dir / "game.log")
389401
shutil.rmtree(runtime, ignore_errors=True)
390402
if xvfb:
391-
if xvfb.poll() is None:
392-
xvfb.terminate()
393-
xvfb.wait(timeout=5)
403+
stop_process(xvfb)
394404
if xvfb_log:
395405
xvfb_log.close()
396406

‎scripts/ci.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def main() -> int:
5959
run("Test local relocation validation", [sys.executable, "scripts/test-match-local-relocations.py"])
6060
run("Test replay trace comparison", [sys.executable, "scripts/test-replay-trace-comparison.py"])
6161
run("Test replay file validation", [sys.executable, "scripts/test-replay-file.py"])
62+
run("Test replay capture cleanup", [sys.executable, "scripts/test-replay-capture-cleanup.py"])
6263
run("Validate replay route coverage", [sys.executable, "scripts/validate-replay-fixtures.py"])
6364
run(
6465
"Test semantic protocol guards",
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env python3
2+
"""Verify that replay capture cleanup reaps responsive and stalled children."""
3+
import importlib.util
4+
import os
5+
from pathlib import Path
6+
import signal
7+
import subprocess
8+
import sys
9+
import unittest
10+
11+
12+
spec = importlib.util.spec_from_file_location("capture", Path(__file__).with_name("capture-replay.py"))
13+
capture = importlib.util.module_from_spec(spec)
14+
spec.loader.exec_module(capture)
15+
16+
17+
@unittest.skipUnless(os.name == "posix", "Process cleanup tests require POSIX signals")
18+
class CaptureCleanupTests(unittest.TestCase):
19+
def check_child(self, ignore_termination, expected_signal):
20+
script = (
21+
"import signal\n"
22+
+ ("signal.signal(signal.SIGTERM, signal.SIG_IGN)\n" if ignore_termination else "")
23+
+ "print('ready', flush=True)\nsignal.pause()\n"
24+
)
25+
process = subprocess.Popen([sys.executable, "-c", script], stdout=subprocess.PIPE, text=True)
26+
try:
27+
self.assertEqual(process.stdout.readline(), "ready\n")
28+
capture.stop_process(process, timeout=0.1)
29+
self.assertEqual(process.returncode, -expected_signal)
30+
finally:
31+
if process.poll() is None:
32+
process.kill()
33+
process.wait(timeout=5)
34+
process.stdout.close()
35+
36+
def test_responsive_child_exits_on_termination(self):
37+
self.check_child(False, signal.SIGTERM)
38+
39+
def test_stalled_child_is_killed_and_reaped(self):
40+
self.check_child(True, signal.SIGKILL)
41+
42+
def test_completed_child_keeps_its_exit_status(self):
43+
process = subprocess.Popen([sys.executable, "-c", "pass"])
44+
process.wait(timeout=5)
45+
capture.stop_process(process, timeout=0.1)
46+
self.assertEqual(process.returncode, 0)
47+
48+
49+
if __name__ == "__main__":
50+
unittest.main()

0 commit comments

Comments
 (0)