diff --git a/libs/scrap/src/wayland/display.rs b/libs/scrap/src/wayland/display.rs index bed90fd76..1a9f29f25 100644 --- a/libs/scrap/src/wayland/display.rs +++ b/libs/scrap/src/wayland/display.rs @@ -19,6 +19,18 @@ static MISSING_LOGICAL_SIZE_WARNED: std::sync::atomic::AtomicBool = const COMMAND_TIMEOUT: Duration = Duration::from_millis(1000); +// drm builds only: an unnamed-endpoint failure there forks the probe child, and the pollers +// turn every few hundred milliseconds. Every other failure is one cheap in-process error. +#[cfg(any(test, feature = "drm"))] +const FAILED_LOOKUP_BACKOFF: Duration = Duration::from_secs(5); + +#[cfg(any(test, feature = "drm"))] +static LAST_FAILED_LOOKUP: Mutex> = Mutex::new(None); + +#[cfg(feature = "drm")] +static LOOKUP_FAILURE_WARNED: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + pub struct Displays { pub primary: usize, pub displays: Vec, @@ -171,11 +183,76 @@ fn get_primary_monitor() -> Option { .or_else(try_gdbus_primary) } +// Pure, so the backoff policy is testable without a compositor. +#[cfg(any(test, feature = "drm"))] +fn lookup_allowed(failed_at: Option, now: Instant) -> bool { + failed_at.map_or(true, |at| { + now.saturating_duration_since(at) >= FAILED_LOOKUP_BACKOFF + }) +} + +#[cfg(feature = "drm")] +fn backed_off() -> bool { + let failed_at = *LAST_FAILED_LOOKUP.lock().unwrap(); + !lookup_allowed(failed_at, Instant::now()) +} + +// Mirrors the probe module's gate, latch included: connecting consumes WAYLAND_SOCKET, so a +// once-named endpoint must stay named for the life of the process. +#[cfg(feature = "drm")] +fn endpoint_named() -> bool { + use std::sync::atomic::{AtomicBool, Ordering}; + static WAS_NAMED: AtomicBool = AtomicBool::new(false); + let named = ["WAYLAND_DISPLAY", "WAYLAND_SOCKET"] + .iter() + .any(|key| std::env::var_os(key).is_some_and(|value| !value.is_empty())); + if named { + WAS_NAMED.store(true, Ordering::Release); + } + WAS_NAMED.load(Ordering::Acquire) +} + +// Enumerates and keeps the failure stamp current. Suppresses nothing itself: one-shot callers +// (session init, pipewire) must always get a fresh read, or a transient failure latches. +fn enumerate_displays() -> hbb_common::ResultType> { + // Read before connecting, which consumes WAYLAND_SOCKET. + #[cfg(feature = "drm")] + let named = endpoint_named(); + let probed = get_wayland_displays(); + // Only the failure that would fork stamps; a named endpoint fails cheaply in-process. + #[cfg(feature = "drm")] + { + *LAST_FAILED_LOOKUP.lock().unwrap() = (probed.is_err() && !named).then(Instant::now); + if let Err(err) = &probed { + if !LOOKUP_FAILURE_WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) { + warn!("Failed to get wayland displays: {}", err); + } + } else { + LOOKUP_FAILURE_WARNED.store(false, std::sync::atomic::Ordering::Relaxed); + } + } + probed +} + +// True when a lookup now could neither hit the cache nor probe. Pollers skip their turn on +// it and keep their last published state; one-shot callers must not consult it. +#[cfg(feature = "drm")] +pub fn wayland_lookup_suppressed() -> bool { + DISPLAYS.lock().unwrap().is_none() && backed_off() +} + +// Whether any failure stamp exists, expired or not: pollers use it to tell a first failure +// from one that has already persisted across a backoff. +#[cfg(feature = "drm")] +pub fn wayland_failure_stamped() -> bool { + LAST_FAILED_LOOKUP.lock().unwrap().is_some() +} + pub fn get_displays() -> Arc { let mut lock = DISPLAYS.lock().unwrap(); match lock.as_ref() { Some(displays) => displays.clone(), - None => match get_wayland_displays() { + None => match enumerate_displays() { Ok(displays) => { let mut primary_index = None; if let Some(name) = get_primary_monitor() { @@ -201,8 +278,9 @@ pub fn get_displays() -> Arc { *lock = Some(displays.clone()); displays } - Err(err) => { - warn!("Failed to get wayland displays: {}", err); + Err(_err) => { + #[cfg(not(feature = "drm"))] + warn!("Failed to get wayland displays: {}", _err); Arc::new(Displays { primary: 0, displays: Vec::new(), @@ -215,6 +293,8 @@ pub fn get_displays() -> Arc { #[inline] pub fn clear_wayland_displays_cache() { let _ = DISPLAYS.lock().unwrap().take(); + // The failure stamp survives on purpose: it describes the seat, not the cache, and the + // capturer rebuild loop clears about once a second. } // Return (min_x, max_x, min_y, max_y) @@ -223,17 +303,21 @@ pub fn get_desktop_rect_for_uinput() -> Option<(i32, i32, i32, i32)> { desktop_rect_of(&wayland_displays.displays) } -// The desktop rect and per-display logical rects, always read live from the -// compositor in a single roundtrip. Skips the displays cache and the primary-monitor -// detection (which may spawn external commands), so it is cheap enough to poll for -// layout changes. https://github.com/rustdesk/rustdesk/issues/15601 +// The desktop rect and per-display logical rects, read live from the compositor in a single +// roundtrip (drm builds may skip a turn during the failure backoff). Skips the displays cache +// and the primary-monitor detection, cheap enough to poll. rustdesk/rustdesk#15601 pub fn get_layout_for_uinput_live() -> Option<((i32, i32, i32, i32), Vec)> { - match get_wayland_displays() { + #[cfg(feature = "drm")] + if backed_off() { + return None; + } + match enumerate_displays() { Ok(displays) => { desktop_rect_of(&displays).map(|rect| (rect, logical_rects_of(&displays))) } - Err(err) => { - warn!("Failed to get wayland displays: {}", err); + Err(_err) => { + #[cfg(not(feature = "drm"))] + warn!("Failed to get wayland displays: {}", _err); None } } @@ -386,6 +470,40 @@ fn map_axis(v: i32, base_origin: i32, base_extent: i32, live_origin: i32, live_e mod tests { use super::*; + #[test] + fn test_lookup_backoff_boundaries() { + // Future `now`s sidestep Instant subtraction, which can panic near boot. + let failed_at = Instant::now(); + assert!(lookup_allowed(None, failed_at)); + assert!(!lookup_allowed( + Some(failed_at), + failed_at + FAILED_LOOKUP_BACKOFF / 2 + )); + assert!(lookup_allowed( + Some(failed_at), + failed_at + FAILED_LOOKUP_BACKOFF + )); + } + + #[test] + fn test_lookup_stamp_from_the_future_only_waits() { + // saturating_duration_since answers zero rather than underflowing. + let now = Instant::now(); + assert!(!lookup_allowed(Some(now + FAILED_LOOKUP_BACKOFF), now)); + } + + #[test] + fn test_clear_keeps_the_failure_stamp() { + // The stamp describes the seat, not the cache: the ~1/s capturer rebuild loop clears, + // and dropping the stamp with it would defeat the backoff. Sole test touching these + // statics; serialize before adding another. + *LAST_FAILED_LOOKUP.lock().unwrap() = Some(Instant::now()); + clear_wayland_displays_cache(); + let stamp = *LAST_FAILED_LOOKUP.lock().unwrap(); + assert!(stamp.is_some()); + *LAST_FAILED_LOOKUP.lock().unwrap() = None; + } + fn display( x: i32, y: i32, diff --git a/src/server/display_service.rs b/src/server/display_service.rs index 7572caf10..235c7ca86 100644 --- a/src/server/display_service.rs +++ b/src/server/display_service.rs @@ -346,8 +346,21 @@ fn check_get_displays_changed_msg() -> Option { // list that overwrites the login peer-info displays and the client shows "No displays". #[cfg(feature = "drm")] if super::drm_capturer::is_available_cached() { - if let Some(displays) = super::drm_capturer::get_display_infos() { - SYNC_DISPLAYS.lock().unwrap().check_changed(&displays); + let synced = !SYNC_DISPLAYS.lock().unwrap().displays.is_empty(); + let stamped_before = scrap::wayland::display::wayland_failure_stamped(); + // With nothing published yet, even the unaugmented DRM list beats the empty + // broadcast below; with a synced layout, a suppressed turn keeps it instead. + if !synced || !scrap::wayland::display::wayland_lookup_suppressed() { + if let Some(displays) = super::drm_capturer::get_display_infos() { + // A first failure keeps the synced layout for one backoff; only a + // failure that persists across one replaces it with the DRM stack. + if !synced + || stamped_before + || !scrap::wayland::display::wayland_lookup_suppressed() + { + SYNC_DISPLAYS.lock().unwrap().check_changed(&displays); + } + } } } return get_displays_msg();