mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-08 21:41:02 +03:00
fix(wayland): back off the polling display lookups after a failure (drm) (#15865)
* fix(wayland): back off the polling display lookups after a failure (drm) In drm builds an enumeration that fails with no endpoint named in the environment falls back to the socket probe, which forks a child bounded by seconds, and the display service asks again every 300 ms -- at a greeter with no reachable compositor that is a probe child per turn, forever. Such a failure now stamps a shared 5 s backoff, and only the polling callers honor it: the 300 ms displays-changed check skips its turn and the 1.5 s live layout poll returns no answer for that turn. Only the failure that would fork stamps. A session server is spawned with WAYLAND_DISPLAY set, so its failed connect bails in-process before any fork; stamping there would buy nothing and cost recovery latency, so live sessions keep master's behavior exactly. The stamp also survives clear_wayland_displays_cache: it describes the seat, not the cache, and the ~1/s capturer rebuild loop clears on every teardown -- dropping the stamp with the cache would let that loop defeat the backoff and would turn every post-hotplug failure into a "first" one forever. The displays-changed check weighs the backoff against what is already published. With nothing synced yet it always populates -- an unaugmented DRM list beats the empty broadcast the send path would otherwise emit. With a synced layout, a suppressed turn keeps it, and a fresh first failure keeps it too; only a failure that persists across a backoff replaces it with the DRM stack, so a hotplug at a failing seat converges within one backoff while a transient failure never tears down a good layout. One-shot callers -- session init, pipewire stream setup, capturer info -- keep probing fresh through get_displays, whose failure semantics are unchanged: replaying a transient failure there would latch an empty answer into session-long state. Non-drm builds compile none of this. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(wayland): log DRM lookup failure once * fix(wayland): reset lookup warning after recovery --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Option<Instant>> = 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<WaylandDisplayInfo>,
|
||||
@@ -171,11 +183,76 @@ fn get_primary_monitor() -> Option<String> {
|
||||
.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<Instant>, 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<Vec<WaylandDisplayInfo>> {
|
||||
// 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<Displays> {
|
||||
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<Displays> {
|
||||
*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<Displays> {
|
||||
#[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<DisplayRect>)> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user