Field reports on 0.31.x, Bazzite and Nobara: after disconnecting, the box's own physical screen stays black.
I could not reproduce it. The previous comment has the full negative write-up — five scenarios across both distro families on the real VMs, all recovering cleanly, and the mechanism the first version of this PR proposed disproved on glass. The branch has been rewritten from scratch on that basis.
So this does not guess at the trigger. It closes the gap that lets any trigger end as a dark panel, and fixes the one black-screen path I could prove.
1. The restore never checked its own work
do_restore_tv_session issues a lifecycle verb and logs what systemd said about the job. "The job succeeded" and "the box shows a picture" are different questions, and nothing in this file has ever asked the second one — the restore walks away the moment the verb returns, so every way the box can end up dark looks identical to success in the log.
So measure it. A detached watcher polls detect_active_session(), whose None means no compositor of our uid is running at all — exactly the symptom. Still dark 25 s after the hand-back, it climbs a ladder, each rung measured on both images (Bazzite 44.20260818, Nobara f44, 2026-08-22):
stop the autologin unit. Its login session's script is parked on systemctl --user --wait start <unit> on both images, so a stop releases that wait, the session exits, and Relogin=true logs back in — starting the unit inside a session with a seat. stop, not restart: a restart does not release the parked waiter (measured), which is exactly why it cannot rescue a box the ordinary restart already failed to bring back.
Restart the display manager — what the pre-0.31.0 takeover did on every disconnect, and proven on the Bazzite VM to return the box to game mode.
PUNKTFUNK_RECOVER_SESSION_CMD, then an ERROR naming the command a human has to run.
Detached, and that is load-bearing. The restore holds RESTORE_FLIGHT, which a reconnecting client must take before it can re-take the box; watching for up to a minute while holding it would put that wait in front of every reconnect. The watcher also stands down the instant takeover_live() says a new takeover armed — the box belongs to that stream now, and a remedy fired into it would be a fresh bug. It runs afterclear_takeover() so that check means "a client reconnected", not "our own takeover has not been filed yet".
Skipped on the shutdown path: restore_takeover_now runs inside native.rs's 20 s SHUTDOWN_RESTORE_GRACE, and spending that grace watching would cost the hand-back rather than check it. What covers a shutdown that left the box dark is the next host start — which this PR also makes true.
2. A crashed host left the box's game mode asleep — provably
restore_takeover_on_startup sweeps a leftover idle drop-in and logs that the box's "own Game Mode session would have started and then done nothing". Removing the file does not touch the unit running under it: its ExecStart is still the sleep, so it sits active drawing nothing. Nothing below that sweep restarts it either — the takeover file may be absent, unparseable, or fail takeover_state_is_live, and all three exits leave the box on a dark panel with its game mode "running". Any host killed mid-takeover (SIGKILL, OOM, a yanked update) lands exactly there, and it survives until someone reboots.
hand_back_idled_units_after_crash restarts those units — gated on the box actually being dark, so a user already in game mode or on a desktop is never bounced, and only for active instances (under a just-removed idle drop-in, active means "running the sleep").
Deliberately not changed
The restart verb on the ordinary restore path. It works on both distros (measured), and 0.31.0 chose it deliberately for the idled unit. The stop idea from v1 of this PR survives only as escalation rung 1, where it runs after the proven path has already failed.
cargo test -p pf-vdisplay --lib — 248 passed, 0 failed.
listed_autologin_units factored out of stop_autologin_sessions so both callers share it; its column parsing — which decides whether a live gaming session can be told from a dead leftover — finally has a test against real --plain output from both images.
On-glass validation of both halves on the VMs follows, against a canary built from this branch.
Field reports on `0.31.x`, Bazzite and Nobara: after disconnecting, the box's own physical screen stays black.
**I could not reproduce it.** The [previous comment](https://git.unom.io/unom/punktfunk/pulls/375#issuecomment-4320) has the full negative write-up — five scenarios across both distro families on the real VMs, all recovering cleanly, and the mechanism the first version of this PR proposed disproved on glass. The branch has been rewritten from scratch on that basis.
So this does not guess at the trigger. It closes the gap that lets *any* trigger end as a dark panel, and fixes the one black-screen path I could prove.
## 1. The restore never checked its own work
`do_restore_tv_session` issues a lifecycle verb and logs what systemd said about the **job**. "The job succeeded" and "the box shows a picture" are different questions, and nothing in this file has ever asked the second one — the restore walks away the moment the verb returns, so every way the box can end up dark looks identical to success in the log.
So measure it. A detached watcher polls `detect_active_session()`, whose `None` means no compositor of our uid is running at all — exactly the symptom. Still dark 25 s after the hand-back, it climbs a ladder, each rung measured on both images (Bazzite `44.20260818`, Nobara f44, 2026-08-22):
1. **`stop` the autologin unit.** Its login session's script is parked on `systemctl --user --wait start <unit>` on both images, so a stop releases that wait, the session exits, and `Relogin=true` logs back in — starting the unit inside a session **with a seat**. `stop`, not `restart`: a restart does *not* release the parked waiter (measured), which is exactly why it cannot rescue a box the ordinary restart already failed to bring back.
2. **Restart the display manager** — what the pre-0.31.0 takeover did on every disconnect, and proven on the Bazzite VM to return the box to game mode.
3. **`PUNKTFUNK_RECOVER_SESSION_CMD`**, then an ERROR naming the command a human has to run.
**Detached, and that is load-bearing.** The restore holds `RESTORE_FLIGHT`, which a reconnecting client must take before it can re-take the box; watching for up to a minute while holding it would put that wait in front of every reconnect. The watcher also stands down the instant `takeover_live()` says a new takeover armed — the box belongs to that stream now, and a remedy fired into it would be a fresh bug. It runs *after* `clear_takeover()` so that check means "a client reconnected", not "our own takeover has not been filed yet".
Skipped on the shutdown path: `restore_takeover_now` runs inside `native.rs`'s 20 s `SHUTDOWN_RESTORE_GRACE`, and spending that grace watching would cost the hand-back rather than check it. What covers a shutdown that left the box dark is the next host start — which this PR also makes true.
## 2. A crashed host left the box's game mode asleep — provably
`restore_takeover_on_startup` sweeps a leftover idle drop-in and logs that the box's *"own Game Mode session would have started and then done nothing"*. Removing the **file** does not touch the unit **running under it**: its `ExecStart` is still the sleep, so it sits `active` drawing nothing. Nothing below that sweep restarts it either — the takeover file may be absent, unparseable, or fail `takeover_state_is_live`, and all three exits leave the box on a dark panel with its game mode "running". Any host killed mid-takeover (SIGKILL, OOM, a yanked update) lands exactly there, and it survives until someone reboots.
`hand_back_idled_units_after_crash` restarts those units — gated on the box actually being dark, so a user already in game mode or on a desktop is never bounced, and only for `active` instances (under a just-removed idle drop-in, active means "running the sleep").
## Deliberately not changed
The `restart` verb on the ordinary restore path. It works on both distros (measured), and 0.31.0 chose it deliberately for the idled unit. The `stop` idea from v1 of this PR survives only as escalation rung 1, where it runs *after* the proven path has already failed.
## Checks
- `cargo clippy -p pf-vdisplay --all-targets -- -D warnings` — clean.
- `cargo fmt` — clean.
- `cargo test -p pf-vdisplay --lib` — 248 passed, 0 failed.
- `listed_autologin_units` factored out of `stop_autologin_sessions` so both callers share it; its column parsing — which decides whether a live gaming session can be told from a dead leftover — finally has a test against real `--plain` output from both images.
On-glass validation of both halves on the VMs follows, against a canary built from this branch.
On-glass validation: could NOT reproduce the bug. Do not merge as-is.
I drove the real path on both VMs (home-bazzite-2 119, home-nobara-1 123 on home-node-3) with the headless punktfunk-probe against the pre-fix canary 0.32.0-0.ci15103.g2b91339c. The reported black screen did not reproduce in any scenario, and the premise of this PR is wrong on two counts.
What the boxes actually do
Both images satisfy every precondition I reasoned about:
Bazzite 44.20260818.0
Nobara (f44)
DM
sddm.service
plasmalogin.service
Relogin=true
yes (/usr/lib/sddm/sddm.conf.d/holo.conf)
yes (/etc/plasmalogin.conf)
session script
parked on systemctl --user --wait start gamescope-session-plus@<x>.service
same
unit
@ogui-steam
@steam
Scenarios run (all recovered cleanly)
Bazzite, control on the old DM-stop build (0.31.0-0.ci14629.gd161c126) — took the DM branch, restored the display manager, box back. Confirms the pre-0.31.0 half of the write-up.
Bazzite, pre-fix idled build, 25 s session — idle drop-in + sleep infinity + relogin to session 428 during; after disconnect the drop-in is gone, gamescope is back in the same seat0 session, dpms=On, enabled=enabled.
Bazzite, immediate-disconnect race (keep-alive secs=0, so the restore fires inside the relogin window) — clean.
Nobara, pre-fix idled build, 25 s session — idled=true, relogin to session 11, then removed the takeover's idle drop-in + restored the TV's autologin gaming session, gamescope back, dpms=On.
systemctl --user --wait start does not return on a restart. It only returns on a genuine deactivation. So the host's restart never ends the login session, gamescope starts inside the live seat0 session and gets DRM master normally. The seat-less race this PR is built on does not happen. Verified by hand on Bazzite before touching punktfunk at all.
The loop reaching DM boxes is deliberate, not a regression.d9662c01 chose that verb for exactly this case — "restart, not start: the idle takeover leaves the unit ACTIVE" — and it works on both distros.
So this PR would replace a working, intentional design with a different one on an unproven theory. Converting to WIP.
What is still worth keeping
await_relogin_or_start's backstop reasoning and the handback_verb test are cheap, but they defend against a failure nobody has demonstrated. Parking until there is a real reproduction.
To actually diagnose this
Needed from the two reporters:
journalctl --user -u punktfunk-host --since <disconnect> — the host logs every restore decision by name (scheduled TV-session restore, removed the takeover's idle drop-in, restored the TV's autologin gaming session, or the desktop-active early return). Their log will name the branch taken.
What "black" means: SDDM/plasmalogin greeter, no signal at all, or Game Mode simply not returning?
systemctl --user status gamescope-session-plus@*.service, loginctl list-sessions, and cat /sys/class/drm/*/dpmswhile it is black.
Whether a game was running, and whether the box had been switched to desktop and back during the session — the desktop-active early return at do_restore_tv_session is the one path that legitimately leaves the box un-restored, and it is my leading remaining suspect.
My rig differs from the field in one way worth noting: the video pipeline never delivered frames on either VM (no PipeWire frame within 2.5s … the compositor produced no frames), so these were takeover/teardown exercises rather than fully streaming sessions with a game loaded.
## On-glass validation: could NOT reproduce the bug. Do not merge as-is.
I drove the real path on both VMs (`home-bazzite-2` 119, `home-nobara-1` 123 on home-node-3) with the headless `punktfunk-probe` against the pre-fix canary `0.32.0-0.ci15103.g2b91339c`. **The reported black screen did not reproduce in any scenario**, and the premise of this PR is wrong on two counts.
### What the boxes actually do
Both images satisfy every precondition I reasoned about:
| | Bazzite `44.20260818.0` | Nobara (f44) |
|---|---|---|
| DM | `sddm.service` | `plasmalogin.service` |
| `Relogin=true` | yes (`/usr/lib/sddm/sddm.conf.d/holo.conf`) | yes (`/etc/plasmalogin.conf`) |
| session script | parked on `systemctl --user --wait start gamescope-session-plus@<x>.service` | same |
| unit | `@ogui-steam` | `@steam` |
### Scenarios run (all recovered cleanly)
1. **Bazzite, control on the old DM-stop build** (`0.31.0-0.ci14629.gd161c126`) — took the DM branch, `restored the display manager`, box back. Confirms the pre-0.31.0 half of the write-up.
2. **Bazzite, pre-fix idled build, 25 s session** — idle drop-in + `sleep infinity` + relogin to session 428 during; after disconnect the drop-in is gone, gamescope is back **in the same seat0 session**, `dpms=On`, `enabled=enabled`.
3. **Bazzite, immediate-disconnect race** (keep-alive `secs=0`, so the restore fires inside the relogin window) — clean.
4. **Nobara, pre-fix idled build, 25 s session** — `idled=true`, relogin to session 11, then `removed the takeover's idle drop-in` + `restored the TV's autologin gaming session`, gamescope back, `dpms=On`.
5. **Nobara, 3 rapid connect/disconnect cycles** — clean.
### Where the reasoning was wrong
* **`systemctl --user --wait start` does not return on a `restart`.** It only returns on a genuine deactivation. So the host's `restart` never ends the login session, gamescope starts *inside* the live seat0 session and gets DRM master normally. The seat-less race this PR is built on does not happen. Verified by hand on Bazzite before touching punktfunk at all.
* **The loop reaching DM boxes is deliberate, not a regression.** d9662c01 chose that verb for exactly this case — "`restart`, not `start`: the idle takeover leaves the unit ACTIVE" — and it works on both distros.
So this PR would replace a working, intentional design with a different one on an unproven theory. Converting to WIP.
### What is still worth keeping
`await_relogin_or_start`'s backstop reasoning and the `handback_verb` test are cheap, but they defend against a failure nobody has demonstrated. Parking until there is a real reproduction.
### To actually diagnose this
Needed from the two reporters:
1. `journalctl --user -u punktfunk-host --since <disconnect>` — the host logs every restore decision by name (`scheduled TV-session restore`, `removed the takeover's idle drop-in`, `restored the TV's autologin gaming session`, or the desktop-active early return). Their log will name the branch taken.
2. What "black" means: SDDM/plasmalogin greeter, no signal at all, or Game Mode simply not returning?
3. `systemctl --user status gamescope-session-plus@*.service`, `loginctl list-sessions`, and `cat /sys/class/drm/*/dpms` **while it is black**.
4. Whether a game was running, and whether the box had been switched to desktop and back during the session — the desktop-active early return at `do_restore_tv_session` is the one path that legitimately leaves the box un-restored, and it is my leading remaining suspect.
My rig differs from the field in one way worth noting: the video pipeline never delivered frames on either VM (`no PipeWire frame within 2.5s … the compositor produced no frames`), so these were takeover/teardown exercises rather than fully streaming sessions with a game loaded.
enricobuehler
changed title from The idled takeover started the box's game mode with no seat, and the panel stayed black to WIP: The idled takeover's seat-less hand-back — NOT REPRODUCED, do not merge2026-08-22 21:51:42 +00:00
Field reports on 0.31.x, Bazzite and Nobara: after disconnecting, the box's own
physical screen stays black.
I could not reproduce it (PR #375 has the full negative write-up: five scenarios
across both distro families on the real VMs, all recovering cleanly, and the
mechanism I first proposed disproved on glass). So this does not guess at the
trigger. It closes the gap that lets ANY trigger end as a dark panel, and fixes
the one black-screen path I could prove.
## The restore never checked its own work
`do_restore_tv_session` issues a lifecycle verb and logs what systemd said about
the JOB. "The job succeeded" and "the box shows a picture" are different
questions, and nothing in this file has ever asked the second one — the restore
walks away the moment the verb returns, so every way the box can end up dark
looks identical to success in the log.
So measure it. After the hand-back a detached watcher polls
`detect_active_session()`, whose `None` means no compositor of our uid is running
at all — exactly the symptom. If the box is still dark 25 s later it climbs a
ladder of remedies, each measured on both images (Bazzite 44.20260818, Nobara
f44, 2026-08-22):
1. STOP the autologin unit. Its login session's script is parked on
`systemctl --user --wait start <unit>` on both images, so a stop releases that
wait, the session exits, and `Relogin=true` logs back in — starting the unit
inside a session with a seat. `stop`, not `restart`: a restart does NOT
release the parked waiter (measured), which is why it cannot rescue a box the
ordinary restart already failed to bring back.
2. Restart the display manager — what the pre-0.31.0 takeover did on every
disconnect, and proven on the Bazzite VM to return the box to game mode.
3. `PUNKTFUNK_RECOVER_SESSION_CMD`, then an ERROR naming the command a human has
to run.
Detached, and that is load-bearing: the restore holds `RESTORE_FLIGHT`, which a
reconnecting client must take before it can re-take the box, so watching for up
to a minute while holding it would put that wait in front of every reconnect.
The watcher also stands down the instant `takeover_live()` says a new takeover
armed — the box belongs to that stream now, and a remedy fired into it would be
a fresh bug. It runs after `clear_takeover()` so that check means "a client
reconnected" and not "our own takeover has not been filed yet".
Skipped on the shutdown path: `restore_takeover_now` runs inside `native.rs`'s
20 s `SHUTDOWN_RESTORE_GRACE`, and spending that grace watching would cost the
hand-back rather than check it. What covers a shutdown that left the box dark is
the next host start — which this commit also makes true.
## A crashed host left the box's game mode asleep, provably
`restore_takeover_on_startup` sweeps a leftover idle drop-in off the box and logs
that the box's "own Game Mode session would have started and then done nothing".
Removing the FILE does not touch the unit RUNNING under it: its `ExecStart` is
still the sleep, so it sits `active` drawing nothing. Nothing below that sweep
restarts it either — the takeover file may be absent, unparseable, or fail
`takeover_state_is_live`, and all three exits leave the box on a dark panel with
its game mode "running". Any host killed mid-takeover (SIGKILL, OOM, a yanked
update) lands exactly there, and it survives until someone reboots.
`hand_back_idled_units_after_crash` restarts those units, gated on the box
actually being dark so a user already in game mode or on a desktop is never
bounced, and only for ACTIVE instances — under a just-removed idle drop-in,
active means "running the sleep".
## Not changed
The `restart` verb on the ordinary restore path. It works on both distros
(measured), and 0.31.0 chose it deliberately for the idled unit. The `stop` idea
survives only as escalation rung 1, where it runs after the proven path has
already failed.
`listed_autologin_units` is factored out of `stop_autologin_sessions` so both
callers share it, and its column parsing — which decides whether a live gaming
session can be told from a dead leftover — finally has a test against real
`--plain` output from both images.
enricobuehler
changed title from WIP: The idled takeover's seat-less hand-back — NOT REPRODUCED, do not merge to The hand-back never checked that the panel came back, and a crashed host left game mode asleep2026-08-22 23:03:15 +00:00
On-glass validation: bug reproduced and fixed on BOTH distros
Canary 0.32.0-0.ci15147.gc63e8cee (built from this branch, run 19984) against pre-fix 0.32.0-0.ci15103.g2b91339c, on home-bazzite-2 (119) and home-nobara-1 (123).
The black screen, reproduced
Construct exactly what a host killed mid-takeover leaves behind — idle drop-in in place, unit killed and restarted under it, no takeover state file — then start the host so restore_takeover_on_startup sweeps it.
Pre-fix, both boxes:
----- AFTER HOST RESTART (the sweep ran) -----
dropin: ← removed
ActiveState=active SubState=running
mainproc: /usr/bin/sleep infinity ← unit still running the placeholder
box gamescope: 0 ← nothing drawing
dpms: On ← panel powered, and blank
with only removed a leftover idle drop-in … would have started and then done nothing in the log. Game Mode reports active, the panel is lit and black, and it stays that way until a reboot. That is the reported symptom, on the shipping build, on both distro families.
23:27:47 WARN the box's Game Mode is running the dead host's idle placeholder and its panel is
dark — restarting it units=["gamescope-session-plus@ogui-steam.service"]
23:27:48 INFO the box is driving its own panel again — hand-back complete
Nobara:
23:36:03 WARN … units=["gamescope-session-plus@steam.service"]
23:36:04 INFO the box is driving its own panel again — hand-back complete
No regression on the ordinary disconnect
A real streamed session (the Bazzite run delivered 747 frames with audio and latency percentiles — not a degraded takeover-only exercise), then disconnect:
23:31:48 freed Steam: … idled=true
23:32:14 removed the takeover's idle drop-in — the box's own Game Mode runs for real again
23:32:14 restored the TV's autologin gaming session (debounce elapsed, no client)
23:32:15 the box is driving its own panel again — hand-back complete
Same on Nobara (23:37:09). The verb is unchanged, the watcher confirms the outcome in ~0.5 s, and no escalation rung fired on either box — the ladder is dormant unless the box is actually dark.
crash gap pre-fix
crash gap post-fix
disconnect post-fix
Bazzite 44.20260818
black
recovered
works, verified
Nobara f44
black
recovered
works, verified
Scope note
This fixes a black screen that is definitely real and definitely reachable on the shipping build. Whether it is the one the two users hit is not proven — their trigger was "after disconnecting", and a clean disconnect recovers fine on both boxes here (see the previous comment). What makes this worth shipping regardless is the second half: the hand-back now measures its own outcome, so any other route to a dark panel gets caught and escalated instead of being logged as success. If a report survives this, the log will now name which rung failed.
## On-glass validation: bug reproduced and fixed on BOTH distros
Canary `0.32.0-0.ci15147.gc63e8cee` (built from this branch, run 19984) against pre-fix
`0.32.0-0.ci15103.g2b91339c`, on `home-bazzite-2` (119) and `home-nobara-1` (123).
### The black screen, reproduced
Construct exactly what a host killed mid-takeover leaves behind — idle drop-in in place, unit killed and restarted under it, no takeover state file — then start the host so `restore_takeover_on_startup` sweeps it.
**Pre-fix, both boxes:**
```
----- AFTER HOST RESTART (the sweep ran) -----
dropin: ← removed
ActiveState=active SubState=running
mainproc: /usr/bin/sleep infinity ← unit still running the placeholder
box gamescope: 0 ← nothing drawing
dpms: On ← panel powered, and blank
```
with only `removed a leftover idle drop-in … would have started and then done nothing` in the log. Game Mode reports `active`, the panel is lit and black, and it stays that way until a reboot. That is the reported symptom, on the shipping build, on both distro families.
**Post-fix, both boxes** — same script, same box:
```
mainproc: /usr/bin/bash /usr/share/gamescope-session-plus/gamescope-session-plus <instance>
box gamescope: 1
```
Bazzite:
```
23:27:47 WARN the box's Game Mode is running the dead host's idle placeholder and its panel is
dark — restarting it units=["gamescope-session-plus@ogui-steam.service"]
23:27:48 INFO the box is driving its own panel again — hand-back complete
```
Nobara:
```
23:36:03 WARN … units=["gamescope-session-plus@steam.service"]
23:36:04 INFO the box is driving its own panel again — hand-back complete
```
### No regression on the ordinary disconnect
A real streamed session (the Bazzite run delivered 747 frames with audio and latency percentiles — not a degraded takeover-only exercise), then disconnect:
```
23:31:48 freed Steam: … idled=true
23:32:14 removed the takeover's idle drop-in — the box's own Game Mode runs for real again
23:32:14 restored the TV's autologin gaming session (debounce elapsed, no client)
23:32:15 the box is driving its own panel again — hand-back complete
```
Same on Nobara (`23:37:09`). The verb is unchanged, the watcher confirms the outcome in ~0.5 s, and **no escalation rung fired** on either box — the ladder is dormant unless the box is actually dark.
| | crash gap pre-fix | crash gap post-fix | disconnect post-fix |
|---|---|---|---|
| Bazzite `44.20260818` | **black** | recovered | works, verified |
| Nobara f44 | **black** | recovered | works, verified |
### Scope note
This fixes a black screen that is definitely real and definitely reachable on the shipping build. Whether it is *the* one the two users hit is not proven — their trigger was "after disconnecting", and a clean disconnect recovers fine on both boxes here (see the [previous comment](https://git.unom.io/unom/punktfunk/pulls/375#issuecomment-4320)). What makes this worth shipping regardless is the second half: the hand-back now measures its own outcome, so any *other* route to a dark panel gets caught and escalated instead of being logged as success. If a report survives this, the log will now name which rung failed.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Field reports on
0.31.x, Bazzite and Nobara: after disconnecting, the box's own physical screen stays black.I could not reproduce it. The previous comment has the full negative write-up — five scenarios across both distro families on the real VMs, all recovering cleanly, and the mechanism the first version of this PR proposed disproved on glass. The branch has been rewritten from scratch on that basis.
So this does not guess at the trigger. It closes the gap that lets any trigger end as a dark panel, and fixes the one black-screen path I could prove.
1. The restore never checked its own work
do_restore_tv_sessionissues a lifecycle verb and logs what systemd said about the job. "The job succeeded" and "the box shows a picture" are different questions, and nothing in this file has ever asked the second one — the restore walks away the moment the verb returns, so every way the box can end up dark looks identical to success in the log.So measure it. A detached watcher polls
detect_active_session(), whoseNonemeans no compositor of our uid is running at all — exactly the symptom. Still dark 25 s after the hand-back, it climbs a ladder, each rung measured on both images (Bazzite44.20260818, Nobara f44, 2026-08-22):stopthe autologin unit. Its login session's script is parked onsystemctl --user --wait start <unit>on both images, so a stop releases that wait, the session exits, andRelogin=truelogs back in — starting the unit inside a session with a seat.stop, notrestart: a restart does not release the parked waiter (measured), which is exactly why it cannot rescue a box the ordinary restart already failed to bring back.PUNKTFUNK_RECOVER_SESSION_CMD, then an ERROR naming the command a human has to run.Detached, and that is load-bearing. The restore holds
RESTORE_FLIGHT, which a reconnecting client must take before it can re-take the box; watching for up to a minute while holding it would put that wait in front of every reconnect. The watcher also stands down the instanttakeover_live()says a new takeover armed — the box belongs to that stream now, and a remedy fired into it would be a fresh bug. It runs afterclear_takeover()so that check means "a client reconnected", not "our own takeover has not been filed yet".Skipped on the shutdown path:
restore_takeover_nowruns insidenative.rs's 20 sSHUTDOWN_RESTORE_GRACE, and spending that grace watching would cost the hand-back rather than check it. What covers a shutdown that left the box dark is the next host start — which this PR also makes true.2. A crashed host left the box's game mode asleep — provably
restore_takeover_on_startupsweeps a leftover idle drop-in and logs that the box's "own Game Mode session would have started and then done nothing". Removing the file does not touch the unit running under it: itsExecStartis still the sleep, so it sitsactivedrawing nothing. Nothing below that sweep restarts it either — the takeover file may be absent, unparseable, or failtakeover_state_is_live, and all three exits leave the box on a dark panel with its game mode "running". Any host killed mid-takeover (SIGKILL, OOM, a yanked update) lands exactly there, and it survives until someone reboots.hand_back_idled_units_after_crashrestarts those units — gated on the box actually being dark, so a user already in game mode or on a desktop is never bounced, and only foractiveinstances (under a just-removed idle drop-in, active means "running the sleep").Deliberately not changed
The
restartverb on the ordinary restore path. It works on both distros (measured), and 0.31.0 chose it deliberately for the idled unit. Thestopidea from v1 of this PR survives only as escalation rung 1, where it runs after the proven path has already failed.Checks
cargo clippy -p pf-vdisplay --all-targets -- -D warnings— clean.cargo fmt— clean.cargo test -p pf-vdisplay --lib— 248 passed, 0 failed.listed_autologin_unitsfactored out ofstop_autologin_sessionsso both callers share it; its column parsing — which decides whether a live gaming session can be told from a dead leftover — finally has a test against real--plainoutput from both images.On-glass validation of both halves on the VMs follows, against a canary built from this branch.
On-glass validation: could NOT reproduce the bug. Do not merge as-is.
I drove the real path on both VMs (
home-bazzite-2119,home-nobara-1123 on home-node-3) with the headlesspunktfunk-probeagainst the pre-fix canary0.32.0-0.ci15103.g2b91339c. The reported black screen did not reproduce in any scenario, and the premise of this PR is wrong on two counts.What the boxes actually do
Both images satisfy every precondition I reasoned about:
44.20260818.0sddm.serviceplasmalogin.serviceRelogin=true/usr/lib/sddm/sddm.conf.d/holo.conf)/etc/plasmalogin.conf)systemctl --user --wait start gamescope-session-plus@<x>.service@ogui-steam@steamScenarios run (all recovered cleanly)
0.31.0-0.ci14629.gd161c126) — took the DM branch,restored the display manager, box back. Confirms the pre-0.31.0 half of the write-up.sleep infinity+ relogin to session 428 during; after disconnect the drop-in is gone, gamescope is back in the same seat0 session,dpms=On,enabled=enabled.secs=0, so the restore fires inside the relogin window) — clean.idled=true, relogin to session 11, thenremoved the takeover's idle drop-in+restored the TV's autologin gaming session, gamescope back,dpms=On.Where the reasoning was wrong
systemctl --user --wait startdoes not return on arestart. It only returns on a genuine deactivation. So the host'srestartnever ends the login session, gamescope starts inside the live seat0 session and gets DRM master normally. The seat-less race this PR is built on does not happen. Verified by hand on Bazzite before touching punktfunk at all.d9662c01chose that verb for exactly this case — "restart, notstart: the idle takeover leaves the unit ACTIVE" — and it works on both distros.So this PR would replace a working, intentional design with a different one on an unproven theory. Converting to WIP.
What is still worth keeping
await_relogin_or_start's backstop reasoning and thehandback_verbtest are cheap, but they defend against a failure nobody has demonstrated. Parking until there is a real reproduction.To actually diagnose this
Needed from the two reporters:
journalctl --user -u punktfunk-host --since <disconnect>— the host logs every restore decision by name (scheduled TV-session restore,removed the takeover's idle drop-in,restored the TV's autologin gaming session, or the desktop-active early return). Their log will name the branch taken.systemctl --user status gamescope-session-plus@*.service,loginctl list-sessions, andcat /sys/class/drm/*/dpmswhile it is black.do_restore_tv_sessionis the one path that legitimately leaves the box un-restored, and it is my leading remaining suspect.My rig differs from the field in one way worth noting: the video pipeline never delivered frames on either VM (
no PipeWire frame within 2.5s … the compositor produced no frames), so these were takeover/teardown exercises rather than fully streaming sessions with a game loaded.The idled takeover started the box's game mode with no seat, and the panel stayed blackto WIP: The idled takeover's seat-less hand-back — NOT REPRODUCED, do not mergea8a054f2b0toc63e8cee39WIP: The idled takeover's seat-less hand-back — NOT REPRODUCED, do not mergeto The hand-back never checked that the panel came back, and a crashed host left game mode asleepOn-glass validation: bug reproduced and fixed on BOTH distros
Canary
0.32.0-0.ci15147.gc63e8cee(built from this branch, run 19984) against pre-fix0.32.0-0.ci15103.g2b91339c, onhome-bazzite-2(119) andhome-nobara-1(123).The black screen, reproduced
Construct exactly what a host killed mid-takeover leaves behind — idle drop-in in place, unit killed and restarted under it, no takeover state file — then start the host so
restore_takeover_on_startupsweeps it.Pre-fix, both boxes:
with only
removed a leftover idle drop-in … would have started and then done nothingin the log. Game Mode reportsactive, the panel is lit and black, and it stays that way until a reboot. That is the reported symptom, on the shipping build, on both distro families.Post-fix, both boxes — same script, same box:
Bazzite:
Nobara:
No regression on the ordinary disconnect
A real streamed session (the Bazzite run delivered 747 frames with audio and latency percentiles — not a degraded takeover-only exercise), then disconnect:
Same on Nobara (
23:37:09). The verb is unchanged, the watcher confirms the outcome in ~0.5 s, and no escalation rung fired on either box — the ladder is dormant unless the box is actually dark.44.20260818Scope note
This fixes a black screen that is definitely real and definitely reachable on the shipping build. Whether it is the one the two users hit is not proven — their trigger was "after disconnecting", and a clean disconnect recovers fine on both boxes here (see the previous comment). What makes this worth shipping regardless is the second half: the hand-back now measures its own outcome, so any other route to a dark panel gets caught and escalated instead of being logged as success. If a report survives this, the log will now name which rung failed.