mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-04 00:33:12 +00:00
* fix: refresh wayland uinput range on compositor layout change The uinput absolute range is computed once at session init. If the compositor layout changes mid-session (monitor scale or position change, or a portal virtual output appearing once capture starts), injected coordinates are rescaled by the stale range and land offset. Poll the live desktop bounding box from the display service loop while subscribed (one wayland roundtrip, throttled to 1.5s, no subprocesses) and re-apply the uinput resolution when it changes. Also read a fresh layout when computing the initial range in check_init, since the cache is not cleared when a session closes through the restore-token path. This is the X component of #15601. The stale advertised origins (the Y component) are not touched here: re-advertising DisplayInfo mid-session trips the portal re-negotiation and can drop displays. Signed-off-by: Cody Harris <codyharris7188@gmail.com> * fix: bound the mouse resolution IPC wait during session init Wrap update_mouse_resolution in the same 3s timeout the periodic refresh uses, so a hung IPC response can't stall check_init. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: build timeout future inside runtime, split linux lazy_static Constructing the timeout future eagerly as the block_on argument panics with 'there is no reactor running'; move it into the async block so it is built inside the runtime context. Also move WAYLAND_UINPUT_RECT into its own cfg-gated lazy_static block, an attribute on a single item inside the shared block does not compile. * fix: confirm uinput mouse device adopted new range before caching rect send_refresh() now waits for the mouse service to ack that it recreated the device with the new range instead of firing and forgetting, and update_mouse_resolution() propagates that result. The layout poller only caches the rect after the device actually adopts the range, so a failed refresh errors and retries on the next check. The ack read is bounded by IPC_REQUEST_TIMEOUT, matching the keyboard get-key-state path. * fix: propagate refresh failures instead of caching a stale range - input_service: error when the custom-mouse downcast fails so the poller retries instead of caching an unconfirmed refresh - uinput: on device recreation failure, keep the current device and the IPC connection and withhold the ack so the client retries, instead of killing the mouse handler * fix: remap injected wayland coords onto the live layout after a monitor moves The range refresh corrects the uinput ABS bounds, but a single-display client sends whole-desktop coordinates offset by the origin of the display it follows, taken from the layout advertised at session init. When another monitor is rescaled or moved that origin shifts, so the coordinate lands offset before it reaches uinput and the range refresh cannot recover it. Snapshot the per-display layout at init, poll the live layout on the existing 1.5s throttle, and when they differ remap each injected move into the followed display's current rectangle (matched by connector name, index fallback when the compositor reports none). No-op and lock-free while the layout is unchanged. --------- Signed-off-by: Cody Harris <codyharris7188@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
645 lines
22 KiB
Rust
645 lines
22 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);
|
|
}
|
|
|
|
#[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;
|
|
}
|
|
{
|
|
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() {
|
|
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()
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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()
|
|
&& 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);
|
|
}
|
|
}
|