fix(linux): Play from a desktop launcher no longer has systemd stop the game #2540

Merged
klikso merged 1 commit from linux/play-own-scope into main 2026-11-12 03:05:10 +00:00
klikso commented 2026-11-11 22:39:44 +00:00 (Migrated from github.com)

On Linux desktops that start each launcher as a systemd service (KDE/SteamOS, GNOME, COSMIC), pressing Play in the launcher window starts the game and then systemd kills it about 90 seconds later, which players see as a silent crash.

Fixes #2529

What changed

  • builder/linux/hookshot_builder.py: new own_scope(). When the Builder is running inside a .service cgroup and systemd-run is available, Play starts the game through systemd-run --user --scope --collect --quiet --, so it lives in a scope of its own and the launcher's service can stop without it. A trial systemd-run ... true runs first; if the user manager can't be reached (or there is no systemd, or the launcher is in a scope or a terminal), Play starts the game exactly as before.
  • tests/test_linux_play_scope.py (new, game-free, runs on Linux and Windows): when the scope is used and when it isn't (scope / cgroup v1 / no /proc / no systemd-run / failing trial / timeout), and that play() puts the prefix in front of both the diagnostics script and the direct-exe fallback.
  • tests/test_linux_play_diagnostics.py: stubs own_scope in the namespace it builds play() in.

The game not exiting on SIGTERM in a windowed run (also noted in the issue) is left for a separate PR.

How it was tested

  • python -I -B tests/run_all.py play builder on Windows: all pass (the Linux-only checks skip there).
  • Steam Deck (SteamOS, systemd 261): the new test passes. A stand-in launcher run as a transient user service (systemd-run --user --unit=... python3 launcher.py, the reporter's setup) that starts a long-lived child with start_new_session=True and exits:
    • before: the service stops and the child is gone;
    • after: the child keeps running in app.slice/run-p<pid>-i<n>.scope.
  • Not run in the game itself; no screen changes.

Assisted-by: Claude Code

🤖 Generated with Claude Code

On Linux desktops that start each launcher as a systemd service (KDE/SteamOS, GNOME, COSMIC), pressing Play in the launcher window starts the game and then systemd kills it about 90 seconds later, which players see as a silent crash. Fixes #2529 ## What changed - `builder/linux/hookshot_builder.py`: new `own_scope()`. When the Builder is running inside a `.service` cgroup and `systemd-run` is available, Play starts the game through `systemd-run --user --scope --collect --quiet --`, so it lives in a scope of its own and the launcher's service can stop without it. A trial `systemd-run ... true` runs first; if the user manager can't be reached (or there is no systemd, or the launcher is in a scope or a terminal), Play starts the game exactly as before. - `tests/test_linux_play_scope.py` (new, game-free, runs on Linux and Windows): when the scope is used and when it isn't (scope / cgroup v1 / no `/proc` / no `systemd-run` / failing trial / timeout), and that `play()` puts the prefix in front of both the diagnostics script and the direct-exe fallback. - `tests/test_linux_play_diagnostics.py`: stubs `own_scope` in the namespace it builds `play()` in. The game not exiting on SIGTERM in a windowed run (also noted in the issue) is left for a separate PR. ## How it was tested - `python -I -B tests/run_all.py play builder` on Windows: all pass (the Linux-only checks skip there). - Steam Deck (SteamOS, systemd 261): the new test passes. A stand-in launcher run as a transient user service (`systemd-run --user --unit=... python3 launcher.py`, the reporter's setup) that starts a long-lived child with `start_new_session=True` and exits: - before: the service stops and the child is gone; - after: the child keeps running in `app.slice/run-p<pid>-i<n>.scope`. - Not run in the game itself; no screen changes. Assisted-by: Claude Code 🤖 Generated with [Claude Code](https://claude.com/claude-code)
toniosim commented 2026-11-11 22:58:41 +00:00 (Migrated from github.com)

Tested on COSMIC (Pop!_OS 24.04, systemd 255, NVIDIA 595.104.02, RTX 3070 Ti). Before this PR, Play from the launcher run as a user service (systemd-run --user, same as the desktop's app-hookshot@….service) made the unit go to stop-sigterm as soon as the Builder exited, and the game was SIGKILLed 90 s later (#2529).

With this PR's hookshot_builder.py (run with the v2.1.0 AppImage's Python/Tk), same setup, Play clicked: the unit went to inactive/dead cleanly, the game ran in its own run-r….scope, and it was still alive 151 s later, past the 90 s stop timeout. No SIGTERM/SIGKILL in the journal.

Caveat: this ran the PR's builder from source, not a packaged AppImage built from it.

Tested on COSMIC (Pop!_OS 24.04, systemd 255, NVIDIA 595.104.02, RTX 3070 Ti). Before this PR, Play from the launcher run as a user service (`systemd-run --user`, same as the desktop's `app-hookshot@….service`) made the unit go to `stop-sigterm` as soon as the Builder exited, and the game was SIGKILLed 90 s later (#2529). With this PR's `hookshot_builder.py` (run with the v2.1.0 AppImage's Python/Tk), same setup, Play clicked: the unit went to `inactive/dead` cleanly, the game ran in its own `run-r….scope`, and it was still alive 151 s later, past the 90 s stop timeout. No SIGTERM/SIGKILL in the journal. Caveat: this ran the PR's builder from source, not a packaged AppImage built from it.
mooki000 commented 2026-11-12 03:58:58 +00:00 (Migrated from github.com)

Thanks for the fix, and @toniosim for the analysis. I tested the merged change on Arch (Hyprland, systemd 262.1) with a transient user service shaped like the issue's launcher service and a stand-in for the game: on its base the child was killed 90.1 s after Play; with the fix it was still alive in its own scope after 96 s. So it fixes the reported case here.

Two cases are left, both shown by injection (not an estimate of how often they happen):

  • the probe with true succeeds but the real systemd-run then fails: Play starts nothing and does not fall back;
  • the real systemd-run is 250 ms slow: the window closes first, the launcher's service ends, and the game is never started.

#2643 is a small follow-up for both: Play waits (at most two seconds, usually no time at all) until the real runner has left the launcher's control group before the window closes, and starts the game the plain way exactly once if that does not happen. Ten ordinary launches behave as before on both. Not tested: a real desktop's Play click, GNOME, KDE or COSMIC, and a real log-out.

Thanks for the fix, and @toniosim for the analysis. I tested the merged change on Arch (Hyprland, systemd 262.1) with a transient user service shaped like the issue's launcher service and a stand-in for the game: on its base the child was killed 90.1 s after Play; with the fix it was still alive in its own scope after 96 s. So it fixes the reported case here. Two cases are left, both shown by injection (not an estimate of how often they happen): - the probe with `true` succeeds but the real `systemd-run` then fails: Play starts nothing and does not fall back; - the real `systemd-run` is 250 ms slow: the window closes first, the launcher's service ends, and the game is never started. #2643 is a small follow-up for both: Play waits (at most two seconds, usually no time at all) until the real runner has left the launcher's control group before the window closes, and starts the game the plain way exactly once if that does not happen. Ten ordinary launches behave as before on both. Not tested: a real desktop's Play click, GNOME, KDE or COSMIC, and a real log-out.
Commenting is not possible because the repository is archived.
No description provided.