fix(host): a leftover Sunshine folder is not a conflict, and a crashed host gives the screen back #52

Merged
enricobuehler merged 1 commits from worktree-conflict-detect-and-isolate-recovery into main 2026-08-04 21:00:59 +00:00
Owner

Three things a Discord field report (Windows host, 0.1x → canary) turned up. Two are fixed here; the third is diagnosed in the notes below but not fixed.

1. "It thinks I have Sunshine/Apollo running" — it didn't

Both were uninstalled. Sunshine's and Apollo's uninstallers leave their config/ and logs/ directories in Program Files behind, and detect.rs counted a bare directory — or a service registered at any start type, including disabled — as a live conflict.

The installer's own probe was narrowed to "service start type <= 2" after exactly this aborted a winget install in the field, and the tray dropped its always-on warning for the same reason in 3e782852. The runtime probe never got the same treatment — so the one surface a user actually looks at kept shouting, and reinstalling Punktfunk could never clear it, because the leftover is not our state.

Evidence::is_active now draws the line (running, or set to start on its own). Only active detections reach the startup warning, the detect-conflicts exit code, and /local/summary. Dormant findings still appear in the full report, under a heading saying they need no action — that report is where "why does it think I have Apollo?" gets answered.

  • Windows reads the real SCM start type via query_config; unreadable ⇒ reported dormant (a genuinely running host is caught by the process scan anyway).
  • Linux checks for a .wants/.requires symlink, not merely a unit file — an installed-but-not-enabled unit starts nothing.
  • detect-conflicts now exits 1 only for an active conflict, so installers/support scripts gating on it stop aborting over leftovers.

2. The console card claimed "running" regardless of evidence

host_conflicts_title was hardcoded to "Another game-streaming server is running on this machine", so a leftover folder was announced as a running server. It now says "active", and each entry names what was actually observed — Sunshine (running), Apollo (starts automatically).

3. "The exclusive screen never times out re-enabling the display"

restore_displays_ccd runs from exactly one place — teardown_removed, gated on the in-memory ccd_saved. Windows deliberately never saves the isolated topology to the CCD database, precisely so teardown can restore the user's layout. So a host that crashed, was killed, or was stopped mid-session left the operator's panels deactivated with nothing in the product to put them back.

There was already a startup recovery leg, but only for the experimental pnp_disable_monitors axis, which is false by default — the default Exclusive path had no recovery at all.

New isolate_journal:

  • marks the deactivated targets before the apply (dying mid-apply is inside the window this exists to cover),
  • clears after the restore — restore_displays_ccd wraps an _inner so a crash part-way through keeps the marker,
  • startup_recover() force-EXTENDs at host start if a marker survived.

EXTEND rather than replaying the saved CCD blob on purpose: that blob pins the virtual display's target id, which dies with the crashed host, so a replay would mostly fail ERROR_BAD_CONFIGURATION into the very backstop restore_displays_ccd already keeps — and EXTEND stays correct across a reboot, where saved ids are stale anyway.

Verification

Green on three boxes, each with an on-box proof that the machine compiled these bytes (not a stale tree):

Linux .21 Windows .133 Windows .173
cargo fmt --all --check 0 0 0
clippy -D warnings 0 (--workspace --all-targets --locked) 0 0
detect tests 15/15 15/15 15/15
isolate_journal tests — (Windows-only) 4/4

isolate_journal carries 4 unit tests: mark→pending()→clear round-trip, empty set writes no marker, an unchanged re-mark does not rewrite (asserted with a sentinel rather than mtimes, which would pass vacuously at coarse filesystem timestamp resolution), and a corrupt marker still requests recovery. They are possible because pf_paths::config_dir() honors PUNKTFUNK_CONFIG_DIR, and because startup_recover was split into pending() (the decision) plus the force-EXTEND — so no test drives a real SetDisplayConfig.

