feat(host,web): experimental PnP monitor-devnode disable for Exclusive sessions
ci / web (push) Successful in 1m0s
ci / docs-site (push) Successful in 1m6s
apple / swift (push) Successful in 1m9s
decky / build-publish (push) Successful in 22s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 47s
windows-host / package (push) Failing after 3m40s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m54s
android / android (push) Has been cancelled
apple / screenshots (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m21s
flatpak / build-publish (push) Failing after 2m26s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 1m2s
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
release / apple (push) Failing after 19m24s
ci / web (push) Successful in 1m0s
ci / docs-site (push) Successful in 1m6s
apple / swift (push) Successful in 1m9s
decky / build-publish (push) Successful in 22s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 47s
windows-host / package (push) Failing after 3m40s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m54s
android / android (push) Has been cancelled
apple / screenshots (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m21s
flatpak / build-publish (push) Failing after 2m26s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 1m2s
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
release / apple (push) Failing after 19m24s
Second experiment against the connected-but-dark-head stutter (field-proven on the reporter's box: unplugging his standby HDMI TV removes a metronomic ~4 s double-jolt; DDC/CI is a dead end for TVs — measured on the lab LG, VCP 0xD6 gets no I2C ACK). An Exclusive isolate only removes physical monitors from the CCD topology; their PnP devnodes stay live, so every standby wake (auto input scan, Instant-On HPD cycling) still triggers the full Windows reaction: PnP arrival/removal, CCD re-evaluation, DWM invalidation — the suspected hiccup mechanism (Apollo #368's Device-Manager-refresh signature). - New `pnp_disable_monitors` display-policy axis (default off): orthogonal to presets like game_session/ddc_power_off, surfaced in GET/PUT /display/settings + the enforced list, carried through the layout transform. - windows/monitor_devnode.rs: after the isolate takes, disable exactly the deactivated monitors' devnodes — CCD target → monitor device path (DISPLAYCONFIG_TARGET_DEVICE_NAME) → PnP instance id → CM_Disable_DevNode with CM_DISABLE_PERSIST, so a hot-plug RE-ARRIVAL stays disabled (that persistence is the whole point). Teardown re-enables BEFORE the CCD restore (+300 ms re-arrival settle) so restored paths have their monitors back. Precise selection — co-installed third-party virtual displays are never touched. - Crash safety: instance ids journal to <config>/pnp-disabled-monitors.json before disabling; serve startup re-enables leftovers from a crashed host. Worst case is documented in the console help (Device Manager re-enable). - Web console: second Experimental-badged toggle (the DDC block refactored into a shared ExperimentalToggle), EN/DE strings, preset-switch carry. Verified: Linux 263 tests + clippy + fmt clean; Windows (RTX box) 220/220 + clippy clean; web tsc + production build clean; openapi.json regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,9 @@ mod log_capture;
|
||||
mod metronome;
|
||||
mod mgmt;
|
||||
mod mgmt_token;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "windows/monitor_devnode.rs"]
|
||||
mod monitor_devnode;
|
||||
mod native_pairing;
|
||||
mod pipeline;
|
||||
mod punktfunk1;
|
||||
@@ -199,6 +202,11 @@ fn real_main() -> Result<()> {
|
||||
// driver to a stray second host started while the service sat idle.
|
||||
#[cfg(target_os = "windows")]
|
||||
vdisplay::manager::claim_instance_eagerly();
|
||||
// Crash recovery for the experimental `pnp_disable_monitors` axis: re-enable any
|
||||
// monitor devnodes a previous host disabled for an Exclusive session and never
|
||||
// restored (crash/kill/power loss) — before any new session touches the topology.
|
||||
#[cfg(target_os = "windows")]
|
||||
monitor_devnode::startup_recover();
|
||||
gamestream::serve(mgmt_opts, native, gamestream)
|
||||
}
|
||||
// Print the management API's OpenAPI document (for client codegen).
|
||||
|
||||
@@ -1054,6 +1054,7 @@ fn display_settings_state() -> DisplaySettingsState {
|
||||
// EXPERIMENTAL, Windows-only in effect: acted on at the `exclusive` isolate
|
||||
// (`vdisplay/windows/manager.rs`); stored-but-inert elsewhere.
|
||||
"ddc_power_off".into(),
|
||||
"pnp_disable_monitors".into(),
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -1263,6 +1264,7 @@ async fn set_display_layout(ApiJson(req): ApiJson<DisplayLayoutRequest>) -> Resp
|
||||
req.positions,
|
||||
store.game_session(),
|
||||
store.ddc_power_off(),
|
||||
store.pnp_disable_monitors(),
|
||||
);
|
||||
if let Err(e) = store.set(policy) {
|
||||
return api_error(
|
||||
@@ -2948,8 +2950,9 @@ mod tests {
|
||||
assert!(enforced.contains(&"mode_conflict"));
|
||||
assert!(enforced.contains(&"identity"));
|
||||
assert!(enforced.contains(&"layout"));
|
||||
// The experimental DDC/CI panel-off axis is acted on (Windows exclusive-isolate path).
|
||||
// The experimental DDC/CI + PnP-disable axes are acted on (Windows exclusive-isolate path).
|
||||
assert!(enforced.contains(&"ddc_power_off"));
|
||||
assert!(enforced.contains(&"pnp_disable_monitors"));
|
||||
}
|
||||
|
||||
/// The display state/release endpoints are wired + auth-gated. On the test host no backend has
|
||||
|
||||
@@ -234,6 +234,15 @@ pub struct DisplayPolicy {
|
||||
/// untouched.
|
||||
#[serde(default)]
|
||||
pub ddc_power_off: bool,
|
||||
/// EXPERIMENTAL (Windows): after an `Exclusive` isolate deactivates the physical monitors,
|
||||
/// additionally DISABLE their PnP device nodes (persistently, so a standby monitor/TV whose
|
||||
/// hot-plug events re-arrive stays disabled) and re-enable them at restore. Targets the same
|
||||
/// "connected-but-dark head" periodic-stutter class as [`Self::ddc_power_off`], but at the
|
||||
/// Windows-reaction level: a disabled devnode's wake events trigger no PnP arrival, no CCD
|
||||
/// re-evaluation, no DWM invalidation. A crash-recovery journal re-enables leftovers on host
|
||||
/// startup. Orthogonal to `preset` (like `game_session`); `#[serde(default)]` = off.
|
||||
#[serde(default)]
|
||||
pub pnp_disable_monitors: bool,
|
||||
}
|
||||
|
||||
fn one() -> u32 {
|
||||
@@ -258,6 +267,7 @@ impl Default for DisplayPolicy {
|
||||
max_displays: 4,
|
||||
game_session: GameSession::default(),
|
||||
ddc_power_off: false,
|
||||
pnp_disable_monitors: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,6 +328,7 @@ impl EffectivePolicy {
|
||||
positions: BTreeMap<String, Position>,
|
||||
game_session: GameSession,
|
||||
ddc_power_off: bool,
|
||||
pnp_disable_monitors: bool,
|
||||
) -> DisplayPolicy {
|
||||
DisplayPolicy {
|
||||
version: 1,
|
||||
@@ -334,6 +345,7 @@ impl EffectivePolicy {
|
||||
// Preserve the orthogonal axes (EffectivePolicy doesn't carry them).
|
||||
game_session,
|
||||
ddc_power_off,
|
||||
pnp_disable_monitors,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,6 +466,13 @@ impl DisplayPolicyStore {
|
||||
self.get().ddc_power_off
|
||||
}
|
||||
|
||||
/// The experimental PnP monitor-devnode-disable axis — orthogonal to the preset (like
|
||||
/// [`Self::game_session`]), read directly off the stored policy (default off when
|
||||
/// unconfigured).
|
||||
pub fn pnp_disable_monitors(&self) -> bool {
|
||||
self.get().pnp_disable_monitors
|
||||
}
|
||||
|
||||
/// Persist + adopt a new policy (sanitized first). The in-memory value changes only if the disk
|
||||
/// write succeeds, so a full disk can't leave memory and file disagreeing.
|
||||
pub fn set(&self, policy: DisplayPolicy) -> Result<()> {
|
||||
@@ -769,10 +788,12 @@ mod tests {
|
||||
let mut positions = BTreeMap::new();
|
||||
positions.insert("1".to_string(), Position { x: 0, y: 0 });
|
||||
positions.insert("7".to_string(), Position { x: 2560, y: 0 });
|
||||
let p = eff.with_manual_layout(positions, GameSession::Dedicated, true);
|
||||
// The orthogonal axes (game-session, DDC power-off) are preserved through the transform.
|
||||
let p = eff.with_manual_layout(positions, GameSession::Dedicated, true, true);
|
||||
// The orthogonal axes (game-session, DDC power-off, PnP disable) are preserved through
|
||||
// the transform.
|
||||
assert_eq!(p.game_session, GameSession::Dedicated);
|
||||
assert!(p.ddc_power_off);
|
||||
assert!(p.pnp_disable_monitors);
|
||||
// Preset drops to Custom so the explicit fields (incl. the layout) rule…
|
||||
assert_eq!(p.preset, Preset::Custom);
|
||||
// …every other behavior axis is preserved verbatim…
|
||||
@@ -797,8 +818,9 @@ mod tests {
|
||||
assert_eq!(p.keep_alive, KeepAlive::default());
|
||||
assert_eq!(p.topology, Topology::Auto);
|
||||
assert_eq!(p.version, 1);
|
||||
// A file written before the experimental DDC axis existed defaults it OFF.
|
||||
// A file written before the experimental DDC/PnP axes existed defaults them OFF.
|
||||
assert!(!p.ddc_power_off);
|
||||
assert!(!p.pnp_disable_monitors);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -121,6 +121,9 @@ struct Monitor {
|
||||
/// How many physical panels acknowledged the EXPERIMENTAL DDC/CI off command at this monitor's
|
||||
/// isolate (`ddc_power_off` policy axis) — teardown wakes them after the CCD restore iff > 0.
|
||||
ddc_panels_off: u32,
|
||||
/// PnP instance ids of monitor devnodes the EXPERIMENTAL `pnp_disable_monitors` axis disabled
|
||||
/// at this monitor's isolate — teardown re-enables them BEFORE the CCD restore.
|
||||
pnp_disabled: Vec<String>,
|
||||
/// Generation stamp; a [`MonitorLease`] only releases if its gen still matches (stale-lease no-op).
|
||||
gen: u64,
|
||||
}
|
||||
@@ -672,6 +675,7 @@ impl VirtualDisplayManager {
|
||||
}
|
||||
let mut ccd_saved: Option<SavedConfig> = None;
|
||||
let mut ddc_panels_off = 0u32;
|
||||
let mut pnp_disabled: Vec<String> = Vec::new();
|
||||
match &gdi_name {
|
||||
Some(n) => {
|
||||
tracing::info!(backend = self.driver.name(), "target {} -> {n}", added.target_id);
|
||||
@@ -699,6 +703,19 @@ impl VirtualDisplayManager {
|
||||
// `Copy` target id by value and returns an owned `SavedConfig` (no borrowed memory
|
||||
// crosses), under the `state` lock — the sole topology mutator.
|
||||
ccd_saved = unsafe { isolate_displays_ccd(added.target_id) };
|
||||
// EXPERIMENTAL `pnp_disable_monitors` policy axis: AFTER the isolate took,
|
||||
// additionally disable the deactivated monitors' PnP devnodes (persistent
|
||||
// across hot-plug re-arrival) so a standby monitor/TV's periodic wake
|
||||
// events no longer trigger the Windows reaction cascade — the suspected
|
||||
// hiccup mechanism (rationale + crash journal in `windows/monitor_devnode.rs`).
|
||||
if crate::vdisplay::policy::prefs().pnp_disable_monitors() {
|
||||
if let Some(saved) = &ccd_saved {
|
||||
pnp_disabled = crate::monitor_devnode::disable_for_deactivated(
|
||||
saved,
|
||||
added.target_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Topology::Primary => {
|
||||
// On a headless box the IDD auto-activates as the SOLE display, so a physical
|
||||
@@ -757,6 +774,7 @@ impl VirtualDisplayManager {
|
||||
pinger: Some(pinger),
|
||||
ccd_saved,
|
||||
ddc_panels_off,
|
||||
pnp_disabled,
|
||||
gen: self.gen.fetch_add(1, Ordering::Relaxed),
|
||||
})
|
||||
}
|
||||
@@ -818,6 +836,13 @@ impl VirtualDisplayManager {
|
||||
}
|
||||
// Re-attach detached display(s) BEFORE the REMOVE so the box is never left with zero displays.
|
||||
if let Some(saved) = &mon.ccd_saved {
|
||||
// EXPERIMENTAL `pnp_disable_monitors` restore: re-enable the devnodes FIRST and let
|
||||
// them re-arrive, so the CCD restore below re-activates paths whose monitors exist
|
||||
// again (a disabled devnode would leave the restored path modeless/EDID-less).
|
||||
if !mon.pnp_disabled.is_empty() {
|
||||
crate::monitor_devnode::enable_instances(&mon.pnp_disabled);
|
||||
thread::sleep(Duration::from_millis(300));
|
||||
}
|
||||
restore_displays_ccd(saved);
|
||||
// EXPERIMENTAL `ddc_power_off` wake: the restore re-activated the physical paths, and
|
||||
// returning signal alone wakes DPMS-off panels on most firmware — the explicit ON is
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
//! PnP monitor-devnode disable — the EXPERIMENTAL `pnp_disable_monitors` display-policy axis.
|
||||
//!
|
||||
//! An `Exclusive` isolate removes the physical monitors from the desktop TOPOLOGY (CCD), but their
|
||||
//! PnP device nodes stay live — so a standby monitor/TV that periodically wakes its connection
|
||||
//! (auto input scan, Instant-On HPD cycling, DP link events) still triggers the full Windows
|
||||
//! reaction each time: PnP arrival/removal, CCD re-evaluation, DWM invalidation. Field evidence
|
||||
//! (Apollo #368's Device-Manager refresh at every hitch; our own reporter's TV where unplugging the
|
||||
//! cable removes a metronomic ~4 s double-jolt) says this reaction cascade is the expensive part.
|
||||
//!
|
||||
//! This module disables the deactivated monitors' devnodes for the stream's duration
|
||||
//! (`CM_Disable_DevNode` with `CM_DISABLE_PERSIST`, so a devnode that hot-plug re-arrives STAYS
|
||||
//! disabled — that persistence is the whole point) and re-enables them at teardown before the CCD
|
||||
//! restore. Selection is precise: only the monitors on targets the isolate actually deactivated,
|
||||
//! mapped CCD target → monitor device interface path (`DISPLAYCONFIG_TARGET_DEVICE_NAME`) → PnP
|
||||
//! instance id — never "every monitor but ours", so co-installed third-party virtual displays are
|
||||
//! untouched.
|
||||
//!
|
||||
//! Crash safety: instance ids are journaled to `<config>/pnp-disabled-monitors.json` BEFORE the
|
||||
//! disable and cleared after a successful re-enable; [`startup_recover`] re-enables leftovers when
|
||||
//! the host starts after a crash/kill. Worst case (host dies AND never restarts) the monitor stays
|
||||
//! disabled until the user re-enables it in Device Manager — the web-console help text says so.
|
||||
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::Devices::DeviceAndDriverInstallation::{
|
||||
CM_Disable_DevNode, CM_Enable_DevNode, CM_Locate_DevNodeW, CM_DISABLE_PERSIST,
|
||||
CM_LOCATE_DEVNODE_NORMAL, CM_LOCATE_DEVNODE_PHANTOM, CR_SUCCESS,
|
||||
};
|
||||
use windows::Win32::Devices::Display::{
|
||||
DisplayConfigGetDeviceInfo, DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME,
|
||||
DISPLAYCONFIG_TARGET_DEVICE_NAME,
|
||||
};
|
||||
use windows::Win32::Foundation::LUID;
|
||||
|
||||
/// The crash-recovery journal: PnP instance ids we disabled and have not yet re-enabled.
|
||||
fn journal_path() -> std::path::PathBuf {
|
||||
crate::gamestream::config_dir().join("pnp-disabled-monitors.json")
|
||||
}
|
||||
|
||||
fn read_journal() -> Vec<String> {
|
||||
match std::fs::read(journal_path()) {
|
||||
Ok(bytes) => serde_json::from_slice(&bytes).unwrap_or_default(),
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist `ids` as the outstanding-disable set (union semantics handled by the callers). Failure
|
||||
/// is logged, not fatal — the feature degrades to "no crash journal", not "no feature".
|
||||
fn write_journal(ids: &[String]) {
|
||||
let path = journal_path();
|
||||
if ids.is_empty() {
|
||||
let _ = std::fs::remove_file(&path);
|
||||
return;
|
||||
}
|
||||
if let Some(dir) = path.parent() {
|
||||
let _ = crate::gamestream::create_private_dir(dir);
|
||||
}
|
||||
if let Err(e) = std::fs::write(&path, serde_json::to_vec_pretty(&ids).unwrap_or_default()) {
|
||||
tracing::warn!(error = %e, "PnP-disable: could not write the crash-recovery journal");
|
||||
}
|
||||
}
|
||||
|
||||
/// `\\?\DISPLAY#GSM83CD#5&367fb4cb&0&UID4352#{guid}` → `DISPLAY\GSM83CD\5&367fb4cb&0&UID4352`.
|
||||
/// The standard device-interface-path → instance-id transform: strip the `\\?\` prefix and the
|
||||
/// trailing `#{interface-class-guid}`, then `#` separators become `\`.
|
||||
fn instance_id_from_interface_path(path: &str) -> Option<String> {
|
||||
let rest = path.strip_prefix(r"\\?\")?;
|
||||
let cut = rest.rfind("#{")?;
|
||||
Some(rest[..cut].replace('#', "\\"))
|
||||
}
|
||||
|
||||
fn utf16z(buf: &[u16]) -> String {
|
||||
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
|
||||
String::from_utf16_lossy(&buf[..len])
|
||||
}
|
||||
|
||||
/// Resolve a CCD target to its monitor's (instance id, friendly name).
|
||||
fn monitor_instance(adapter: LUID, target_id: u32) -> Option<(String, String)> {
|
||||
let mut req = DISPLAYCONFIG_TARGET_DEVICE_NAME::default();
|
||||
req.header.r#type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;
|
||||
req.header.size = std::mem::size_of::<DISPLAYCONFIG_TARGET_DEVICE_NAME>() as u32;
|
||||
req.header.adapterId = adapter;
|
||||
req.header.id = target_id;
|
||||
// SAFETY: `req` is a properly-sized DISPLAYCONFIG_TARGET_DEVICE_NAME local whose header
|
||||
// (type/size/adapterId/id) is fully initialised; the API writes only within the struct.
|
||||
let rc = unsafe { DisplayConfigGetDeviceInfo(&mut req.header) };
|
||||
if rc != 0 {
|
||||
return None;
|
||||
}
|
||||
let id = instance_id_from_interface_path(&utf16z(&req.monitorDevicePath))?;
|
||||
Some((id, utf16z(&req.monitorFriendlyDeviceName)))
|
||||
}
|
||||
|
||||
/// Apply enable/disable to one PnP instance id. Returns whether the config action succeeded.
|
||||
fn set_devnode(id: &str, disable: bool) -> bool {
|
||||
let wide: Vec<u16> = id.encode_utf16().chain([0]).collect();
|
||||
let mut devinst = 0u32;
|
||||
// A disabled (or currently-departed) devnode may not be in the live tree — locate PHANTOM for
|
||||
// the enable path so recovery still finds it; the disable path requires a present device.
|
||||
let flags = if disable {
|
||||
CM_LOCATE_DEVNODE_NORMAL
|
||||
} else {
|
||||
CM_LOCATE_DEVNODE_PHANTOM
|
||||
};
|
||||
// SAFETY: `wide` is a live NUL-terminated UTF-16 instance id outliving the call; `devinst` is
|
||||
// a valid out-param.
|
||||
let cr = unsafe { CM_Locate_DevNodeW(&mut devinst, PCWSTR(wide.as_ptr()), flags) };
|
||||
if cr != CR_SUCCESS {
|
||||
tracing::warn!(id, cr = cr.0, "PnP-disable: CM_Locate_DevNodeW failed");
|
||||
return false;
|
||||
}
|
||||
// SAFETY: `devinst` is the devnode the locate above resolved; plain value flags.
|
||||
let cr = unsafe {
|
||||
if disable {
|
||||
// PERSIST is the point: the standby monitor's hot-plug RE-ARRIVAL must stay disabled,
|
||||
// otherwise every wake event recreates an enabled devnode and the churn is back.
|
||||
CM_Disable_DevNode(devinst, CM_DISABLE_PERSIST)
|
||||
} else {
|
||||
CM_Enable_DevNode(devinst, 0)
|
||||
}
|
||||
};
|
||||
if cr != CR_SUCCESS {
|
||||
tracing::warn!(
|
||||
id,
|
||||
cr = cr.0,
|
||||
disable,
|
||||
"PnP-disable: CM_{}_DevNode failed",
|
||||
if disable { "Disable" } else { "Enable" }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Disable the devnodes of every monitor the `Exclusive` isolate deactivated: all ACTIVE paths in
|
||||
/// the pre-isolate snapshot whose target is not `keep_target_id`. Journals BEFORE disabling.
|
||||
/// Returns the instance ids to re-enable at teardown.
|
||||
pub fn disable_for_deactivated(
|
||||
saved: &crate::win_display::SavedConfig,
|
||||
keep_target_id: u32,
|
||||
) -> Vec<String> {
|
||||
const DISPLAYCONFIG_PATH_ACTIVE: u32 = 0x0000_0001;
|
||||
let mut targets: Vec<(String, String)> = Vec::new();
|
||||
for p in &saved.0 {
|
||||
if p.targetInfo.id == keep_target_id || p.flags & DISPLAYCONFIG_PATH_ACTIVE == 0 {
|
||||
continue;
|
||||
}
|
||||
match monitor_instance(p.targetInfo.adapterId, p.targetInfo.id) {
|
||||
Some(hit) => {
|
||||
if !targets.contains(&hit) {
|
||||
targets.push(hit);
|
||||
}
|
||||
}
|
||||
None => tracing::debug!(
|
||||
target_id = p.targetInfo.id,
|
||||
"PnP-disable: no monitor device name for deactivated target — skipping"
|
||||
),
|
||||
}
|
||||
}
|
||||
if targets.is_empty() {
|
||||
tracing::info!("PnP-disable: no physical monitor devnodes to disable");
|
||||
return Vec::new();
|
||||
}
|
||||
// Journal FIRST (union with any outstanding ids), so a crash between here and the disable
|
||||
// over-recovers instead of leaking a disabled monitor.
|
||||
let mut journal = read_journal();
|
||||
for (id, _) in &targets {
|
||||
if !journal.contains(id) {
|
||||
journal.push(id.clone());
|
||||
}
|
||||
}
|
||||
write_journal(&journal);
|
||||
let mut disabled = Vec::new();
|
||||
for (id, name) in targets {
|
||||
if set_devnode(&id, true) {
|
||||
tracing::info!(id, monitor = name, "PnP-disable: monitor devnode disabled");
|
||||
disabled.push(id);
|
||||
}
|
||||
}
|
||||
disabled
|
||||
}
|
||||
|
||||
/// Re-enable `ids` (teardown / recovery) and clear them from the journal.
|
||||
pub fn enable_instances(ids: &[String]) -> u32 {
|
||||
let mut ok = 0u32;
|
||||
for id in ids {
|
||||
if set_devnode(id, false) {
|
||||
tracing::info!(id, "PnP-disable: monitor devnode re-enabled");
|
||||
ok += 1;
|
||||
}
|
||||
}
|
||||
let journal: Vec<String> = read_journal()
|
||||
.into_iter()
|
||||
.filter(|j| !ids.contains(j))
|
||||
.collect();
|
||||
write_journal(&journal);
|
||||
ok
|
||||
}
|
||||
|
||||
/// Host-startup crash recovery: re-enable any devnodes a previous host disabled but never
|
||||
/// restored (crash, kill, power loss). Call once, early in `serve`.
|
||||
pub fn startup_recover() {
|
||||
let leftovers = read_journal();
|
||||
if leftovers.is_empty() {
|
||||
return;
|
||||
}
|
||||
tracing::warn!(
|
||||
count = leftovers.len(),
|
||||
"PnP-disable: found monitor devnodes a previous host left disabled — re-enabling"
|
||||
);
|
||||
enable_instances(&leftovers);
|
||||
}
|
||||
Reference in New Issue
Block a user