fix(linux): acknowledge Play's scope hand-off before closing #2643
Closed
mooki000 wants to merge 1 commit from
mooki000/fix/linux-play-scope-handoff into main
pull from: mooki000/fix/linux-play-scope-handoff
merge into: MagicBeanDev:main
MagicBeanDev:main
MagicBeanDev:github/pr/2893
MagicBeanDev:github/pr/2890
MagicBeanDev:github/pr/2887
MagicBeanDev:github/pr/2878
MagicBeanDev:github/pr/2875
MagicBeanDev:github/pr/2874
MagicBeanDev:github/pr/2873
MagicBeanDev:github/pr/2871
MagicBeanDev:github/pr/2756
MagicBeanDev:github/pr/2897
MagicBeanDev:github/pr/2896
MagicBeanDev:github/pr/2718
MagicBeanDev:github/pr/2842
MagicBeanDev:github/pr/2691
MagicBeanDev:github/pr/2895
MagicBeanDev:github/pr/2743
MagicBeanDev:github/pr/2765
MagicBeanDev:github/pr/2708
MagicBeanDev:github/pr/2732
MagicBeanDev:github/pr/2755
MagicBeanDev:github/pr/2817
MagicBeanDev:github/pr/2876
MagicBeanDev:release-please--branches--main
MagicBeanDev:release-please--branches--main--release-notes
MagicBeanDev:github/pr/2778
MagicBeanDev:github/pr/2888
MagicBeanDev:github/pr/2866
MagicBeanDev:github/pr/2877
MagicBeanDev:github/pr/2800
MagicBeanDev:github/pr/2829
MagicBeanDev:github/pr/2850
MagicBeanDev:github/pr/2889
MagicBeanDev:github/pr/2882
MagicBeanDev:github/pr/2860
MagicBeanDev:github/pr/2849
MagicBeanDev:github/pr/2886
MagicBeanDev:github/pr/2794
MagicBeanDev:github/pr/2840
MagicBeanDev:github/pr/2830
MagicBeanDev:github/pr/2834
MagicBeanDev:github/pr/2719
MagicBeanDev:github/pr/2742
MagicBeanDev:github/pr/2741
MagicBeanDev:github/pr/2820
MagicBeanDev:github/pr/2771
MagicBeanDev:github/pr/2859
MagicBeanDev:github/pr/2858
MagicBeanDev:github/pr/2823
MagicBeanDev:github/pr/2757
MagicBeanDev:github/pr/2769
MagicBeanDev:github/pr/2844
MagicBeanDev:github/pr/2843
MagicBeanDev:github/pr/2673
MagicBeanDev:github/pr/2855
MagicBeanDev:github/pr/2739
MagicBeanDev:github/pr/2807
MagicBeanDev:github/pr/2851
MagicBeanDev:github/pr/2789
MagicBeanDev:github/pr/2814
MagicBeanDev:github/pr/2818
MagicBeanDev:github/pr/2774
MagicBeanDev:github/pr/2772
MagicBeanDev:github/pr/2750
MagicBeanDev:github/pr/2786
MagicBeanDev:github/pr/2785
MagicBeanDev:github/pr/2782
MagicBeanDev:github/pr/2779
MagicBeanDev:github/pr/2775
MagicBeanDev:github/pr/2706
MagicBeanDev:github/pr/2766
MagicBeanDev:github/pr/2747
MagicBeanDev:github/pr/2759
MagicBeanDev:github/pr/2745
MagicBeanDev:github/pr/2740
MagicBeanDev:github/pr/2733
MagicBeanDev:github/pr/2725
MagicBeanDev:github/pr/2727
MagicBeanDev:github/pr/2728
MagicBeanDev:github/pr/2729
MagicBeanDev:github/pr/2721
MagicBeanDev:github/pr/2722
MagicBeanDev:github/pr/2715
MagicBeanDev:github/pr/2714
MagicBeanDev:github/pr/2707
MagicBeanDev:github/pr/2703
MagicBeanDev:github/pr/2705
MagicBeanDev:github/pr/2697
MagicBeanDev:github/pr/2679
MagicBeanDev:github/pr/2676
MagicBeanDev:github/pr/2669
MagicBeanDev:docs/what-hookshot-connects-to
MagicBeanDev:fix/hdr-encode-test
MagicBeanDev:fix/main-tests
MagicBeanDev:fix/play-cache-files
MagicBeanDev:perf/game-barrier-order
MagicBeanDev:feat/heap-lazy
MagicBeanDev:feat/texture-detail
MagicBeanDev:perf/astc-opaque-bc1
MagicBeanDev:fix/fs-dir-lock
MagicBeanDev:fix/framegen-native-kept
MagicBeanDev:fix/update-no-downgrade
MagicBeanDev:port/k10-gcn-f16-mirrors
MagicBeanDev:fix/framegen-mult-cap
MagicBeanDev:chore/release-as-2.1.1
MagicBeanDev:perf/pcache-load-switch
MagicBeanDev:fix/prepare-progress-log
MagicBeanDev:fix/update-closes-stale-game
MagicBeanDev:fix/hdr-scaled-sampled
MagicBeanDev:perf/first-start-preset
MagicBeanDev:perf/zero-upload
MagicBeanDev:fix/vcore-escape-2s
MagicBeanDev:test/fake-gh-atomic
MagicBeanDev:perf/batch-idle-flush
MagicBeanDev:test/finish-stub-a
MagicBeanDev:chore/record-2.1.0
MagicBeanDev:fix/main-red-tests
MagicBeanDev:fix/test-display-startup-driver-cache
MagicBeanDev:feat/mesh-shader-mode
MagicBeanDev:fix/menu-probes-at-startup
MagicBeanDev:fix/fps-counter-classic
MagicBeanDev:fix/test-log-batch-flake
MagicBeanDev:fix/narrow-screen-hud
MagicBeanDev:ci/prereleases
MagicBeanDev:release/v2.0.15
MagicBeanDev:hotfix/v2.0.15
MagicBeanDev:fix/present-ahead-replay
MagicBeanDev:release/v2.0.13
MagicBeanDev:fix/bracket-paths-2013
MagicBeanDev:fix/hdr-overlays-2013
MagicBeanDev:fix/fps-counter-scale-2013
MagicBeanDev:fix/stick-seen-at-rest
MagicBeanDev:hotfix/menu-fits-screen
MagicBeanDev:fix/f1-always-opens-menu
MagicBeanDev:fix/ini-keep-lines
MagicBeanDev:claude/cranky-pike-7cf247
MagicBeanDev:perf/texture-answer-flat
MagicBeanDev:fix/stamp-test-modules
MagicBeanDev:fix/appimage-fonts
MagicBeanDev:fix/display-awake
MagicBeanDev:release/v2.0.12
MagicBeanDev:fix/discord-windows
MagicBeanDev:rc/v2.0.12-tests
MagicBeanDev:fix/dlss-keep-feature
MagicBeanDev:rc/v2.0.12
MagicBeanDev:button-icons
No reviewers
Labels
Clear labels
accessibility
Barrier affecting people with disabilities
autorelease: pending
autorelease: tagged
bug
Something isn't working
dependencies
Pull requests that update a dependency file
documentation
Improvements or additions to documentation
duplicate
This issue or pull request already exists
enhancement
New feature or request
github_actions
Pull requests that update GitHub Actions code
good first issue
Good for newcomers
help wanted
Extra attention is needed
invalid
This doesn't seem right
question
Further information is requested
testing wanted
wontfix
This will not be worked on
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
Milestone
Clear milestone
No items
No milestone
Assignees
Clear assignees
No assignees
2 participants
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/Hookshot!2643
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki000/fix/linux-play-scope-handoff"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this is for: make Linux Builder Play wait for the game’s actual scope hand-off before closing, and start it directly if that hand-off fails. This follows #2540 and @toniosim’s analysis in #2529; thanks to @klikso for the original fix. What is measured: game-free checks and 26 transient-user-unit trials on Arch/Hyprland with systemd 262.1, comparing
9c5612f0with the runtime code in6ba5433d. The two injected failures start nothing on main and start the stand-in on this branch; ten ordinary launches work on each. Not measured: a real game, a graphical Play click, a rebuilt AppImage, or logout.Refs #2529
Refs #2540
Problem
own_scope()proves thatsystemd-run … trueworks.play()then starts a different, asynchronoussystemd-runand immediately returns toplay_and_close(), which destroys the Builder window.Two controlled cases reproduce the remaining gap:
Type=simple,ExitType=main,KillMode=control-groupservice, the Builder exits and its service stops that runner before the hand-off.The delays/failures are deliberate injections, not an estimate of their frequency on players’ desktops.
What changed
builder/linux/hookshot_builder.py: share the existing unified-cgroup reader, then poll the real child until it leaves the launcher’s cgroup and its descendants. Stop at the first proof; otherwise stop on child exit or a two-second bound./dev/nullbefore executing the existing diagnostics command. The game’s stdin/stderr socket arrangement is unchanged.builder/linux/play-diagnostics.sh: include the fallback notice alongside the existing exit status inhookshot_startup.log, when that log is published after the game exits.tests/test_linux_play_scope.py: injected clock, cgroups and process outcomes cover actual-call failure, a delayed hand-off, bounded fallback exactly once, first-proof return without a fixed delay, and exec failure.--sourceand--caseallow each regression to be checked against main independently.tests/test_linux_play_diagnostics.py: exercise publication of the fallback notice while preserving bounded stderr and the game’s own logging behavior.readme.md: describe the bounded hand-off and the limitation of a plain fallback.The original scope-selection policy and successful trial remain. Terminal launches, existing scopes, absent systemd, and an unavailable user manager retain their previous choice of launch path.
How it was tested
Arch/Hyprland, kernel
7.2.15-1-omarchy, systemd262.1-1-arch, GCC 16.2.1 and Clang 23.1.3, 2026-11-11. Shared host; no window or GPU use, and no performance claim. The runs below were made on6ba5433dover base9c5612f0. The branch has since been rebased onto mainde866017as80f16e6d; the launcher and its two tests are unchanged by the rebase, and both tests were run again there (pass), as were the five cases against main's launcher (each fails).Game-free checks
test_linux_docker_packages.py(PPA parser sees--no-updateinstead of the PPA).9c5612f0and failed with the same error signature. This is not a green-suite claim; every check touching the launcher passes. The inherited failures are listed below and are not changed in this PR.Full-suite failure list
test_wayland_libdecor_hint.py,test_display_startup_failure.py,test_texel_buffer_ownership.py,test_audio_aux.py,test_audio_biquad.py,test_dlss_checks.py,test_env_on.py,test_framegen_preset.py,test_hdr_encode.py,test_linux_docker_packages.py,test_mods_size_table.py,test_mouse_native.py,test_readback_part.py,test_rumble_menu.py.Real transient units: every trial
The real imported Builder module’s
Builder.play_and_close()was called without constructing Tk; only the object’sdestroywas replaced. The executable was an authored heartbeat script with a start counter, not a mock of the launch code.The delayed/ordinary trials use
ExitType=mainto model the reported launcher lifetime. The exit-72 and timeout trials useExitType=cgroupto isolate fallback: a plain child cannot escape a main-lifetime service, and this change does not claim otherwise. These properties model the issue, not every GNOME/KDE/COSMIC release’s defaults.The fallback’s actual startup log:
All 48 named transient services/scopes were collected, all stand-ins and shims removed, and the final all-state unit listing for the test prefix was empty. No persistent unit, desktop file, configuration change, or user game-file change was made.
Not changed here
Logout policy is unchanged. In the earlier review, the generated scope had empty
PartOf/BindsTo; stopping an isolated mock session boundary stopped a deliberately bound control scope but left that scope alive. Inference, not a real logout result: a lingering user manager could leave it running unless the game or desktop independently exits it. This patch does not add session dependencies.The existing stale-game closer is Windows-only; this patch does not add a Linux closer. It also does not change the game’s response to SIGTERM.
Not tested
A real desktop’s Play click; a GNOME, KDE or COSMIC session; actual logout; Flatpak confinement; a rebuilt AppImage; Windows or a real game/GPU run. The stand-in tests do not establish how the real game responds to display loss.
Risks
Overlaps
The overlap check found #2547 adds its Add to Steam paragraph at the same README insertion point; the hunk was read. This is a documentation-ordering conflict, not a shared launcher implementation change. #1759, #1872, #1958, #2317, #2373, #2430 and #2465 share a changed code file and merge cleanly according to the overlap check. This follows, rather than replaces, merged #2540.
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra [email protected]
I merged this into current main and ran tests/test_linux_play_scope.py and tests/test_linux_play_diagnostics.py (both pass), plus
run_all.py linux builder play. The only failure there is test_portable_local_builder, which is Windows-only and fails the same way on main when forced. CI is green.I traced the hand-off.
own_scopeusessystemd-run --user --scope, which moves its own PID into the new scope before it execs. Sowait_scope(builder/linux/hookshot_builder.py:186) polling/proc/<runner pid>/cgroupfor a cgroup outside the launcher's is the right proof. The gate instart_scoped(:203) only lets the shellexecafter "go\n". On timeout or failure, the closed socket plusos.killpg(:224) and the reap happen before the plainPopen, so a late scope transfer can't start a second game. The diagnostics script keeps its stdin/stderr sockets, and stdout is put back to /dev/null before the exec.Two things for the record, neither blocking. Every Play from a service-launched launcher can now wait up to 2 s, though in practice it returns at the first proof. And the fallback still dies with a
ExitType=mainservice, as the readme now says. A real GNOME/KDE Play click would still be good to have.