Not fixed here, worth knowing

  • The gaming-rig preset is KeepAlive::Forever + Topology::Exclusive, so on session end the display is Pinned, teardown never runs, and the physical panel stays dark indefinitely — by design, and literally the reporter's "never times out". Escape hatch is POST /api/v1/display/release. Needs a UX decision, not a code fix.
  • Not proven on glass. The isolate → crash → restart → force-EXTEND cycle was never exercised against a real panel (no Windows box with a lit display was available). Everything above is compile/lint/unit level.
  • The dark-desk backstop in restore_displays_ccd only counts external_physical, so a laptop internal panel left dark by a failed in-process restore is still not re-lit. The new startup recovery covers the crash case, not that one.
  • The reporter's blank screen is not root-caused by this PR. PR #37 and #41 are both merged, so a canary user already carries both wire legs; remaining suspects are a host path MTU below the learned floor, a leftover SudoVDA driver from the uninstalled Apollo fighting the isolate, or IddCx slot exhaustion (0x80070490).
Three things a Discord field report (Windows host, 0.1x → canary) turned up. Two are fixed here; the third is diagnosed in the notes below but not fixed. ## 1. "It thinks I have Sunshine/Apollo running" — it didn't Both were uninstalled. Sunshine's and Apollo's uninstallers leave their `config/` and `logs/` directories in Program Files behind, and `detect.rs` counted a bare directory — or a service registered at **any** start type, including `disabled` — as a live conflict. The installer's own probe was narrowed to "service start type <= 2" after exactly this aborted a `winget install` in the field, and the tray dropped its always-on warning for the same reason in `3e782852`. The runtime probe never got the same treatment — so the one surface a user actually looks at kept shouting, and **reinstalling Punktfunk could never clear it**, because the leftover is not our state. `Evidence::is_active` now draws the line (running, or set to start on its own). Only active detections reach the startup warning, the `detect-conflicts` exit code, and `/local/summary`. Dormant findings still appear in the full report, under a heading saying they need no action — that report is where "why does it think I have Apollo?" gets answered. - Windows reads the real SCM start type via `query_config`; unreadable ⇒ reported dormant (a genuinely running host is caught by the process scan anyway). - Linux checks for a `.wants`/`.requires` **symlink**, not merely a unit file — an installed-but-not-enabled unit starts nothing. - `detect-conflicts` now exits 1 only for an active conflict, so installers/support scripts gating on it stop aborting over leftovers. ## 2. The console card claimed "running" regardless of evidence `host_conflicts_title` was hardcoded to "Another game-streaming server is **running** on this machine", so a leftover folder was announced as a running server. It now says "active", and each entry names what was actually observed — `Sunshine (running)`, `Apollo (starts automatically)`. ## 3. "The exclusive screen never times out re-enabling the display" `restore_displays_ccd` runs from exactly one place — `teardown_removed`, gated on the **in-memory** `ccd_saved`. Windows deliberately never saves the isolated topology to the CCD database, precisely so teardown can restore the user's layout. So a host that crashed, was killed, or was stopped mid-session left the operator's panels deactivated with nothing in the product to put them back. There was already a startup recovery leg, but only for the **experimental** `pnp_disable_monitors` axis, which is `false` by default — the default Exclusive path had no recovery at all. New `isolate_journal`: - **marks** the deactivated targets *before* the apply (dying mid-apply is inside the window this exists to cover), - **clears** after the restore — `restore_displays_ccd` wraps an `_inner` so a crash part-way through keeps the marker, - **`startup_recover()`** force-EXTENDs at host start if a marker survived. EXTEND rather than replaying the saved CCD blob on purpose: that blob pins the *virtual* display's target id, which dies with the crashed host, so a replay would mostly fail `ERROR_BAD_CONFIGURATION` into the very backstop `restore_displays_ccd` already keeps — and EXTEND stays correct across a reboot, where saved ids are stale anyway. ## Verification Green on three boxes, each with an on-box proof that the machine compiled these bytes (not a stale tree): | | Linux `.21` | Windows `.133` | Windows `.173` | |---|---|---|---| | `cargo fmt --all --check` | 0 | 0 | 0 | | clippy `-D warnings` | 0 (`--workspace --all-targets --locked`) | 0 | 0 | | detect tests | 15/15 | 15/15 | 15/15 | | `isolate_journal` tests | — (Windows-only) | — | **4/4** | `isolate_journal` carries 4 unit tests: mark→`pending()`→clear round-trip, empty set writes no marker, an unchanged re-mark does not rewrite (asserted with a sentinel rather than mtimes, which would pass vacuously at coarse filesystem timestamp resolution), and a corrupt marker still requests recovery. They are possible because `pf_paths::config_dir()` honors `PUNKTFUNK_CONFIG_DIR`, and because `startup_recover` was split into `pending()` (the decision) plus the force-EXTEND — so no test drives a real `SetDisplayConfig`. ## Not fixed here, worth knowing - **The `gaming-rig` preset is `KeepAlive::Forever` + `Topology::Exclusive`**, so on session end the display is *Pinned*, teardown never runs, and the physical panel stays dark indefinitely — by design, and literally the reporter's "never times out". Escape hatch is `POST /api/v1/display/release`. Needs a UX decision, not a code fix. - **Not proven on glass.** The isolate → crash → restart → force-EXTEND cycle was never exercised against a real panel (no Windows box with a lit display was available). Everything above is compile/lint/unit level. - The dark-desk backstop in `restore_displays_ccd` only counts `external_physical`, so a **laptop internal panel** left dark by a *failed in-process restore* is still not re-lit. The new startup recovery covers the crash case, not that one. - The reporter's **blank screen is not root-caused** by this PR. PR #37 and #41 are both merged, so a canary user already carries both wire legs; remaining suspects are a host path MTU below the learned floor, a leftover SudoVDA driver from the uninstalled Apollo fighting the isolate, or IddCx slot exhaustion (`0x80070490`).
enricobuehler added 1 commit 2026-08-04 20:54:02 +00:00
fix(host): a leftover Sunshine folder is not a conflict, and a crashed host gives the screen back
ci / rust-arm64 (pull_request) Successful in 1m24s
apple / swift (pull_request) Successful in 1m20s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m12s
android / android (pull_request) Successful in 2m56s
ci / web (pull_request) Successful in 2m0s
ci / rust (pull_request) Successful in 9m53s
b31495bea5
Three things a field report (Discord, upgrade from 0.1x) turned up, all on the
Windows host.

