Sleeping or shutting the host down during a stream works again #391

Merged
enricobuehler merged 2 commits from worktree-host-power-during-stream into main 2026-08-24 09:53:04 +00:00
Owner

Field report: "shutting down or putting the host to sleep, for example through the Steam Big Picture UI, does not work during a stream" — several people, including on their own boxes.

Two independent faults, both ours. Both reproduced on the Bazzite lab VM (.41) on 2026-08-24.

1. Sleep: we vetoed it ourselves

Since b7a00137 the host holds a logind sleep:idle inhibitor in block mode for the whole length of a stream, so a passive (video-only) viewer could not have the box suspend out from under them.

A block lock on sleep refuses every suspend, not just the one an idle timer asks for. "Sleep" in Steam's power menu reaches logind as the same Suspend() call, and logind answers:

Operation inhibited by "Punktfunk" (PID …), reason is "a client is streaming".

Nothing in that UI surfaces a D-Bus error, so the menu entry just does nothing — for as long as anyone is watching the box. Reproduced by taking the identical lock by hand and running systemctl suspend --dry-run.

Fix: hold the veto only while the stream is quiet. Any client input on either plane drops it synchronously — releasing is a close(2) on the inhibitor fd, no round trip, so a Sleep press cannot race it — and it is re-taken only after 30 s of silence.

That is the line the original justification already drew ("remote INPUT resets the compositor's idle timers, but a video-only viewer sends none"): a person choosing Sleep is, by definition, sending input, and a passive viewer never does. The case the lock was written for is unchanged — 30 s of nothing and the veto is back.

Both planes stamp the clock at their single input entry point: the native input thread's channel recv (before the grant tests — a denied event still means a person is there) and the GameStream control plane's three decode arms, past the keepalive gate, because a keepalive is the one thing a passive viewer does send.

2. Shut Down: no active session, so polkit refuses

Not the inhibitor — a sleep lock does not block poweroff, and that was checked rather than assumed.

logind ships power-off/reboot/suspend as allow_active: yes. polkit decides "active" from the caller's own logind session and, for a caller that has none — every systemd --user unit, which is exactly what the managed gamescope session is — falls back to the user's elected DISPLAY session. logind elects that only from user/greeter class sessions, never from the user manager's own, so a takeover that stops the display manager removes the last candidate and all three actions drop to auth_admin_keep: an interactive password prompt, put to a non-interactive caller, on a screen that is switched off.

On SteamOS-like boxes that is precisely the refused call. Steam does not ask logind for "Shut Down" at all — it writes $STEAMOS_STEAM_SHUTDOWN_SENTINEL and exits, and gamescope-session-plus runs a plain poweroff once Steam is gone. During a stream that wrapper is ours, in the session-less transient unit.

Measured, same box, same command:

state pkcheck --action-id org.freedesktop.login1.power-off from a systemd --user unit
sddm up authorized
sddm stopped auth_admin_keep
sddm stopped, with this rule authorized

Fix: packaging/linux/49-punktfunk-power.rules, scoped to the (shipped-empty) punktfunk group — the same group the takeover's own root helper authorizes on, so this grants to exactly the population the fault reaches and to nobody else. The three actions are the three entries in Steam's power menu; the -multiple-sessions and -ignore-inhibit variants are deliberately left out. Installed by rpm/deb/arch; the Bazzite sysext rides the rpm.

Scope, honestly

