mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-18 18:31:02 +03:00
* 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>
724 lines
26 KiB
Rust
724 lines
26 KiB
Rust
use super::*;
|
|
use crate::common::SimpleCallOnReturn;
|
|
#[cfg(target_os = "linux")]
|
|
use crate::platform::linux::is_x11;
|
|
#[cfg(windows)]
|
|
use crate::virtual_display_manager;
|
|
#[cfg(windows)]
|
|
use hbb_common::get_version_number;
|
|
use hbb_common::protobuf::MessageField;
|
|
use scrap::Display;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
// https://github.com/rustdesk/rustdesk/discussions/6042, avoiding dbus call
|
|
|
|
pub const NAME: &'static str = "display";
|
|
|
|
#[cfg(windows)]
|
|
const DUMMY_DISPLAY_SIDE_MAX_SIZE: usize = 1024;
|
|
|
|
struct ChangedResolution {
|
|
original: (i32, i32),
|
|
changed: (i32, i32),
|
|
}
|
|
|
|
lazy_static::lazy_static! {
|
|
static ref IS_CAPTURER_MAGNIFIER_SUPPORTED: bool = is_capturer_mag_supported();
|
|
static ref CHANGED_RESOLUTIONS: Arc<RwLock<HashMap<String, ChangedResolution>>> = Default::default();
|
|
static ref SYNC_DISPLAYS: Arc<Mutex<SyncDisplaysInfo>> = Default::default();
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
lazy_static::lazy_static! {
|
|
static ref WAYLAND_UINPUT_RECT: Mutex<WaylandUinputRect> = Default::default();
|
|
static ref WAYLAND_LAYOUT: Mutex<WaylandLayout> = Default::default();
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
const WAYLAND_LAYOUT_CHECK_INTERVAL: Duration = Duration::from_millis(1500);
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[derive(Default)]
|
|
struct WaylandUinputRect {
|
|
rect: Option<(i32, i32, i32, i32)>,
|
|
last_check: Option<std::time::Instant>,
|
|
}
|
|
|
|
// Per-display layout used to correct injected coordinates when the compositor moves a
|
|
// monitor mid-session. The client keeps sending coordinates offset by the layout it was
|
|
// told at session init (`baseline`); we remap them onto the current layout (`live`).
|
|
// https://github.com/rustdesk/rustdesk/issues/15601
|
|
#[cfg(target_os = "linux")]
|
|
#[derive(Default)]
|
|
struct WaylandLayout {
|
|
baseline: Vec<scrap::wayland::display::DisplayRect>,
|
|
live: Vec<scrap::wayland::display::DisplayRect>,
|
|
}
|
|
|
|
// Whether `live` differs from `baseline`. Read on every mouse move, so it is an atomic:
|
|
// the common (no-drift) case never touches the layout mutex.
|
|
#[cfg(target_os = "linux")]
|
|
static WAYLAND_LAYOUT_DRIFTED: AtomicBool = AtomicBool::new(false);
|
|
|
|
#[cfg(target_os = "linux")]
|
|
pub(super) fn set_wayland_uinput_rect(rect: (i32, i32, i32, i32)) {
|
|
WAYLAND_UINPUT_RECT.lock().unwrap().rect = Some(rect);
|
|
}
|
|
|
|
// The uinput ABS range currently programmed into the device, for the DRM path's "reapply only when
|
|
// it changed" check. The PipeWire path compares it inline in refresh_wayland_uinput_rect_if_changed.
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
pub(super) fn wayland_uinput_rect() -> Option<(i32, i32, i32, i32)> {
|
|
WAYLAND_UINPUT_RECT.lock().unwrap().rect
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
pub(super) fn set_wayland_layout_baseline(baseline: Vec<scrap::wayland::display::DisplayRect>) {
|
|
WAYLAND_LAYOUT_DRIFTED.store(false, Ordering::Relaxed);
|
|
let mut lock = WAYLAND_LAYOUT.lock().unwrap();
|
|
lock.baseline = baseline;
|
|
lock.live.clear();
|
|
}
|
|
|
|
// Remap an injected coordinate onto the live compositor layout when it has drifted from
|
|
// what the client was told at session init. Lock-free no-op otherwise.
|
|
#[cfg(target_os = "linux")]
|
|
pub(super) fn remap_wayland_uinput_coord(x: i32, y: i32) -> (i32, i32) {
|
|
if !WAYLAND_LAYOUT_DRIFTED.load(Ordering::Relaxed) {
|
|
return (x, y);
|
|
}
|
|
let lock = WAYLAND_LAYOUT.lock().unwrap();
|
|
scrap::wayland::display::remap_to_live_layout(x, y, &lock.baseline, &lock.live)
|
|
}
|
|
|
|
// The uinput absolute range is set when the session inits. If the compositor layout
|
|
// changes afterwards (monitor scale/position change, or a portal virtual output
|
|
// appearing once the capture starts), injected coordinates get rescaled by the stale
|
|
// range and land offset, https://github.com/rustdesk/rustdesk/issues/15601
|
|
#[cfg(target_os = "linux")]
|
|
fn refresh_wayland_uinput_rect_if_changed() {
|
|
if is_x11() || !crate::input_service::wayland_use_uinput() {
|
|
return;
|
|
}
|
|
// Nothing to poll at a login screen; the DRM path owns the rect there.
|
|
#[cfg(feature = "drm")]
|
|
if crate::platform::linux::is_login_screen_wayland_cached() {
|
|
return;
|
|
}
|
|
{
|
|
let mut lock = WAYLAND_UINPUT_RECT.lock().unwrap();
|
|
if let Some(last_check) = lock.last_check {
|
|
if last_check.elapsed() < WAYLAND_LAYOUT_CHECK_INTERVAL {
|
|
return;
|
|
}
|
|
}
|
|
lock.last_check = Some(std::time::Instant::now());
|
|
}
|
|
let Some((rect, live_rects)) = scrap::wayland::display::get_layout_for_uinput_live() else {
|
|
return;
|
|
};
|
|
// Refresh the per-display layout every poll: monitor origins can shift (e.g. two
|
|
// displays swap positions) without changing the overall desktop rect, and the mouse
|
|
// path needs the current per-display geometry to correct coordinates.
|
|
let drifted = {
|
|
let mut layout = WAYLAND_LAYOUT.lock().unwrap();
|
|
let drifted = !layout.baseline.is_empty()
|
|
&& !live_rects.is_empty()
|
|
&& layout.baseline != live_rects;
|
|
layout.live = live_rects;
|
|
drifted
|
|
};
|
|
// The remap corrects for per-display origin shifts; the uinput ABS range corrects for
|
|
// the overall bounding box. Only enable the remap once the range matches the live
|
|
// layout, otherwise moves would be remapped into a range the device is not yet using.
|
|
// A drift with no bbox change (origins swapped) needs no range update and enables now.
|
|
let mut range_ok = WAYLAND_UINPUT_RECT.lock().unwrap().rect == Some(rect);
|
|
if !range_ok {
|
|
let (minx, maxx, miny, maxy) = rect;
|
|
log::info!(
|
|
"desktop layout changed, update mouse resolution: ({}, {}), ({}, {})",
|
|
minx,
|
|
maxx,
|
|
miny,
|
|
maxy
|
|
);
|
|
match tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
{
|
|
Ok(rt) => {
|
|
// Bound the IPC wait, this runs on the display service loop and
|
|
// `set_resolution()` has no timeout on the response read.
|
|
// timeout must be built inside the runtime, or it panics
|
|
// "there is no reactor running". See clipboard_service.rs.
|
|
match rt.block_on(async {
|
|
timeout(
|
|
3_000,
|
|
crate::input_service::update_mouse_resolution(minx, maxx, miny, maxy),
|
|
)
|
|
.await
|
|
}) {
|
|
// Record the rect only after a successful apply, so a transient
|
|
// failure is retried on the next check.
|
|
Ok(Ok(())) => {
|
|
WAYLAND_UINPUT_RECT.lock().unwrap().rect = Some(rect);
|
|
range_ok = true;
|
|
}
|
|
Ok(Err(err)) => log::error!("Failed to update mouse resolution: {}", err),
|
|
Err(err) => log::error!("Failed to update mouse resolution: {}", err),
|
|
}
|
|
}
|
|
Err(err) => {
|
|
log::error!("Failed to build tokio runtime: {}", err);
|
|
}
|
|
}
|
|
}
|
|
// Publish the flag last: a `true` read is always backed by a current `live` and a
|
|
// matching uinput range. A failed range apply leaves this false and retries next poll.
|
|
WAYLAND_LAYOUT_DRIFTED.store(drifted && range_ok, Ordering::Relaxed);
|
|
}
|
|
|
|
// https://github.com/rustdesk/rustdesk/pull/8537
|
|
static TEMP_IGNORE_DISPLAYS_CHANGED: AtomicBool = AtomicBool::new(false);
|
|
|
|
#[derive(Default)]
|
|
struct SyncDisplaysInfo {
|
|
displays: Vec<DisplayInfo>,
|
|
is_synced: bool,
|
|
}
|
|
|
|
impl SyncDisplaysInfo {
|
|
fn check_changed(&mut self, displays: &[DisplayInfo]) {
|
|
if self.displays.as_slice() == displays {
|
|
return;
|
|
}
|
|
|
|
self.displays = displays.to_vec();
|
|
if !TEMP_IGNORE_DISPLAYS_CHANGED.load(Ordering::Relaxed) {
|
|
self.is_synced = false;
|
|
}
|
|
}
|
|
|
|
fn get_update_sync_displays(&mut self) -> Option<Vec<DisplayInfo>> {
|
|
if self.is_synced {
|
|
return None;
|
|
}
|
|
self.is_synced = true;
|
|
Some(self.displays.clone())
|
|
}
|
|
}
|
|
|
|
pub fn temp_ignore_displays_changed() -> SimpleCallOnReturn {
|
|
TEMP_IGNORE_DISPLAYS_CHANGED.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
SimpleCallOnReturn {
|
|
b: true,
|
|
f: Box::new(move || {
|
|
// Wait for a while to make sure check_display_changed() is called
|
|
// after video service has sending its `SwitchDisplay` message(`try_broadcast_display_changed()`).
|
|
std::thread::sleep(Duration::from_millis(1000));
|
|
TEMP_IGNORE_DISPLAYS_CHANGED.store(false, Ordering::Relaxed);
|
|
// Trigger the display changed message.
|
|
SYNC_DISPLAYS.lock().unwrap().is_synced = false;
|
|
}),
|
|
}
|
|
}
|
|
|
|
// This function is really useful, though a duplicate check if display changed.
|
|
// The video server will then send the following messages to the client:
|
|
// 1. the supported resolutions of the {idx} display
|
|
// 2. the switch resolution message, so that the client can record the custom resolution.
|
|
pub(super) fn check_display_changed(
|
|
ndisplay: usize,
|
|
idx: usize,
|
|
(x, y, w, h): (i32, i32, usize, usize),
|
|
) -> Option<DisplayInfo> {
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
// wayland do not support changing display for now
|
|
if !is_x11() {
|
|
return None;
|
|
}
|
|
}
|
|
|
|
let lock = SYNC_DISPLAYS.lock().unwrap();
|
|
// If plugging out a monitor && lock.displays.get(idx) is None.
|
|
// 1. The client version < 1.2.4. The client side has to reconnect.
|
|
// 2. The client version > 1.2.4, The client side can handle the case because sync peer info message will be sent.
|
|
// But it is acceptable to for the user to reconnect manually, because the monitor is unplugged.
|
|
let d = lock.displays.get(idx)?;
|
|
if ndisplay != lock.displays.len() {
|
|
return Some(d.clone());
|
|
}
|
|
if !(d.x == x && d.y == y && d.width == w as i32 && d.height == h as i32) {
|
|
Some(d.clone())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_last_changed_resolution(display_name: &str, original: (i32, i32), changed: (i32, i32)) {
|
|
let mut lock = CHANGED_RESOLUTIONS.write().unwrap();
|
|
match lock.get_mut(display_name) {
|
|
Some(res) => res.changed = changed,
|
|
None => {
|
|
lock.insert(
|
|
display_name.to_owned(),
|
|
ChangedResolution { original, changed },
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
pub fn restore_resolutions() {
|
|
for (name, res) in CHANGED_RESOLUTIONS.read().unwrap().iter() {
|
|
let (w, h) = res.original;
|
|
log::info!("Restore resolution of display '{}' to ({}, {})", name, w, h);
|
|
if let Err(e) = crate::platform::change_resolution(name, w as _, h as _) {
|
|
log::error!(
|
|
"Failed to restore resolution of display '{}' to ({},{}): {}",
|
|
name,
|
|
w,
|
|
h,
|
|
e
|
|
);
|
|
}
|
|
}
|
|
// Can be cleared because restore resolutions is called when there is no client connected.
|
|
CHANGED_RESOLUTIONS.write().unwrap().clear();
|
|
}
|
|
|
|
#[inline]
|
|
fn is_capturer_mag_supported() -> bool {
|
|
#[cfg(windows)]
|
|
return scrap::CapturerMag::is_supported();
|
|
#[cfg(not(windows))]
|
|
false
|
|
}
|
|
|
|
#[inline]
|
|
pub fn capture_cursor_embedded() -> bool {
|
|
scrap::is_cursor_embedded()
|
|
}
|
|
|
|
#[inline]
|
|
#[cfg(windows)]
|
|
pub fn is_privacy_mode_mag_supported() -> bool {
|
|
return *IS_CAPTURER_MAGNIFIER_SUPPORTED
|
|
&& get_version_number(&crate::VERSION) > get_version_number("1.1.9");
|
|
}
|
|
|
|
pub fn new() -> GenericService {
|
|
let svc = EmptyExtraFieldService::new(NAME.to_owned(), true);
|
|
GenericService::run(&svc.clone(), run);
|
|
svc.sp
|
|
}
|
|
|
|
fn displays_to_msg(displays: Vec<DisplayInfo>) -> Message {
|
|
let mut pi = PeerInfo {
|
|
..Default::default()
|
|
};
|
|
pi.displays = displays.clone();
|
|
|
|
#[cfg(windows)]
|
|
if crate::platform::is_installed() {
|
|
let m = crate::virtual_display_manager::get_platform_additions();
|
|
pi.platform_additions = serde_json::to_string(&m).unwrap_or_default();
|
|
}
|
|
|
|
// current_display should not be used in server.
|
|
// It is set to 0 for compatibility with old clients.
|
|
pi.current_display = 0;
|
|
let mut msg_out = Message::new();
|
|
msg_out.set_peer_info(pi);
|
|
msg_out
|
|
}
|
|
|
|
fn check_get_displays_changed_msg() -> Option<Message> {
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
if !is_x11() {
|
|
// On the DRM/KMS capture path the PipeWire enumeration (which is what feeds
|
|
// `SYNC_DISPLAYS` via `check_update_displays`) is bypassed, so populate the sync list
|
|
// from the DRM display list here. Without this the display service broadcasts an empty
|
|
// list that overwrites the login peer-info displays and the client shows "No displays".
|
|
#[cfg(feature = "drm")]
|
|
if super::drm_capturer::is_available_cached() {
|
|
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();
|
|
}
|
|
}
|
|
check_update_displays(&try_get_displays().ok()?);
|
|
get_displays_msg()
|
|
}
|
|
|
|
pub fn check_displays_changed() -> ResultType<()> {
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
// Currently, wayland need to call wayland::clear() before call Display::all(), otherwise it will cause
|
|
// block, or even crash here, https://github.com/rustdesk/rustdesk/blob/0bb4d43e9ea9d9dfb9c46c8d27d1a97cd0ad6bea/libs/scrap/src/wayland/pipewire.rs#L235
|
|
if !is_x11() {
|
|
return Ok(());
|
|
}
|
|
}
|
|
check_update_displays(&try_get_displays()?);
|
|
Ok(())
|
|
}
|
|
|
|
fn get_displays_msg() -> Option<Message> {
|
|
let displays = SYNC_DISPLAYS.lock().unwrap().get_update_sync_displays()?;
|
|
Some(displays_to_msg(displays))
|
|
}
|
|
|
|
fn run(sp: EmptyExtraFieldService) -> ResultType<()> {
|
|
while sp.ok() {
|
|
sp.snapshot(|sps| {
|
|
if !TEMP_IGNORE_DISPLAYS_CHANGED.load(Ordering::Relaxed) {
|
|
if sps.has_subscribes() {
|
|
SYNC_DISPLAYS.lock().unwrap().is_synced = false;
|
|
bail!("new subscriber");
|
|
}
|
|
}
|
|
Ok(())
|
|
})?;
|
|
|
|
if let Some(msg_out) = check_get_displays_changed_msg() {
|
|
sp.send(msg_out);
|
|
log::info!("Displays changed");
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
if sp.has_subscribes() {
|
|
refresh_wayland_uinput_rect_if_changed();
|
|
}
|
|
|
|
std::thread::sleep(Duration::from_millis(300));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[inline]
|
|
pub(super) fn get_original_resolution(
|
|
display_name: &str,
|
|
w: usize,
|
|
h: usize,
|
|
) -> MessageField<Resolution> {
|
|
#[cfg(windows)]
|
|
let is_rustdesk_virtual_display =
|
|
crate::virtual_display_manager::rustdesk_idd::is_virtual_display(&display_name);
|
|
#[cfg(not(windows))]
|
|
let is_rustdesk_virtual_display = false;
|
|
Some(if is_rustdesk_virtual_display {
|
|
Resolution {
|
|
width: 0,
|
|
height: 0,
|
|
..Default::default()
|
|
}
|
|
} else {
|
|
let changed_resolutions = CHANGED_RESOLUTIONS.write().unwrap();
|
|
let (width, height) = match changed_resolutions.get(display_name) {
|
|
Some(res) => {
|
|
res.original
|
|
/*
|
|
The resolution change may not happen immediately, `changed` has been updated,
|
|
but the actual resolution is old, it will be mistaken for a third-party change.
|
|
if res.changed.0 != w as i32 || res.changed.1 != h as i32 {
|
|
// If the resolution is changed by third process, remove the record in changed_resolutions.
|
|
changed_resolutions.remove(display_name);
|
|
(w as _, h as _)
|
|
} else {
|
|
res.original
|
|
}
|
|
*/
|
|
}
|
|
None => (w as _, h as _),
|
|
};
|
|
Resolution {
|
|
width,
|
|
height,
|
|
..Default::default()
|
|
}
|
|
})
|
|
.into()
|
|
}
|
|
|
|
pub(super) fn get_sync_displays() -> Vec<DisplayInfo> {
|
|
SYNC_DISPLAYS.lock().unwrap().displays.clone()
|
|
}
|
|
|
|
pub(super) fn get_display_info(idx: usize) -> Option<DisplayInfo> {
|
|
SYNC_DISPLAYS.lock().unwrap().displays.get(idx).cloned()
|
|
}
|
|
|
|
// True when at least one advertised (synced) display is NOT served by the DRM/KMS capture path,
|
|
// i.e. a mixed DRM + PipeWire session. The cursor service (platform::linux::get_cursor /
|
|
// get_cursor_data) uses this to decide whether a hidden DRM hardware-cursor sentinel is
|
|
// authoritative: in a pure-DRM session it is (the pointer is genuinely off every captured CRTC),
|
|
// but in a mixed session the sentinel only means the pointer moved onto a PipeWire-served display,
|
|
// whose cursor must come from the normal path instead of being hidden everywhere.
|
|
//
|
|
// When DRM capture is active the advertised list is enumerated from the DRM display list, so a DRM
|
|
// list shorter than the synced list means at least one advertised display is served by PipeWire.
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
pub fn has_non_drm_backed_display() -> bool {
|
|
match super::drm_capturer::display_count_and_any_demoted() {
|
|
// A display served by PipeWire is either ABSENT from the DRM list (a shorter count, e.g. a
|
|
// pure-portal display) or PRESENT-BUT-DEMOTED (kept in place at the same index and marked
|
|
// offline so the index space stays aligned -- see get_display_infos). The count check alone
|
|
// misses the demotion case (same count), so a demoted display is treated as non-DRM-backed
|
|
// too. This is what gates the hidden-cursor sentinel: it stays authoritative only in a
|
|
// pure-DRM session. The scalar accessor is deliberate: this is polled every cursor tick
|
|
// while the sentinel is active, and cloning + geometry-augmenting the whole list per tick
|
|
// (what get_display_infos does) answered the same two facts.
|
|
Some((count, any_demoted)) => {
|
|
count < SYNC_DISPLAYS.lock().unwrap().displays.len() || any_demoted
|
|
}
|
|
None => false,
|
|
}
|
|
}
|
|
|
|
// Display to DisplayInfo
|
|
// The DisplayInfo is be sent to the peer.
|
|
pub(super) fn check_update_displays(all: &Vec<Display>) {
|
|
let _ = update_sync_displays(all);
|
|
}
|
|
|
|
/// Whether there is a compositor on this seat worth asking. `get_displays()` does not cache
|
|
/// its failure, so where there is none it re-probes every call for an answer that cannot
|
|
/// change any caller's outcome. Last in the `&&` chain, so it never runs first on a poll.
|
|
#[inline]
|
|
#[cfg(target_os = "linux")]
|
|
fn wayland_has_compositor() -> bool {
|
|
#[cfg(feature = "drm")]
|
|
{
|
|
!crate::platform::linux::is_login_screen_wayland_cached()
|
|
}
|
|
#[cfg(not(feature = "drm"))]
|
|
{
|
|
true
|
|
}
|
|
}
|
|
|
|
// Return the converted input snapshot while updating the shared display cache.
|
|
pub(super) fn update_sync_displays(all: &Vec<Display>) -> Vec<DisplayInfo> {
|
|
// For compatibility: if only one display, scale remains 1.0 and we use the physical size for `uinput`.
|
|
// If there are multiple displays, we use the logical size for `uinput` by setting scale to d.scale().
|
|
#[cfg(target_os = "linux")]
|
|
let use_logical_scale = !is_x11()
|
|
&& crate::is_server()
|
|
&& wayland_has_compositor()
|
|
&& scrap::wayland::display::get_displays().displays.len() > 1;
|
|
let displays = all
|
|
.iter()
|
|
.map(|d| {
|
|
let display_name = d.name();
|
|
#[allow(unused_assignments)]
|
|
#[allow(unused_mut)]
|
|
let mut scale = 1.0;
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
scale = d.scale();
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
if use_logical_scale {
|
|
scale = d.scale();
|
|
}
|
|
}
|
|
let original_resolution = get_original_resolution(
|
|
&display_name,
|
|
((d.width() as f64) / scale).round() as usize,
|
|
(d.height() as f64 / scale).round() as usize,
|
|
);
|
|
DisplayInfo {
|
|
x: d.origin().0 as _,
|
|
y: d.origin().1 as _,
|
|
width: d.width() as _,
|
|
height: d.height() as _,
|
|
name: display_name,
|
|
online: d.is_online(),
|
|
cursor_embedded: false,
|
|
original_resolution,
|
|
scale,
|
|
..Default::default()
|
|
}
|
|
})
|
|
.collect::<Vec<DisplayInfo>>();
|
|
SYNC_DISPLAYS.lock().unwrap().check_changed(&displays);
|
|
displays
|
|
}
|
|
|
|
pub fn is_inited_msg() -> Option<Message> {
|
|
#[cfg(target_os = "linux")]
|
|
if !is_x11() {
|
|
return super::wayland::is_inited();
|
|
}
|
|
None
|
|
}
|
|
|
|
// Return the primary index with the refreshed list so login cannot mix display snapshots.
|
|
pub async fn update_get_sync_displays_on_login() -> ResultType<(Vec<DisplayInfo>, usize)> {
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
if !is_x11() {
|
|
let (displays, primary_display_idx) =
|
|
super::wayland::get_displays_and_primary().await?;
|
|
let primary_display_idx =
|
|
normalize_primary_display_idx(primary_display_idx, displays.len());
|
|
return Ok((displays, primary_display_idx));
|
|
}
|
|
}
|
|
#[cfg(not(windows))]
|
|
let displays = display_service::try_get_displays();
|
|
#[cfg(windows)]
|
|
let displays = display_service::try_get_displays_add_amyuni_headless();
|
|
let displays = displays?;
|
|
let primary_display_idx = get_primary_2(&displays);
|
|
let sync_displays = update_sync_displays(&displays);
|
|
let primary_display_idx =
|
|
normalize_primary_display_idx(primary_display_idx, sync_displays.len());
|
|
Ok((sync_displays, primary_display_idx))
|
|
}
|
|
|
|
#[inline]
|
|
fn normalize_primary_display_idx(primary_display_idx: usize, display_len: usize) -> usize {
|
|
// Zero is the protocol fallback when the list is empty or its primary index is stale.
|
|
if primary_display_idx < display_len {
|
|
primary_display_idx
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn get_primary_2(all: &Vec<Display>) -> usize {
|
|
all.iter().position(|d| d.is_primary()).unwrap_or(0)
|
|
}
|
|
|
|
#[inline]
|
|
#[cfg(windows)]
|
|
fn no_displays(displays: &Vec<Display>) -> bool {
|
|
let display_len = displays.len();
|
|
if display_len == 0 {
|
|
true
|
|
} else if display_len == 1 {
|
|
let display = &displays[0];
|
|
if display.width() > DUMMY_DISPLAY_SIDE_MAX_SIZE
|
|
|| display.height() > DUMMY_DISPLAY_SIDE_MAX_SIZE
|
|
{
|
|
return false;
|
|
}
|
|
let any_real = crate::platform::resolutions(&display.name())
|
|
.iter()
|
|
.any(|r| {
|
|
(r.height as usize) > DUMMY_DISPLAY_SIDE_MAX_SIZE
|
|
|| (r.width as usize) > DUMMY_DISPLAY_SIDE_MAX_SIZE
|
|
});
|
|
!any_real
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
#[cfg(not(windows))]
|
|
pub fn try_get_displays() -> ResultType<Vec<Display>> {
|
|
Ok(Display::all()?)
|
|
}
|
|
|
|
#[inline]
|
|
#[cfg(windows)]
|
|
pub fn try_get_displays() -> ResultType<Vec<Display>> {
|
|
try_get_displays_(false)
|
|
}
|
|
|
|
// We can't get full control of the virtual display if we use amyuni idd.
|
|
// If we add a virtual display, we cannot remove it automatically.
|
|
// So when using amyuni idd, we only add a virtual display for headless if it is required.
|
|
// eg. when the client is connecting.
|
|
#[inline]
|
|
#[cfg(windows)]
|
|
pub fn try_get_displays_add_amyuni_headless() -> ResultType<Vec<Display>> {
|
|
try_get_displays_(true)
|
|
}
|
|
|
|
#[inline]
|
|
#[cfg(windows)]
|
|
pub fn try_get_displays_(add_amyuni_headless: bool) -> ResultType<Vec<Display>> {
|
|
let mut displays = Display::all()?;
|
|
|
|
// Do not add virtual display if the platform is not installed or the virtual display is not supported.
|
|
if !crate::platform::is_installed() || !virtual_display_manager::is_virtual_display_supported()
|
|
{
|
|
return Ok(displays);
|
|
}
|
|
|
|
// Enable headless virtual display when
|
|
// 1. `amyuni` idd is not used.
|
|
// 2. `amyuni` idd is used and `add_amyuni_headless` is true.
|
|
if virtual_display_manager::is_amyuni_idd() && !add_amyuni_headless {
|
|
return Ok(displays);
|
|
}
|
|
|
|
// The following code causes a bug.
|
|
// The virtual display cannot be added when there's no session(eg. when exiting from RDP).
|
|
// Because `crate::platform::desktop_changed()` always returns true at that time.
|
|
//
|
|
// The code only solves a rare case:
|
|
// 1. The control side is connecting.
|
|
// 2. The windows session is switching, no displays are detected, but they're there.
|
|
// Then the controlled side plugs in a virtual display for "headless".
|
|
//
|
|
// No need to do the following check. But the code is kept here for marking the issue.
|
|
// If there're someones reporting the issue, we may add a better check by waiting for a while. (switching session).
|
|
// But I don't think it's good to add the timeout check without any issue.
|
|
//
|
|
// If is switching session, no displays may be detected.
|
|
// if displays.is_empty() && crate::platform::desktop_changed() {
|
|
// return Ok(displays);
|
|
// }
|
|
|
|
let no_displays_v = no_displays(&displays);
|
|
if no_displays_v {
|
|
log::debug!("no displays, create virtual display");
|
|
if let Err(e) = virtual_display_manager::plug_in_headless() {
|
|
log::error!("plug in headless failed {}", e);
|
|
} else {
|
|
displays = Display::all()?;
|
|
}
|
|
}
|
|
Ok(displays)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::normalize_primary_display_idx;
|
|
|
|
#[test]
|
|
fn normalize_primary_display_idx_bounds() {
|
|
assert_eq!(normalize_primary_display_idx(0, 0), 0);
|
|
assert_eq!(normalize_primary_display_idx(0, 2), 0);
|
|
assert_eq!(normalize_primary_display_idx(1, 2), 1);
|
|
assert_eq!(normalize_primary_display_idx(2, 2), 0);
|
|
}
|
|
}
|