fix(windows-host): cross-plane IDD serialization, linger-expiry race, second-host guard

Batch C of the audit's medium tier (M7+M8+M9):

- M7: GameStream sessions now run the same begin_idd_setup dance as
  punktfunk/1 before creating the shared monitor. A GS connect could
  previously ADD/reconfigure the monitor while a native session was
  mid-build (and vice versa), and its sealed-channel delivery replaced the
  native ring (newest-wins) — each plane could freeze the other. GS has no
  cooperative stop plumbing, so it registers a flag nobody reads: a later
  session signals it, waits the 3 s grace, then force-preempts — the
  intended handover.
- M8: the linger-expiry teardown now runs UNDER the state lock. Running it
  outside let a concurrent acquire see Idle and ADD+isolate while the old
  monitor's pinger-join / CCD-restore / REMOVE were still in flight — a
  failed or de-isolated session exactly at the expiry boundary. A racing
  acquire now waits the few teardown seconds instead. Lock order stays
  state → device everywhere; the pinger takes only the device lock.
- M9: a named mutex (Global\punktfunk-vdisplay-manager) makes a SECOND host
  process fail its vdisplay open loudly instead of firing a startup
  CLEAR_ALL that razes the live host's monitors mid-stream (the admin
  footgun the shared watchdog then masked).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 17:28:22 +00:00
parent b46aa15afb
commit fe54aff658
2 changed files with 79 additions and 30 deletions
@@ -269,6 +269,23 @@ fn open_gs_virtual_source(
));
#[cfg(not(target_os = "linux"))]
vd.set_launch_command(app.and_then(|a| a.cmd.clone()));
// Serialize with the punktfunk/1 plane's IDD-push setup dance (Goal-1 §2.5). A GameStream
// connect used to skip it entirely, so it could ADD/reconfigure the shared monitor while a
// native session was mid-build (and vice versa), and its sealed-channel delivery would replace
// the native session's ring (newest-wins) — each plane could freeze the other. GameStream has
// no cooperative stop-flag plumbing, so it registers a flag nobody reads: a LATER session that
// preempts this one signals it, waits the 3 s release grace, then force-preempts the monitor —
// this session then fails on capture and tears down cleanly (the intended handover).
#[cfg(target_os = "windows")]
let _idd_setup_guard = matches!(
crate::session_plan::CaptureBackend::resolve(),
crate::session_plan::CaptureBackend::IddPush
)
.then(|| {
crate::vdisplay::manager::vdm().begin_idd_setup(std::sync::Arc::new(
std::sync::atomic::AtomicBool::new(false),
))
});
let vout = vd
.create(punktfunk_core::Mode {
width: cfg.width,