Since 0.31.0 the Bazzite/Deck takeover no longer stops the display manager, so fault 2 should not reach a current Bazzite host — the rule covers the flavors that still stop one (Nobara's plasmalogin and other mask-fragile DMs) and older installs. Fault 1 reaches every Linux host regardless of flavor.

The mechanisms were verified rather than the click-through: the lab box no longer has a punktfunk-probe binary, so no end-to-end stream was driven. If a Bazzite 0.31.x host still cannot shut down mid-stream, loginctl show-user $(id -u) -p Display during a stream is the one line that tells us whether this is the cause.

Checks

  • cargo test -p punktfunk-host --bins sleep_inhibit in the CI image — whole crate compiles, 2/2 new tests pass
  • cargo clippy -p punktfunk-host --all-targets -- -D warnings — clean
  • the polkit rule verified on a real box in the failing state, and the box restored afterwards
Field report: "shutting down or putting the host to sleep, for example through the Steam Big Picture UI, does not work during a stream" — several people, including on their own boxes. Two independent faults, both ours. Both reproduced on the Bazzite lab VM (`.41`) on 2026-08-24. ## 1. Sleep: we vetoed it ourselves Since b7a00137 the host holds a logind `sleep:idle` inhibitor in **`block`** mode for the whole length of a stream, so a passive (video-only) viewer could not have the box suspend out from under them. A `block` lock on `sleep` refuses **every** suspend, not just the one an idle timer asks for. "Sleep" in Steam's power menu reaches logind as the same `Suspend()` call, and logind answers: ``` Operation inhibited by "Punktfunk" (PID …), reason is "a client is streaming". ``` Nothing in that UI surfaces a D-Bus error, so the menu entry just does nothing — for as long as anyone is watching the box. Reproduced by taking the identical lock by hand and running `systemctl suspend --dry-run`. **Fix:** hold the veto only while the stream is *quiet*. Any client input on either plane drops it synchronously — releasing is a `close(2)` on the inhibitor fd, no round trip, so a Sleep press cannot race it — and it is re-taken only after 30 s of silence. That is the line the original justification already drew ("remote INPUT resets the compositor's idle timers, but a video-only viewer sends none"): a person choosing Sleep is, by definition, sending input, and a passive viewer never does. The case the lock was written for is unchanged — 30 s of nothing and the veto is back. Both planes stamp the clock at their single input entry point: the native input thread's channel recv (before the grant tests — a denied event still means a person is there) and the GameStream control plane's three decode arms, past the keepalive gate, because a keepalive is the one thing a passive viewer *does* send. ## 2. Shut Down: no active session, so polkit refuses Not the inhibitor — a sleep lock does not block poweroff, and that was checked rather than assumed. logind ships `power-off`/`reboot`/`suspend` as `allow_active: yes`. polkit decides "active" from the caller's own logind session and, for a caller that has none — every `systemd --user` unit, which is exactly what the managed gamescope session is — falls back to the user's elected DISPLAY session. logind elects that only from `user`/`greeter` class sessions, never from the user manager's own, so a takeover that **stops the display manager** removes the last candidate and all three actions drop to `auth_admin_keep`: an interactive password prompt, put to a non-interactive caller, on a screen that is switched off. On SteamOS-like boxes that is precisely the refused call. Steam does not ask logind for "Shut Down" at all — it writes `$STEAMOS_STEAM_SHUTDOWN_SENTINEL` and exits, and `gamescope-session-plus` runs a plain `poweroff` once Steam is gone. During a stream that wrapper is ours, in the session-less transient unit. Measured, same box, same command: | state | `pkcheck --action-id org.freedesktop.login1.power-off` from a `systemd --user` unit | |---|---| | sddm up | authorized | | sddm stopped | `auth_admin_keep` | | sddm stopped, **with this rule** | authorized | **Fix:** `packaging/linux/49-punktfunk-power.rules`, scoped to the (shipped-empty) `punktfunk` group — the same group the takeover's own root helper authorizes on, so this grants to exactly the population the fault reaches and to nobody else. The three actions are the three entries in Steam's power menu; the `-multiple-sessions` and `-ignore-inhibit` variants are deliberately left out. Installed by rpm/deb/arch; the Bazzite sysext rides the rpm. ## Scope, honestly Since 0.31.0 the Bazzite/Deck takeover no longer stops the display manager, so fault 2 should not reach a current Bazzite host — the rule covers the flavors that still stop one (Nobara's plasmalogin and other mask-fragile DMs) and older installs. Fault 1 reaches every Linux host regardless of flavor. The mechanisms were verified rather than the click-through: the lab box no longer has a `punktfunk-probe` binary, so no end-to-end stream was driven. If a Bazzite 0.31.x host still cannot shut down mid-stream, `loginctl show-user $(id -u) -p Display` during a stream is the one line that tells us whether this is the cause. ## Checks - `cargo test -p punktfunk-host --bins sleep_inhibit` in the CI image — whole crate compiles, 2/2 new tests pass - `cargo clippy -p punktfunk-host --all-targets -- -D warnings` — clean - the polkit rule verified on a real box in the failing state, and the box restored afterwards
enricobuehler added 2 commits 2026-08-24 09:52:20 +00:00
Since b7a00137 the host has held a logind `sleep:idle` inhibitor in BLOCK
mode for the whole length of a stream, so a passive (video-only) viewer
could not have the box suspend out from under them. A block lock on
`sleep` refuses EVERY suspend, though, not just the one an idle timer
asks for: "Sleep" in Steam's Big Picture power menu reaches logind as the
same `Suspend()` call, and logind answers it with

    Operation inhibited by "Punktfunk" (PID …), reason is "a client is streaming".

Nothing in that UI surfaces a D-Bus error, so the menu entry simply does
nothing — for as long as anyone is watching the box. Reproduced on a
Bazzite host, 2026-08-24, by taking the same lock by hand.

Hold the veto only while the stream is QUIET. Any client input on either
plane drops it synchronously — releasing is a close(2) on the inhibitor
fd, no round trip, so a Sleep press cannot race it — and it is re-taken
only after 30 s of silence. That is the line the original justification
already drew ("remote INPUT resets the compositor's idle timers, but a
video-only viewer sends none"): a person choosing Sleep is, by
definition, sending input, and a passive viewer never does. The case the
lock was written for is unchanged — 30 s of nothing and the veto is back.

Both planes stamp the clock at their single input entry point: the native
input thread's channel recv (before the grant tests — a denied event
still means a person is there) and the GameStream control plane's three
decode arms, past the keepalive gate, because a keepalive is the one
thing a passive viewer does send.
fix(packaging): a takeover that stops the display manager keeps Steam's power menu working
ci / web (pull_request) Successful in 1m53s
ci / bun-nix (pull_request) Successful in 30s
ci / docs-drift (pull_request) Successful in 28s
android / android (pull_request) Successful in 5m44s
ci / rust-arm64 (pull_request) Successful in 5m59s
ci / docs-site (pull_request) Successful in 5m44s
ci / rust (pull_request) Successful in 15m20s
4d155f4985
Second half of "the power menu does nothing during a stream", and an
independent fault from the sleep veto: on the takeover flavors that STOP
the display manager, the box is left with no active local session, and
that is what logind's power actions are gated on.

logind ships `power-off`/`reboot`/`suspend` as `allow_active: yes`.
polkit decides "active" from the caller's own logind session and, for a
caller that has none — every `systemd --user` unit, which is exactly what
the managed gamescope session is — falls back to the user's elected
DISPLAY session. logind elects that only from `user`/`greeter` class
sessions, never from the user manager's own, so stopping the display
manager removes the last candidate and all three actions drop to
`auth_admin_keep`: an interactive password prompt, put to a
non-interactive caller, on a screen that is switched off.

On SteamOS-like boxes that is the call being refused. Steam does not ask
logind for "Shut Down" at all — it writes $STEAMOS_STEAM_SHUTDOWN_SENTINEL
and exits, and gamescope-session-plus runs a plain `poweroff` once Steam
is gone. During a stream that wrapper is ours, in the session-less
transient unit.

Measured on Bazzite, 2026-08-24: the identical
`pkcheck --action-id org.freedesktop.login1.power-off` from a
`systemd --user` unit answers authorized with sddm up, and
`auth_admin_keep` with sddm stopped — and answers authorized again, in
that same stopped state, with this rule installed.

Scope it to the (shipped-empty) `punktfunk` group, which is the same
group the takeover's own root helper authorizes on: a takeover that stops
a display manager cannot work without that helper, so this grants to
exactly the population the fault reaches. The three actions are the three
entries in Steam's power menu; the `-multiple-sessions` and
`-ignore-inhibit` variants are deliberately left out.
enricobuehler scheduled this pull request to auto merge when all checks succeed 2026-08-24 09:52:58 +00:00
enricobuehler merged commit 8e8451ca0c into main 2026-08-24 09:53:04 +00:00
enricobuehler deleted branch worktree-host-power-during-stream 2026-08-24 09:53:08 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#391