feat(control): relaunch, the game started again over the bridge as the Insert menu's Restart now does #97

Open
dyst wants to merge 2 commits from dyst/Longshot:feat/relaunch into main
Member

Why

A test script that needs a fresh game process, to load a renderer built since or to start a measurement clean, has to quit the game and launch it again itself: the right environment, the saves, a new socket. This lets it ask the running game to start again, the way the Insert menu's Restart now does, and carry on over the same socket.

What changed

  • bridge: cmd_relaunch in host/control.c (the relaunch command) replies first, lets go of its socket (transport_cleanup), then calls the game's relaunch; a game without one answers unavailable.
  • game: game_relaunch in host/control.c does what quit does first (rx_close_asked), then the Insert menu's Restart now path (hos_relaunch_ask, hos_window_closed_exit in host/hos_relaunch.c): the same command and the environment the game started with, so HOS_CONTROL names the same socket. Linux: an execve under the same pid; Windows: a new process that waits for the old one to end (HOS_RESTART_OF).
  • interface: HosControlGame.relaunch (host/control.h), added last.
  • client: relaunch [--timeout 120] in scripts/longshot_ctl.py (relaunch()) waits until a status answers from the process started again: one whose uptime is no longer than the wait. The old process can answer for a moment after its reply, and is not taken for the new one (a bug the first version had; the tests now cover it). A connection the old process drops mid-request is tried again: on Windows the client can meet the old game's pipe as it closes, and the write fails with OSError (EINVAL), which the wait loop let through as a traceback (seen once in a whole run_all with the PC busy; fixed in the second commit).
  • tests: tests/control_test.c and tests/test_control.py (a fake game's relaunch: the reply, the socket let go of, then its relaunch; refused by a game without one); tests/test_longshot_ctl.py (a stand-in that really starts again, os.execv, twice back to back; and test_relaunch_through_a_dropped_connection, a bridge whose first status fails that way, which fails without the fix).

Flags and settings

None (a bridge command and its client command; no switch, menu row or ini key).

Measurements

main: d725b363 (2026-11-13)
this PR: feat/relaunch @ 84411238
Hardware: RTX 3080 (NVIDIA 621.14), Ryzen 9 3900X, 64 GB, Windows 11
Scene: from the title (the save's Continue) into play in Kokiri Forest; headless, muted; both builds from one work folder (only the host differs)
Method: two interleaved rounds each (main, PR, main, PR). main: what a script does today: quit, wait for the process to end, launch again, press A until status says play. This PR: relaunch, then the same presses. Times from the script's clock, from the ask.
How to reproduce: build both into their own game folders and copy a save in. main: longshot_ctl.py launch <game> --headless, press A until play, then quit, launch again and press A to play. This PR: the same first launch, then longshot_ctl.py relaunch and press A to play.

main @ d725b363 this PR @ 84411238
bridge answering again (s), median of 2 5.9 2.2
in play again (s), median of 2 37.8 41.6
main 1: quit + launch: answering after 5.7 s, in play after 39.1 s
pr 1: relaunch: relaunched: answering again after 2.2 s (uptime 0.0 s); answering after 2.4 s, in play after 49.2 s; map KokiriForest_Child
main 2: quit + launch: answering after 6.1 s, in play after 36.5 s
pr 2: relaunch: relaunched: answering again after 1.6 s (uptime 0.1 s); answering after 2.0 s, in play after 34.0 s; map KokiriForest_Child
errors in the PR runs' logs: 0

The bridge is back sooner; the time to play is the game's own start and the presses either way (one slow PR round; two rounds don't separate them). The point is that a script needs no launch logic of its own, not speed. Rebased since onto main c67cd4a0: none of the commits since d725b363 touch host/control.c, host/control.h, host/hos_relaunch.c or the client (the forge's scripts, releases, the builders, the website, shader preparation, a lift fix, menu rows), and the second commit changes only what the client does when a connection fails, so the numbers hold.

Testing

Tested: on the Windows PC above: the relaunch in the game (two rounds, the old process logging "window closed for a restart", the new one serving the same pipe, Continue to Kokiri Forest, no errors in the logs); tests/run_all.py, the whole suite, on this branch and on main c67cd4a0 (496 passed and 5 failed on both, no failure main doesn't have: test_builder_dlss_update, test_check_third_party, test_fpx_sticky, test_release and test_spawn_emit fail on main too); tests/test_control.py and tests/test_longshot_ctl.py also on Linux (Docker, python:3.12 with gcc), three times on this branch's head.

Not tested: the game's relaunch on Linux (an execve of the same pid; the bridge and the client's sides ran there, the restart path itself is the Insert menu's, unchanged).

Needs other testers:

  • Linux: longshot_ctl.py launch <game>, then longshot_ctl.py relaunch, expecting "relaunched: answering again after N s" and the game back at its title, the log saying "window closed for a restart" (T-C36M)

Screenshots

Nothing on screen.

Risk and rollback

A script that sends relaunch to a game whose restart fails leaves it exited, as the Insert menu's Restart now would ("restart: the game could not be started again" in the log). Nothing changes for players: the command needs the control bridge (HOS_CONTROL), which a player's game does not start. Reverting the two commits removes it.

Reviewer notes

The dev-loop proposal's B1, its first part (https://github.com/NotRexed/Hookshot/pull/2636). The second part, a renderer built since taken at the next start (HOS_DEV_DLL_SWAP, scripts/stage_renderer.py), is its own PR and uses this.

Issues

See https://github.com/NotRexed/Hookshot/pull/2636

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • 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.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths.

🤖 Generated with Claude Code

## Why A test script that needs a fresh game process, to load a renderer built since or to start a measurement clean, has to quit the game and launch it again itself: the right environment, the saves, a new socket. This lets it ask the running game to start again, the way the Insert menu's Restart now does, and carry on over the same socket. ## What changed - bridge: `cmd_relaunch` in `host/control.c` (the `relaunch` command) replies first, lets go of its socket (`transport_cleanup`), then calls the game's `relaunch`; a game without one answers `unavailable`. - game: `game_relaunch` in `host/control.c` does what `quit` does first (`rx_close_asked`), then the Insert menu's Restart now path (`hos_relaunch_ask`, `hos_window_closed_exit` in `host/hos_relaunch.c`): the same command and the environment the game started with, so `HOS_CONTROL` names the same socket. Linux: an `execve` under the same pid; Windows: a new process that waits for the old one to end (`HOS_RESTART_OF`). - interface: `HosControlGame.relaunch` (`host/control.h`), added last. - client: `relaunch [--timeout 120]` in `scripts/longshot_ctl.py` (`relaunch()`) waits until a `status` answers from the process started again: one whose `uptime` is no longer than the wait. The old process can answer for a moment after its reply, and is not taken for the new one (a bug the first version had; the tests now cover it). A connection the old process drops mid-request is tried again: on Windows the client can meet the old game's pipe as it closes, and the write fails with `OSError` (EINVAL), which the wait loop let through as a traceback (seen once in a whole `run_all` with the PC busy; fixed in the second commit). - tests: `tests/control_test.c` and `tests/test_control.py` (a fake game's relaunch: the reply, the socket let go of, then its relaunch; refused by a game without one); `tests/test_longshot_ctl.py` (a stand-in that really starts again, `os.execv`, twice back to back; and `test_relaunch_through_a_dropped_connection`, a bridge whose first `status` fails that way, which fails without the fix). ## Flags and settings None (a bridge command and its client command; no switch, menu row or ini key). ## Measurements main: d725b363 (2026-11-13) this PR: feat/relaunch @ 84411238 Hardware: RTX 3080 (NVIDIA 621.14), Ryzen 9 3900X, 64 GB, Windows 11 Scene: from the title (the save's Continue) into play in Kokiri Forest; headless, muted; both builds from one work folder (only the host differs) Method: two interleaved rounds each (main, PR, main, PR). main: what a script does today: `quit`, wait for the process to end, `launch` again, press A until `status` says play. This PR: `relaunch`, then the same presses. Times from the script's clock, from the ask. How to reproduce: build both into their own game folders and copy a save in. main: `longshot_ctl.py launch <game> --headless`, press A until play, then `quit`, `launch` again and press A to play. This PR: the same first launch, then `longshot_ctl.py relaunch` and press A to play. | | main @ d725b363 | this PR @ 84411238 | |---|---|---| | bridge answering again (s), median of 2 | 5.9 | 2.2 | | in play again (s), median of 2 | 37.8 | 41.6 | ``` main 1: quit + launch: answering after 5.7 s, in play after 39.1 s pr 1: relaunch: relaunched: answering again after 2.2 s (uptime 0.0 s); answering after 2.4 s, in play after 49.2 s; map KokiriForest_Child main 2: quit + launch: answering after 6.1 s, in play after 36.5 s pr 2: relaunch: relaunched: answering again after 1.6 s (uptime 0.1 s); answering after 2.0 s, in play after 34.0 s; map KokiriForest_Child errors in the PR runs' logs: 0 ``` The bridge is back sooner; the time to play is the game's own start and the presses either way (one slow PR round; two rounds don't separate them). The point is that a script needs no launch logic of its own, not speed. Rebased since onto main c67cd4a0: none of the commits since d725b363 touch `host/control.c`, `host/control.h`, `host/hos_relaunch.c` or the client (the forge's scripts, releases, the builders, the website, shader preparation, a lift fix, menu rows), and the second commit changes only what the client does when a connection fails, so the numbers hold. ## Testing **Tested:** on the Windows PC above: the relaunch in the game (two rounds, the old process logging "window closed for a restart", the new one serving the same pipe, Continue to Kokiri Forest, no errors in the logs); `tests/run_all.py`, the whole suite, on this branch and on main c67cd4a0 (496 passed and 5 failed on both, no failure main doesn't have: test_builder_dlss_update, test_check_third_party, test_fpx_sticky, test_release and test_spawn_emit fail on main too); `tests/test_control.py` and `tests/test_longshot_ctl.py` also on Linux (Docker, python:3.12 with gcc), three times on this branch's head. **Not tested:** the game's relaunch on Linux (an `execve` of the same pid; the bridge and the client's sides ran there, the restart path itself is the Insert menu's, unchanged). **Needs other testers:** - Linux: `longshot_ctl.py launch <game>`, then `longshot_ctl.py relaunch`, expecting "relaunched: answering again after N s" and the game back at its title, the log saying "window closed for a restart" (T-C36M) ## Screenshots Nothing on screen. ## Risk and rollback A script that sends `relaunch` to a game whose restart fails leaves it exited, as the Insert menu's Restart now would ("restart: the game could not be started again" in the log). Nothing changes for players: the command needs the control bridge (`HOS_CONTROL`), which a player's game does not start. Reverting the two commits removes it. ## Reviewer notes The dev-loop proposal's B1, its first part (https://github.com/NotRexed/Hookshot/pull/2636). The second part, a renderer built since taken at the next start (`HOS_DEV_DLL_SWAP`, `scripts/stage_renderer.py`), is its own PR and uses this. ## Issues See https://github.com/NotRexed/Hookshot/pull/2636 ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [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. ### If applicable - [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. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Member

Test request T-C36M opened for the Linux run (it needs the 'testing wanted' label to be claimed).

Test request T-C36M opened for the Linux run (it needs the 'testing wanted' label to be claimed).
dyst changed title from WIP: feat(control): relaunch, the game started again over the bridge as the Insert menu's Restart now does to feat(control): relaunch, the game started again over the bridge as the Insert menu's Restart now does 2026-11-13 02:38:22 +00:00
dyst force-pushed feat/relaunch from 0fa72f49f0
Some checks failed
Linux AppImage / appimage (pull_request) Blocked by required conditions
Windows builder / Longshot Builder zip (pull_request) Blocked by required conditions
Windows builder / Windows Vulkan back end (llvm-mingw) (pull_request) Blocked by required conditions
Windows builder / dav1d for Windows (llvm-mingw) (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 2s
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / Python source checks (windows-2022, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (windows-2022, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
to 8a323bf0b4
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 8s
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
2026-11-13 11:14:11 +00:00
Compare
Author
Member

Rebased onto main c67cd4a0 and a fix added: on Windows the relaunch client could meet the old game's pipe as it closed, and the write's OSError (EINVAL) ended relaunch in a traceback instead of a retry (seen once in a whole run_all with the PC busy). It now tries again; tests/test_longshot_ctl.py has a check for it that fails without the fix. Whole suite on this head and on main: 496 passed, 5 failed on both, no new failure; test_control.py and test_longshot_ctl.py three times on Linux. The description follows the template after #76 and #106 (only the If applicable boxes that apply, GitHub references as full URLs).

Rebased onto main c67cd4a0 and a fix added: on Windows the relaunch client could meet the old game's pipe as it closed, and the write's OSError (EINVAL) ended relaunch in a traceback instead of a retry (seen once in a whole run_all with the PC busy). It now tries again; tests/test_longshot_ctl.py has a check for it that fails without the fix. Whole suite on this head and on main: 496 passed, 5 failed on both, no new failure; test_control.py and test_longshot_ctl.py three times on Linux. The description follows the template after #76 and #106 (only the If applicable boxes that apply, GitHub references as full URLs).
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 8s
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
This pull request has changes conflicting with the target branch.
  • host/control.c
  • host/control.h
  • tests/test_control.py
  • tests/test_longshot_ctl.py
Some workflows are waiting to be reviewed.
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'feat/relaunch:dyst-feat/relaunch'
git switch 'dyst-feat/relaunch'
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!97
No description provided.