mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-08 05:20:59 +03:00
fix: refresh wayland uinput range on compositor layout change (#15628)
* 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>
This commit is contained in:
@@ -28,6 +28,144 @@ lazy_static::lazy_static! {
|
||||
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);
|
||||
|
||||
@@ -231,6 +369,12 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> {
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
@@ -661,20 +661,22 @@ pub async fn setup_rdp_input() -> ResultType<(), Box<dyn std::error::Error>> {
|
||||
pub async fn update_mouse_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> {
|
||||
set_uinput_resolution(minx, maxx, miny, maxy).await?;
|
||||
|
||||
std::thread::spawn(|| {
|
||||
// Confirm the device adopted the new range before the caller caches it.
|
||||
// spawn_blocking because ENIGO is a std Mutex and send_refresh blocks on IPC.
|
||||
tokio::task::spawn_blocking(move || {
|
||||
if let Some(mouse) = ENIGO.lock().unwrap().get_custom_mouse() {
|
||||
if let Some(mouse) = mouse
|
||||
.as_mut_any()
|
||||
.downcast_mut::<super::uinput::client::UInputMouse>()
|
||||
{
|
||||
allow_err!(mouse.send_refresh());
|
||||
} else {
|
||||
log::error!("failed downcast uinput mouse");
|
||||
return mouse.send_refresh();
|
||||
}
|
||||
bail!("failed to downcast custom mouse to UInputMouse");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
// No custom mouse: nothing to refresh.
|
||||
Ok(())
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -1098,12 +1100,23 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
|
||||
MOUSE_TYPE_MOVE => {
|
||||
// Switching back to absolute movement implicitly disables relative mouse mode.
|
||||
set_relative_mouse_active(conn, false);
|
||||
en.mouse_move_to(evt.x, evt.y);
|
||||
// On Wayland with uinput, the client sends coordinates in the layout it was
|
||||
// told at session init. If the compositor has since moved a monitor, correct
|
||||
// them onto the current layout. https://github.com/rustdesk/rustdesk/issues/15601
|
||||
#[cfg(target_os = "linux")]
|
||||
let (mx, my) = if wayland_use_uinput() {
|
||||
super::display_service::remap_wayland_uinput_coord(evt.x, evt.y)
|
||||
} else {
|
||||
(evt.x, evt.y)
|
||||
};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let (mx, my) = (evt.x, evt.y);
|
||||
en.mouse_move_to(mx, my);
|
||||
*LATEST_PEER_INPUT_CURSOR.lock().unwrap() = Input {
|
||||
conn,
|
||||
time: get_time(),
|
||||
x: evt.x,
|
||||
y: evt.y,
|
||||
x: mx,
|
||||
y: my,
|
||||
};
|
||||
}
|
||||
// MOUSE_TYPE_MOVE_RELATIVE: Relative mouse movement for gaming/3D applications.
|
||||
|
||||
@@ -130,7 +130,16 @@ pub mod client {
|
||||
}
|
||||
|
||||
pub fn send_refresh(&mut self) -> ResultType<()> {
|
||||
self.send(Data::Mouse(DataMouse::Refresh))
|
||||
self.rt
|
||||
.block_on(self.conn.send(&Data::Mouse(DataMouse::Refresh)))?;
|
||||
// Wait for the service to confirm it recreated the device, so a
|
||||
// failed refresh is distinguishable from a good one.
|
||||
match self.rt.block_on(self.conn.next_timeout(IPC_REQUEST_TIMEOUT)) {
|
||||
Ok(Some(Data::Empty)) => Ok(()),
|
||||
Ok(Some(resp)) => bail!("unexpected uinput mouse refresh response: {:?}", &resp),
|
||||
Ok(None) => bail!("uinput mouse refresh failed, connection closed"),
|
||||
Err(e) => bail!("uinput mouse refresh timeout {}, {}", IPC_REQUEST_TIMEOUT, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,9 +860,10 @@ pub mod service {
|
||||
match data {
|
||||
Data::Mouse(data) => {
|
||||
if let DataMouse::Refresh = data {
|
||||
let resolution = RESOLUTION.lock().unwrap();
|
||||
let rng_x = resolution.0.clone();
|
||||
let rng_y = resolution.1.clone();
|
||||
let (rng_x, rng_y) = {
|
||||
let resolution = RESOLUTION.lock().unwrap();
|
||||
(resolution.0.clone(), resolution.1.clone())
|
||||
};
|
||||
log::info!(
|
||||
"Refresh uinput mouce with rng_x: ({}, {}), rng_y: ({}, {})",
|
||||
rng_x.0,
|
||||
@@ -861,11 +871,19 @@ pub mod service {
|
||||
rng_y.0,
|
||||
rng_y.1
|
||||
);
|
||||
mouse = match mouce::UInputMouseManager::new(rng_x, rng_y) {
|
||||
Ok(mouse) => mouse,
|
||||
match mouce::UInputMouseManager::new(rng_x, rng_y) {
|
||||
Ok(m) => {
|
||||
mouse = m;
|
||||
// Ack: device adopted the new range.
|
||||
allow_err!(stream.send(&Data::Empty).await);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to create mouse, {}", e);
|
||||
return;
|
||||
// Keep the current device; withhold the ack
|
||||
// so the client times out and retries.
|
||||
log::error!(
|
||||
"Failed to recreate uinput mouse, keeping current: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -137,6 +137,9 @@ pub(super) async fn check_init() -> ResultType<()> {
|
||||
if !is_x11() {
|
||||
if CAP_DISPLAY_INFO.read().unwrap().is_empty() {
|
||||
if crate::input_service::wayland_use_uinput() {
|
||||
// The cached layout may predate compositor changes made while no session
|
||||
// was active, https://github.com/rustdesk/rustdesk/issues/15601
|
||||
scrap::wayland::display::clear_wayland_displays_cache();
|
||||
if let Some((minx, maxx, miny, maxy)) =
|
||||
scrap::wayland::display::get_desktop_rect_for_uinput()
|
||||
{
|
||||
@@ -147,9 +150,28 @@ pub(super) async fn check_init() -> ResultType<()> {
|
||||
miny,
|
||||
maxy
|
||||
);
|
||||
allow_err!(
|
||||
input_service::update_mouse_resolution(minx, maxx, miny, maxy).await
|
||||
);
|
||||
// Bound the IPC wait like the periodic refresh does, so a hung
|
||||
// response can't stall session init.
|
||||
match timeout(
|
||||
3_000,
|
||||
input_service::update_mouse_resolution(minx, maxx, miny, maxy),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(())) => {
|
||||
super::display_service::set_wayland_uinput_rect((
|
||||
minx, maxx, miny, maxy,
|
||||
));
|
||||
// Snapshot the per-display layout the client's coordinates
|
||||
// will be based on, so the mouse path can correct them if
|
||||
// the compositor moves a monitor mid-session.
|
||||
super::display_service::set_wayland_layout_baseline(
|
||||
scrap::wayland::display::get_display_rects_for_uinput(),
|
||||
);
|
||||
}
|
||||
Ok(Err(err)) => log::error!("Failed to update mouse resolution: {}", err),
|
||||
Err(err) => log::error!("Failed to update mouse resolution: {}", err),
|
||||
}
|
||||
} else {
|
||||
log::warn!("Failed to get desktop rect for uinput");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user