test(control): wait until the bridge listens, and reap it when a quit check fails #457

Closed
mooki wants to merge 3 commits from mooki/Longshot:mac/control-quit into main
Member

Type: test · Risk: low · Tested on: macOS 26.7.1, Apple M4 Max (game-free checks, arm64 and Rosetta x86_64; no game run) · Needs: Linux and Windows, any GPU (game-free checks only)

Why

Anyone running the game-free suite on Linux or macOS can have it stall: tests/test_control.py's quit case can hang for
its whole 180 s limit and leave a bridge process running after the suite ends, whenever the bridge it starts is a little
late to listen. On this Mac under a loaded suite it happened in about 1 run of 150. This PR fixes the test fixture; the
game's bridge is unchanged. Part of the macOS port (#420).

  • quit_case in tests/control_test.c forks: the child runs the bridge, and the parent waits until the socket's file
    exists, then connects once (CHECK(connect(...) == 0)).
  • The bridge makes the file with bind and listens afterwards (transport_open in host/control.c). A connect between
    the two is refused (ECONNREFUSED, measured on macOS; Linux refuses it too [inference: not run here]).
  • CHECK prints and calls exit(1). The child keeps serving and holds the parent's stdout and stderr, so
    test_control.py's subprocess.run(..., capture_output=True, timeout=180) waits for those pipes until its limit.
  • The child is then left running for good, reparented to launchd. Two such bridges from earlier suites were found still
    running on this Mac, with a listening socket and no client.
  • The same happens for any check that fails after the fork. The real clients are not affected: longshot_ctl.py launch
    retries until the bridge answers.

What changed

The quit case now waits for the child's word that its bridge listens, a failed check ends and reaps the forked bridge,
and two new runs make the race certain (quit slow, quit fail) on Linux and macOS. The game's bridge is unchanged: its
only new lines are a hook that only the test build compiles.

  • Read first: quit_case and check_failed in tests/control_test.c.
  • tests/control_test.c:
    • the quit case waits for the child's word on a pipe, written once hos_control_serve has returned (after listen),
      instead of for the socket's file;
    • a failed CHECK ends and reaps the forked bridge before the process exits (check_failed, POSIX only; on Windows
      it only prints and exits, as before);
    • the parent waits 5 s at most for the child's exit, as the Windows branch already does;
    • two new runs: quit slow (the test build's bridge waits 300 ms between bind and listen) and quit fail (a check
      fails once the child serves; it must end with 1 and leave nothing running).
  • host/control.c: a hook for the test build only, under #ifdef HOS_CONTROL_NO_GAME: hos_control_test_bound,
    called between bind and listen when set. The game never defines HOS_CONTROL_NO_GAME; outside those blocks the file
    is byte for byte the control-sockets PR's. The hook is in the POSIX transport, so Windows' test build has none.
  • tests/test_control.py: on Linux and macOS it also runs quit slow (must pass) and quit fail (must end with 1
    and the injected failure). Windows' run list is unchanged.
  • Left out on purpose: the bridge could make its socket's file appear only once it listens (bind under another name,
    listen, rename). That changes the game on every platform, and the real clients already retry.
  • Depends on #456 (mac/control-sockets): this branch carries its two commits, 1ffdbe50 and 60dd4e0c, the rebased
    #456's head. Review only 58a56b55, this PR's own commit; 1ffdbe50..60dd4e0c is the control-sockets PR.
  • Earlier rebase, onto 5e072fc8: test_control.py's run list sits under #58's compile command; #58's command is
    kept as it is.
  • Rebased on 6c9edffe: onto #456's rebased head 60dd4e0c (rebase --onto); 58a56b55 is 26959952 rebased.
    One conflict, with #544 (225c37f8, the Windows peersid case): tests/control_test.c's usage comment and main()
    dispatch, and tests/test_control.py's run list. Both kept: peersid on Windows, quit slow/quit fail on POSIX
    (if windows: … else: …). No end commit.

Measurements