1. "It thinks I have Sunshine/Apollo running." It didn't — they were uninstalled.
   Both uninstallers leave their config/log directory in Program Files behind, and
   `detect.rs` counted a bare directory, or a service registered at ANY start type
   (including `disabled`), as a live conflict. The installer's own probe was
   narrowed to "service start type <= 2" after exactly this cried wolf on a
   `winget install`, and the tray dropped its always-on warning for the same reason
   in 3e782852 — the runtime probe never got the same treatment, so the one surface
   the user actually looks at kept shouting. `Evidence::is_active` now draws the
   line (running, or set to start on its own) and only active detections reach the
   startup warning, the `detect-conflicts` exit code, and `/local/summary`. Dormant
   findings still print in the full report, under a heading that says they need no
   action — that report is where "why does it think I have Apollo?" gets answered.

2. The console's conflicts card hardcoded "Another game-streaming server is
   **running** on this machine" regardless of what was found, so a dormant leftover
   was announced as a running server. It now says "active", and each entry names
   the observation — `Sunshine (running)`, `Apollo (starts automatically)`.

3. "The exclusive screen never times out going back to re-enabling the display."
   `isolate_displays_ccd` deactivates the operator's panels and hands the
   pre-isolate topology to the caller, which restores it at teardown — but that
   snapshot is PROCESS MEMORY, and Windows deliberately never saves the isolated
   topology to the CCD database. So a host that crashed, was killed, or was stopped
   mid-session left the desk dark with nothing in the product to undo it. There was
   one startup recovery leg already, but only for the EXPERIMENTAL
   `pnp_disable_monitors` axis, which is off by default — the default Exclusive path
   had none. `isolate_journal` now marks what an isolate is about to switch off
   (before the apply, so dying mid-apply is covered), clears the mark on restore,
   and force-EXTENDs at host startup if a mark survived. EXTEND rather than
   replaying the saved blob: the blob pins the virtual display's target id, which
   dies with the crashed host, so a replay would mostly fail BAD_CONFIGURATION into
   the very same backstop `restore_displays_ccd` already keeps — and EXTEND stays
   correct across a reboot, where saved ids would be stale.
enricobuehler merged commit 4af8b02be1 into main 2026-08-04 21:00:59 +00:00
enricobuehler deleted branch worktree-conflict-detect-and-isolate-recovery 2026-08-04 21:01: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#52