mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-14 16:31:03 +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);
|
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 struct Displays {
|
||||||
pub primary: usize,
|
pub primary: usize,
|
||||||
pub displays: Vec<WaylandDisplayInfo>,
|
pub displays: Vec<WaylandDisplayInfo>,
|
||||||
@@ -171,11 +183,76 @@ fn get_primary_monitor() -> Option<String> {
|
|||||||
.or_else(try_gdbus_primary)
|
.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> {
|
pub fn get_displays() -> Arc<Displays> {
|
||||||
let mut lock = DISPLAYS.lock().unwrap();
|
let mut lock = DISPLAYS.lock().unwrap();
|
||||||
match lock.as_ref() {
|
match lock.as_ref() {
|
||||||
Some(displays) => displays.clone(),
|
Some(displays) => displays.clone(),
|
||||||
None => match get_wayland_displays() {
|
None => match enumerate_displays() {
|
||||||
Ok(displays) => {
|
Ok(displays) => {
|
||||||
let mut primary_index = None;
|
let mut primary_index = None;
|
||||||
if let Some(name) = get_primary_monitor() {
|
if let Some(name) = get_primary_monitor() {
|
||||||
@@ -201,8 +278,9 @@ pub fn get_displays() -> Arc<Displays> {
|
|||||||
*lock = Some(displays.clone());
|
*lock = Some(displays.clone());
|
||||||
displays
|
displays
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(_err) => {
|
||||||
warn!("Failed to get wayland displays: {}", err);
|
#[cfg(not(feature = "drm"))]
|
||||||
|
warn!("Failed to get wayland displays: {}", _err);
|
||||||
Arc::new(Displays {
|
Arc::new(Displays {
|
||||||
primary: 0,
|
primary: 0,
|
||||||
displays: Vec::new(),
|
displays: Vec::new(),
|
||||||
@@ -215,6 +293,8 @@ pub fn get_displays() -> Arc<Displays> {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn clear_wayland_displays_cache() {
|
pub fn clear_wayland_displays_cache() {
|
||||||
let _ = DISPLAYS.lock().unwrap().take();
|
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)
|
// 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)
|
desktop_rect_of(&wayland_displays.displays)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The desktop rect and per-display logical rects, always read live from the
|
// The desktop rect and per-display logical rects, read live from the compositor in a single
|
||||||
// compositor in a single roundtrip. Skips the displays cache and the primary-monitor
|
// roundtrip (drm builds may skip a turn during the failure backoff). Skips the displays cache
|
||||||
// detection (which may spawn external commands), so it is cheap enough to poll for
|
// and the primary-monitor detection, cheap enough to poll. rustdesk/rustdesk#15601
|
||||||
// layout changes. https://github.com/rustdesk/rustdesk/issues/15601
|
|
||||||
pub fn get_layout_for_uinput_live() -> Option<((i32, i32, i32, i32), Vec<DisplayRect>)> {
|
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) => {
|
Ok(displays) => {
|
||||||
desktop_rect_of(&displays).map(|rect| (rect, logical_rects_of(&displays)))
|
desktop_rect_of(&displays).map(|rect| (rect, logical_rects_of(&displays)))
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(_err) => {
|
||||||
warn!("Failed to get wayland displays: {}", err);
|
#[cfg(not(feature = "drm"))]
|
||||||
|
warn!("Failed to get wayland displays: {}", _err);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -386,6 +470,40 @@ fn map_axis(v: i32, base_origin: i32, base_extent: i32, live_origin: i32, live_e
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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(
|
fn display(
|
||||||
x: i32,
|
x: i32,
|
||||||
y: i32,
|
y: i32,
|
||||||
|
|||||||
@@ -346,8 +346,21 @@ fn check_get_displays_changed_msg() -> Option<Message> {
|
|||||||
// list that overwrites the login peer-info displays and the client shows "No displays".
|
// list that overwrites the login peer-info displays and the client shows "No displays".
|
||||||
#[cfg(feature = "drm")]
|
#[cfg(feature = "drm")]
|
||||||
if super::drm_capturer::is_available_cached() {
|
if super::drm_capturer::is_available_cached() {
|
||||||
if let Some(displays) = super::drm_capturer::get_display_infos() {
|
let synced = !SYNC_DISPLAYS.lock().unwrap().displays.is_empty();
|
||||||
SYNC_DISPLAYS.lock().unwrap().check_changed(&displays);
|
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();
|
return get_displays_msg();
|
||||||
|
|||||||
Reference in New Issue
Block a user