Problem
When a stream ends and display_helper_integration::revert() cannot reach the display helper (the helper can't be started, or the REVERT write fails), it logs Display helper unavailable; cannot send revert. and gives up. The streaming layout then stays in place until something else changes it, usually the next stream.
This happens in practice. On 2026-10-30 a stream on my host ended while the cached helper pipe was dead:
Error: WriteFile failed (233) in WinPipe::send
Error: WriteFile failed (233) in WinPipe::send
Warning: Display helper process ping failed; keeping existing instance and deferring restart.
Info: Display helper unavailable; ...The same helper process answered normally a few seconds later. A new stream started 7 s afterwards, so in that case the stuck layout was hidden. Without that new stream, the REVERT would simply have been lost.
Fix
revert()now remembers a REVERT that never reached the helper, together with its arguments: golden preference, ownership override,allow_disabled_recovery,RevertOwnershipPolicyandFinalRestoreDisplayPolicy. The replay therefore restores exactly what the original call asked for, including removing the virtual display after the restore when that was requested.- The watchdog, which already keeps restarting the helper, replays the REVERT once the helper responds. The replay waits until no stream or launch is active or pending. It also waits until no app is running, unless an app was already running (paused) when the REVERT was issued, as with the paused-game restore or revert-on-disconnect. It holds the stream lifecycle gate (try-lock only) so a launch cannot APPLY between that check and the REVERT.
- A new APPLY or a successful REVERT cancels the pending replay.
streaming_will_stop()keeps the watchdog alive while a replay is pending. After a successful replay the watchdog stops itself (no stream, no app).
The remember-and-replay decision lives in src/platform/windows/failed_revert_policy.h, so it can be unit-tested without the integration layer.
Testing
-
New
test_component_failed_revert_policy(6 tests): the replay uses the original arguments once; a stream or launch keeps it pending; a running app blocks an ended-stream restore; a restore issued while paused replays with the app still paused; an APPLY clears it; a later failure replaces it. -
Builds on canary 399a420e (MinGW UCRT64).
-
Host test with fault injection: a test-only patch made the next REVERT act as "helper unavailable". I streamed a Playnite game with Moonlight and closed the game:
10:42:10.094 Display restore: final stream ended; dispatching restore while keeping virtual display alive. 10:42:10.095 Display helper unavailable; REVERT will be retried once the helper responds. 10:42:10.095 Display helper: keeping watchdog alive to retry a REVERT the helper missed. 10:42:14.198 Display helper: helper is reachable again; retrying the REVERT it missed. 10:42:14.198 Display helper: sending REVERT request (prefer golden if current missing). 10:42:14.209 Display helper: REVERT dispatch result=trueThe helper then logged
recovery validation succeeded, display settings restored.The host test ran on canary a8ef56b0 rather than 399a420e. On 399a420e the display helper never connects on my host: the helper logs
CreateFileW failed (5)on the new private pipe, which may be the same problem as #702. I'll report that separately.
Notes
- The end-stream hotkey path sends its own cleanup REVERT a few seconds after the final-stream one. That second REVERT already covered the miss there, and it also cancels the pending replay.
- After a successful replay, the watchdog stopping itself logs the existing warning
watchdog requested its own stop; deferring join to an external caller.This is the same pathproc::running()re-entry already uses.
🤖 Generated with Claude Code