Verdict: fixed, a game-free proof (a test fixture, nothing to time): after the rebase tests/test_control.py
passes here for arm64 and x86_64 (40 PASS lines each, the two quit runs among them) and the whole suite changes from
main only by that check (FAIL → pass). Before the rebase the two new runs hung 5 of 5 each without the fix, with the
forked bridge left running; with it quit slow passed 20 of 20 and quit fail ended with 1 at once, 20 of 20, nothing
left running.

Commits: main 6c9edffe5d (2026-11-14); this PR mac/control-quit @
58a56b555d (on #456 @ 60dd4e0c). Before the rebase: main
5e072fc8c2 (2026-11-14); this PR mac/control-quit @
2695995219.
Baseline (the second table, before the rebase): mac/control-sockets @ f1f1843d, with this PR's hook and two runs but
not its fix (main cannot build the control fixture on macOS at all, so the race cannot be shown there).
Hardware: Apple M4 Max, 16 cores, 64 GiB, macOS 26.7.1 (25G241); Apple clang 21.0.0. No GPU.
Scene: none. Game-free checks.
Method: after the rebase, tests/test_control.py --cc clang --arch arm64, then --arch x86_64, at 58a56b55 through
tools/slot.py in the suite's clean environment, and the whole suite on this PR compared by test name with main's
run at 6c9edffe. Before the rebase: control_test built as test_control.py builds it on macOS (native arm64), then
run <folder> quit slow and <folder> quit fail with stdout and stderr on pipes: 5 times each without the fix, 20
times each with it, 20 s at most per run (a run past 20 s is a hang). Then tests/test_control.py --cc clang --arch arm64 and --arch x86_64, and the control checks through tests/run_all.py on main and on this PR, interleaved, two
runs each.
How to reproduce: python -I -B tests/test_control.py --cc clang --arch arm64 (and --arch x86_64), with a short
TMPDIR.

Before: main @ 6c9edffe After: this PR @ 58a56b55
tests/test_control.py FAIL (host/control.c does not compile on macOS) pass: 40 PASS lines with --arch arm64, the same 40 with --arch x86_64; 12.6 s in the suite
whole suite on this Mac, 6 at once 531 passed, 32 failed, 17 skipped 532 passed, 31 failed, 17 skipped
main @ 6c9edffe:     531 passed, 32 failed, 17 skipped in 181 s (linux, 6 at once)
this PR @ 58a56b55:  532 passed, 31 failed, 17 skipped in 186 s (linux, 6 at once)

Before the rebase:

control-sockets @ f1f1843d + hook, no fix this PR @ 26959952
quit slow 5 of 5 hang: the parent exits 1 at connect, its child serves on 20 of 20 pass, median 0.38 s
quit fail 5 of 5 hang: the parent exits 1 at the injected failure, its child serves on 20 of 20 end with 1 at once (median 0.09 s), nothing left running
test_control.py --arch arm64 / x86_64 exit 0, 40 PASS lines / exit 0, the same 40
control-sockets @ f1f1843d + hook, no fix (each of the 5 runs, then still running at 20 s with one live child):
quit slow: FAIL:340 connect(c->fd, (struct sockaddr *)&a, sizeof a) == 0
quit fail: FAIL:493 !"a failure injected once the child serves"

this PR @ 26959952:
quit slow: runs=20 hangs=0 passes=20, pass_seconds_median=0.3835 max=0.7469
quit fail: runs=20 hangs=0, each code 1: FAIL:521 !"a failure injected once the child serves", none left running
PASS: quit: the reply, then a clean exit, the socket removed
PASS: quit: a check failing once the child serves ends the child with it
control checks through tests/run_all.py (run 1 / run 2) main @ 5e072fc8 this PR @ 26959952
tests/test_control.py FAIL / FAIL (control.c does not compile on macOS) pass / pass
tests/test_longshot_ctl.py pass / pass pass / pass
tests/test_oot_state_tap.py FAIL / FAIL FAIL / FAIL (the fixture's vsnprintf macro, which the runner PR repairs)

Earlier, on the old base (ab92a95f, on the port's integration branch, the same fixture and hook): the original
quit case hung 8 times in 1,200 runs under load (7 of 900 arm64, 1 of 300 x86_64), each the refused connect. With the
fix, 0 hangs in 1,900 runs (arm64 and x86_64, idle and loaded). Not rerun here.

Screenshots: none; nothing on screen.

Flags and settings

None.

Testing

Tested:

  • After the rebase (head 58a56b55 on 6c9edffe): python -I -B tests/test_control.py --cc clang --arch arm64,
    then --arch x86_64: exit 0, 40 PASS lines each, the two quit runs among them.
  • The whole suite, tests/run_all.py, 6 at once, on the same Mac, after the rebase, compared by test name with main
    (the summary lines are under Measurements):
    • By test name, one check changed: tests/test_control.py, FAIL on main (the bridge does not compile on macOS
      there) → pass here in 12.6 s, with the quit runs. At 26959952, before the rebase, no control fixture was left
      running after the suite. (The control-sockets PR's own suite run then, without this fix, met the quit race under
      load and left one running.)
    • The 31 remaining failures also fail on main. Fifteen were named here before the rebase:
      test_appimage_package_retry, test_collect_wide, test_game_layout, test_low_memory_notice, test_mem_gate,
      test_mem_gate_children, test_mirror_trim, test_oot_state_tap, test_pcache_header_read,
      test_pipeline_cache_file, test_pipeline_cache_save, test_pipeline_play_cache, test_prof_symbols,
      test_program_cache_cleanup, test_program_cache_one_variant. The port's other PRs fix them. The three named
      before as stale on main (test_framegen_resources, test_module_ids, test_pad_layout) now pass on main and
      here.
    • The other 16 are new on main's list since 5e072fc8, and fail on both: test_appimage_player_build,
      test_cpm_discovery, test_crash_dialog, test_crash_record, test_env_on, test_lift_cleanup_error,
      test_linux_preflight, test_longshot_ctl, test_make_game_cache, test_pipeline_tour, test_readback_late,
      test_report_run, test_split_giants, test_vk_pgo, test_vram_budget_row, test_walk_key.
  • Before the rebase: the two new runs without and with the fix, and the control checks on main and here, two
    interleaved runs each, as under Measurements.
  • The Windows and Linux text after the rebase, by unifdef against main @ 6c9edffe: host/control.c with
    -U__APPLE__ -UHOS_CONTROL_NO_GAME (the game build) identical; with -U__APPLE__ -D_WIN32 (Windows, the test build
    too) identical; the Linux test build (-U__APPLE__ -U_WIN32 -DHOS_CONTROL_NO_GAME) differs by the hook only (its
    declaration and its one call between bind and listen). tests/control_test.c differs on every platform:
    CHECK calls check_failed (on Windows it prints and exits 1, as before) and the POSIX quit case is the new one.
  • After the rebase onto main aa95d35f, at fe3e8c91 (58a56b55 before the rebases onto e090cc48 and aa95d35f):
    Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they
    apply) gave the same results as before; python -I -B tests/run_all.py on wave S1's merge 11a5b4e7, which carries
    this head: 568/22/17 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure
    (reports/2026-11-15-rebase-sweep-3.md).

Not tested:

  • Linux. It runs the same POSIX quit case and gets the two new runs. Not run here.
  • Windows. Its run list and code path are unchanged; check_failed only prints and exits there, as before. Not run
    here.

Must be tested:

  • Linux and Windows, any GPU (game-free checks only, no game build): run python -I -B tests/test_control.py on this
    PR (head fe3e8c91) and on main aa95d35f in the same checkout, expecting it to pass on both, with no hang; on Linux
    this PR prints two more PASS lines than main (the quit case with its bridge slow to listen, and quit: a check failing once the child serves ends the child with it), and on Windows the same PASS lines as main (by hand: test
    request T-B6ML).

Risks

What could break:

  • Test code only, apart from the hook, which only the test build (HOS_CONTROL_NO_GAME) compiles.
  • The parent now waits 10 s at most for the child's word. A bridge that takes longer to start fails the check instead of
    hanging it.
  • CHECK is now a call to check_failed on every platform, Windows included (where it prints and exits as before).
  • Landing order: after #456 (mac/control-sockets), whose two commits it carries; best merged right after it.

Turning it off: revert the PR.

Compatibility: Nothing changes: the game build of host/control.c is main's on Windows and Linux; saves, the
options file, the shader caches and the vk ABI are unchanged.

Issues

See #420.

Checklist

Everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description) of the status line's type.
  • Why gives the reason plainly and links the issues that fit (the tracker searched).
  • Measurements has a verdict, or one line saying why there is nothing to measure.
  • Testing names what was tested, what wasn't, and what must still be tested, where and how.
  • Rebased on current main: on aa95d35f (#761, 2026-11-15), head fe3e8c91, on #456 fc2f1040, without a
    conflict; this PR's own commits, old → new: 58a56b55 → fe3e8c91; the commit names elsewhere in this description
    are from before the rebase.
  • python -I -B tests/run_all.py passes (any failure that also fails on main is named). (532 passed, 17 skipped,
    and 31 failures that also fail on main at 6c9edffe (531 passed, 32 failed, 17 skipped), named under Testing. After
    the rebase onto main aa95d35f (head fe3e8c91): wave S1's merge 11a5b4e7, 568/22/17 against main aa95d35f's
    543/33/17, no new failure.)
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

When it applies

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • A game-free test added for the fix, failing on main and passing here. (The two new runs hang without the fix and
    pass with it; on main the fixture does not build on macOS.)
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Docs updated.
    (docs/control.md, in #456's carried commits; this PR's own commit changes test code and the test-build hook only.)
**Type:** test · **Risk:** low · **Tested on:** macOS 26.7.1, Apple M4 Max (game-free checks, arm64 and Rosetta x86_64; no game run) · **Needs:** Linux and Windows, any GPU (game-free checks only) ## Why Anyone running the game-free suite on Linux or macOS can have it stall: `tests/test_control.py`'s quit case can hang for its whole 180 s limit and leave a bridge process running after the suite ends, whenever the bridge it starts is a little late to listen. On this Mac under a loaded suite it happened in about 1 run of 150. This PR fixes the test fixture; the game's bridge is unchanged. Part of the macOS port (#420). - `quit_case` in `tests/control_test.c` forks: the child runs the bridge, and the parent waits until the socket's file exists, then connects once (`CHECK(connect(...) == 0)`). - The bridge makes the file with `bind` and listens afterwards (`transport_open` in `host/control.c`). A connect between the two is refused (`ECONNREFUSED`, measured on macOS; Linux refuses it too [inference: not run here]). - `CHECK` prints and calls `exit(1)`. The child keeps serving and holds the parent's stdout and stderr, so `test_control.py`'s `subprocess.run(..., capture_output=True, timeout=180)` waits for those pipes until its limit. - The child is then left running for good, reparented to launchd. Two such bridges from earlier suites were found still running on this Mac, with a listening socket and no client. - The same happens for any check that fails after the fork. The real clients are not affected: `longshot_ctl.py launch` retries until the bridge answers. ## What changed The quit case now waits for the child's word that its bridge listens, a failed check ends and reaps the forked bridge, and two new runs make the race certain (`quit slow`, `quit fail`) on Linux and macOS. The game's bridge is unchanged: its only new lines are a hook that only the test build compiles. - Read first: `quit_case` and `check_failed` in `tests/control_test.c`. - **`tests/control_test.c`:** - the quit case waits for the child's word on a pipe, written once `hos_control_serve` has returned (after `listen`), instead of for the socket's file; - a failed `CHECK` ends and reaps the forked bridge before the process exits (`check_failed`, POSIX only; on Windows it only prints and exits, as before); - the parent waits 5 s at most for the child's exit, as the Windows branch already does; - two new runs: `quit slow` (the test build's bridge waits 300 ms between `bind` and `listen`) and `quit fail` (a check fails once the child serves; it must end with 1 and leave nothing running). - **`host/control.c`:** a hook for the test build only, under `#ifdef HOS_CONTROL_NO_GAME`: `hos_control_test_bound`, called between `bind` and `listen` when set. The game never defines `HOS_CONTROL_NO_GAME`; outside those blocks the file is byte for byte the control-sockets PR's. The hook is in the POSIX transport, so Windows' test build has none. - **`tests/test_control.py`:** on Linux and macOS it also runs `quit slow` (must pass) and `quit fail` (must end with 1 and the injected failure). Windows' run list is unchanged. - Left out on purpose: the bridge could make its socket's file appear only once it listens (bind under another name, listen, rename). That changes the game on every platform, and the real clients already retry. - Depends on #456 (`mac/control-sockets`): this branch carries its two commits, `1ffdbe50` and `60dd4e0c`, the rebased #456's head. Review only `58a56b55`, this PR's own commit; `1ffdbe50..60dd4e0c` is the control-sockets PR. - **Earlier rebase, onto `5e072fc8`:** `test_control.py`'s run list sits under #58's compile command; #58's command is kept as it is. - **Rebased on `6c9edffe`:** onto #456's rebased head `60dd4e0c` (`rebase --onto`); `58a56b55` is `26959952` rebased. One conflict, with #544 (`225c37f8`, the Windows `peersid` case): `tests/control_test.c`'s usage comment and `main()` dispatch, and `tests/test_control.py`'s run list. Both kept: `peersid` on Windows, `quit slow`/`quit fail` on POSIX (`if windows: … else: …`). No end commit. ## Measurements **Verdict:** fixed, a game-free proof (a test fixture, nothing to time): after the rebase `tests/test_control.py` passes here for arm64 and x86_64 (40 PASS lines each, the two quit runs among them) and the whole suite changes from `main` only by that check (FAIL → pass). Before the rebase the two new runs hung 5 of 5 each without the fix, with the forked bridge left running; with it `quit slow` passed 20 of 20 and `quit fail` ended with 1 at once, 20 of 20, nothing left running. Commits: main 6c9edffe5da1f5b5383977aeab3f544c0de25d4c (2026-11-14); this PR `mac/control-quit` @ 58a56b555ddbc1cb2a2a21ca04e156946efa7a57 (on #456 @ 60dd4e0c). Before the rebase: main 5e072fc8c2adadba5ea610148be1c47800ba0cca (2026-11-14); this PR `mac/control-quit` @ 2695995219b146707cd56958824dad6133ec9f6e. Baseline (the second table, before the rebase): `mac/control-sockets` @ `f1f1843d`, with this PR's hook and two runs but not its fix (`main` cannot build the control fixture on macOS at all, so the race cannot be shown there). Hardware: Apple M4 Max, 16 cores, 64 GiB, macOS 26.7.1 (25G241); Apple clang 21.0.0. No GPU. Scene: none. Game-free checks. Method: after the rebase, `tests/test_control.py --cc clang --arch arm64`, then `--arch x86_64`, at `58a56b55` through `tools/slot.py` in the suite's clean environment, and the whole suite on this PR compared by test name with `main`'s run at `6c9edffe`. Before the rebase: `control_test` built as `test_control.py` builds it on macOS (native arm64), then run `<folder> quit slow` and `<folder> quit fail` with stdout and stderr on pipes: 5 times each without the fix, 20 times each with it, 20 s at most per run (a run past 20 s is a hang). Then `tests/test_control.py --cc clang --arch arm64` and `--arch x86_64`, and the control checks through `tests/run_all.py` on `main` and on this PR, interleaved, two runs each. How to reproduce: `python -I -B tests/test_control.py --cc clang --arch arm64` (and `--arch x86_64`), with a short `TMPDIR`. | | Before: main @ 6c9edffe | After: this PR @ 58a56b55 | |---|---|---| | `tests/test_control.py` | FAIL (`host/control.c` does not compile on macOS) | pass: 40 PASS lines with `--arch arm64`, the same 40 with `--arch x86_64`; 12.6 s in the suite | | whole suite on this Mac, 6 at once | 531 passed, 32 failed, 17 skipped | 532 passed, 31 failed, 17 skipped | ``` main @ 6c9edffe: 531 passed, 32 failed, 17 skipped in 181 s (linux, 6 at once) this PR @ 58a56b55: 532 passed, 31 failed, 17 skipped in 186 s (linux, 6 at once) ``` Before the rebase: | | control-sockets @ f1f1843d + hook, no fix | this PR @ 26959952 | |---|---|---| | `quit slow` | 5 of 5 hang: the parent exits 1 at `connect`, its child serves on | 20 of 20 pass, median 0.38 s | | `quit fail` | 5 of 5 hang: the parent exits 1 at the injected failure, its child serves on | 20 of 20 end with 1 at once (median 0.09 s), nothing left running | | `test_control.py --arch arm64` / `x86_64` | | exit 0, 40 PASS lines / exit 0, the same 40 | ``` control-sockets @ f1f1843d + hook, no fix (each of the 5 runs, then still running at 20 s with one live child): quit slow: FAIL:340 connect(c->fd, (struct sockaddr *)&a, sizeof a) == 0 quit fail: FAIL:493 !"a failure injected once the child serves" this PR @ 26959952: quit slow: runs=20 hangs=0 passes=20, pass_seconds_median=0.3835 max=0.7469 quit fail: runs=20 hangs=0, each code 1: FAIL:521 !"a failure injected once the child serves", none left running PASS: quit: the reply, then a clean exit, the socket removed PASS: quit: a check failing once the child serves ends the child with it ``` | control checks through `tests/run_all.py` (run 1 / run 2) | main @ 5e072fc8 | this PR @ 26959952 | |---|---|---| | `tests/test_control.py` | FAIL / FAIL (`control.c` does not compile on macOS) | pass / pass | | `tests/test_longshot_ctl.py` | pass / pass | pass / pass | | `tests/test_oot_state_tap.py` | FAIL / FAIL | FAIL / FAIL (the fixture's `vsnprintf` macro, which the runner PR repairs) | **Earlier, on the old base** (`ab92a95f`, on the port's integration branch, the same fixture and hook): the original quit case hung 8 times in 1,200 runs under load (7 of 900 arm64, 1 of 300 x86_64), each the refused `connect`. With the fix, 0 hangs in 1,900 runs (arm64 and x86_64, idle and loaded). Not rerun here. Screenshots: none; nothing on screen. ### Flags and settings None. ## Testing **Tested:** - **After the rebase** (head `58a56b55` on `6c9edffe`): `python -I -B tests/test_control.py --cc clang --arch arm64`, then `--arch x86_64`: exit 0, 40 PASS lines each, the two quit runs among them. - The whole suite, `tests/run_all.py`, 6 at once, on the same Mac, after the rebase, compared by test name with `main` (the summary lines are under Measurements): - **By test name, one check changed:** `tests/test_control.py`, FAIL on `main` (the bridge does not compile on macOS there) → pass here in 12.6 s, with the quit runs. At `26959952`, before the rebase, no control fixture was left running after the suite. (The control-sockets PR's own suite run then, without this fix, met the quit race under load and left one running.) - **The 31 remaining failures also fail on `main`.** Fifteen were named here before the rebase: `test_appimage_package_retry`, `test_collect_wide`, `test_game_layout`, `test_low_memory_notice`, `test_mem_gate`, `test_mem_gate_children`, `test_mirror_trim`, `test_oot_state_tap`, `test_pcache_header_read`, `test_pipeline_cache_file`, `test_pipeline_cache_save`, `test_pipeline_play_cache`, `test_prof_symbols`, `test_program_cache_cleanup`, `test_program_cache_one_variant`. The port's other PRs fix them. The three named before as stale on `main` (`test_framegen_resources`, `test_module_ids`, `test_pad_layout`) now pass on `main` and here. - **The other 16 are new on `main`'s list since `5e072fc8`**, and fail on both: `test_appimage_player_build`, `test_cpm_discovery`, `test_crash_dialog`, `test_crash_record`, `test_env_on`, `test_lift_cleanup_error`, `test_linux_preflight`, `test_longshot_ctl`, `test_make_game_cache`, `test_pipeline_tour`, `test_readback_late`, `test_report_run`, `test_split_giants`, `test_vk_pgo`, `test_vram_budget_row`, `test_walk_key`. - Before the rebase: the two new runs without and with the fix, and the control checks on `main` and here, two interleaved runs each, as under Measurements. - The Windows and Linux text after the rebase, by `unifdef` against `main` @ `6c9edffe`: `host/control.c` with `-U__APPLE__ -UHOS_CONTROL_NO_GAME` (the game build) identical; with `-U__APPLE__ -D_WIN32` (Windows, the test build too) identical; the Linux test build (`-U__APPLE__ -U_WIN32 -DHOS_CONTROL_NO_GAME`) differs by the hook only (its declaration and its one call between `bind` and `listen`). `tests/control_test.c` differs on every platform: `CHECK` calls `check_failed` (on Windows it prints and exits 1, as before) and the POSIX quit case is the new one. - **After the rebase onto main aa95d35f**, at `fe3e8c91` (`58a56b55` before the rebases onto e090cc48 and aa95d35f): Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they apply) gave the same results as before; `python -I -B tests/run_all.py` on wave S1's merge `11a5b4e7`, which carries this head: 568/22/17 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure (`reports/2026-11-15-rebase-sweep-3.md`). **Not tested:** - **Linux.** It runs the same POSIX quit case and gets the two new runs. Not run here. - **Windows.** Its run list and code path are unchanged; `check_failed` only prints and exits there, as before. Not run here. **Must be tested:** - Linux and Windows, any GPU (game-free checks only, no game build): run `python -I -B tests/test_control.py` on this PR (head fe3e8c91) and on main aa95d35f in the same checkout, expecting it to pass on both, with no hang; on Linux this PR prints two more PASS lines than main (the quit case with its bridge slow to listen, and `quit: a check failing once the child serves ends the child with it`), and on Windows the same PASS lines as main (by hand: test request T-B6ML). ## Risks **What could break:** - Test code only, apart from the hook, which only the test build (`HOS_CONTROL_NO_GAME`) compiles. - The parent now waits 10 s at most for the child's word. A bridge that takes longer to start fails the check instead of hanging it. - `CHECK` is now a call to `check_failed` on every platform, Windows included (where it prints and exits as before). - Landing order: after #456 (`mac/control-sockets`), whose two commits it carries; best merged right after it. **Turning it off:** revert the PR. **Compatibility:** Nothing changes: the game build of `host/control.c` is `main`'s on Windows and Linux; saves, the options file, the shader caches and the vk ABI are unchanged. ## Issues See #420. ## Checklist ### Everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`) of the status line's type. - [x] Why gives the reason plainly and links the issues that fit (the tracker searched). - [x] Measurements has a verdict, or one line saying why there is nothing to measure. - [x] Testing names what was tested, what wasn't, and what must still be tested, where and how. - [x] Rebased on current `main`: on aa95d35f (#761, 2026-11-15), head `fe3e8c91`, on #456 `fc2f1040`, without a conflict; this PR's own commits, old → new: `58a56b55` → `fe3e8c91`; the commit names elsewhere in this description are from before the rebase. - [x] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is named). (532 passed, 17 skipped, and 31 failures that also fail on `main` at `6c9edffe` (531 passed, 32 failed, 17 skipped), named under Testing. After the rebase onto main aa95d35f (head `fe3e8c91`): wave S1's merge `11a5b4e7`, 568/22/17 against main aa95d35f's 543/33/17, no new failure.) - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### When it applies - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] A game-free test added for the fix, failing on `main` and passing here. (The two new runs hang without the fix and pass with it; on `main` the fixture does not build on macOS.) - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Docs updated. (`docs/control.md`, in #456's carried commits; this PR's own commit changes test code and the test-build hook only.)
Use public Darwin socket, fcntl and getpeereid calls behind Apple-only
branches. Keep the real sockaddr_un path bound, set sun_len, and retain
private-directory and mode checks. Fail closed on peer verification.

Distinguish full disconnects from half-closes without SIGPIPE. Keep full
live listen queues safe from stale-socket removal and allow queued client
bursts to reach the existing busy reply. Leave Linux and Windows source
paths unchanged.

Extend the real game-free control fixture for both Apple clang targets,
including descriptor flags and an exec child. Document the non-atomic
fcntl window and the separate future macOS process-lifecycle requirement.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
test(control): link the bridge's macOS socket helpers into test_oot_state_tap's fixture
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
PR description / Description follows the template (pull_request_target) Successful in 6s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Third-party parts / As their notes say (pull_request) Successful in 12s
Website / Website on Cloudflare (pull_request_target) Has been skipped
f1f1843db3
Main's tests/test_oot_state_tap.py (sampled-state streaming, #58) builds
host/control.c into its own fixture. On macOS, control.c now calls the
Unix socket helpers in host/posix/darwin_control_sockets.c, so the
fixture adds that file there, as tests/test_control.py does. The
Windows and Linux compile commands are unchanged.

With the runner PR's repair of the fixture's vsnprintf macro, the check
passes on macOS; without it, it still stops at that macro.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
test(control): quit's parent waits for its bridge to listen, and a failed check ends the bridge it forked
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 7s
CI / Conventional Commit title (pull_request) Successful in 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Third-party parts / As their notes say (pull_request) Successful in 9s
Website / Website on Cloudflare (pull_request_target) Has been skipped
2695995219
control_test's quit case (Linux and macOS) took the socket's file for a
listening bridge: the bridge binds, which makes the file, then listens,
and a connect between the two is refused. The parent then failed its
check and exited, while the child it forked served on holding the
parent's stdout and stderr, so test_control.py waited for them to its
180 s limit and the child was left running for good. On this Mac under
the full suite's load it happened in about 1 run of 150 (macOS refuses
with ECONNREFUSED as Linux does).

The child now says on a pipe that hos_control_serve returned, which is
after listen; a failed check ends the forked bridge before the process
exits; the parent waits 5 s at most for the child's exit, as Windows
does. Two new runs make the races deterministic: quit slow (the test
build's bridge waits 300 ms between bind and listen, a hook compiled
only with HOS_CONTROL_NO_GAME) and quit fail (a check fails once the
child serves; it must end with 1 and leave nothing behind). Both hung
before this change. The game's bridge is unchanged, and so is Windows.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mooki force-pushed mac/control-quit from 2695995219
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 7s
CI / Conventional Commit title (pull_request) Successful in 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Third-party parts / As their notes say (pull_request) Successful in 9s
Website / Website on Cloudflare (pull_request_target) Has been skipped
to 58a56b555d
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 3s
2026-11-15 00:34:26 +00:00
Compare
mooki changed title from WIP: test(control): wait until the bridge listens, and reap it when a quit check fails to test(control): wait until the bridge listens, and reap it when a quit check fails 2026-11-15 05:25:40 +00:00
mooki force-pushed mac/control-quit from 58a56b555d
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 3s
to b767e799c5
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 6s
Third-party parts / As their notes say (pull_request) Successful in 11s
2026-11-15 07:16:41 +00:00
Compare
mooki force-pushed mac/control-quit from b767e799c5
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 6s
Third-party parts / As their notes say (pull_request) Successful in 11s
to fe3e8c9178
Some checks failed
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 6s
Third-party parts / As their notes say (pull_request) Successful in 10s
PR description / Description follows the template (pull_request_target) Successful in 4s
2026-11-15 13:05:37 +00:00
Compare
Author
Member

Folded into #845 (feat(macos): run the host on macOS: build, runtime, control, crash records and the Vulkan loader)

Folded into #845 (feat(macos): run the host on macOS: build, runtime, control, crash records and the Vulkan loader)
mooki closed this pull request 2026-11-15 18:07:26 +00:00
Some checks failed
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 6s
Third-party parts / As their notes say (pull_request) Successful in 10s
PR description / Description follows the template (pull_request_target) Successful in 4s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!457
No description provided.