mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-14 00:11:01 +03:00
fix(cursor): isolate optional X11 density failures
This commit is contained in:
@@ -607,9 +607,7 @@ pub fn get_cursor() -> ResultType<Option<u64>> {
|
||||
}
|
||||
});
|
||||
#[cfg(feature = "flutter")]
|
||||
let res = res
|
||||
.map(|id| cursor::x11_scale().map(|scale| cursor::cache_id(id, scale)))
|
||||
.transpose()?;
|
||||
let res = res.map(cursor::x11_cursor_id);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -652,7 +650,7 @@ pub fn get_cursor_data(hcursor: u64) -> ResultType<CursorData> {
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "flutter")]
|
||||
let scale = cursor::x11_scale()?;
|
||||
let scale = cursor::x11_cursor_scale();
|
||||
#[cfg(feature = "flutter")]
|
||||
let matches = |id| cursor::cache_id(id, scale) == hcursor;
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use hbb_common::{anyhow::Context, bail, ResultType};
|
||||
use hbb_common::{anyhow::Context, bail, log, ResultType};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
cell::{Cell, RefCell},
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
@@ -8,8 +8,12 @@ use x11rb::{protocol::xproto::ConnectionExt, rust_connection::RustConnection, NO
|
||||
|
||||
mod xsettings;
|
||||
|
||||
#[cfg(test)]
|
||||
mod x11_tests;
|
||||
|
||||
thread_local! {
|
||||
static SETTINGS: RefCell<Option<(RustConnection, usize)>> = const { RefCell::new(None) };
|
||||
static X11_SCALE: Cell<Option<f64>> = const { Cell::new(Some(0.0)) };
|
||||
}
|
||||
|
||||
pub(super) fn cache_id(id: u64, scale: f64) -> u64 {
|
||||
@@ -21,6 +25,30 @@ pub(super) fn cache_id(id: u64, scale: f64) -> u64 {
|
||||
hash.finish()
|
||||
}
|
||||
|
||||
pub(super) fn x11_cursor_id(id: u64) -> u64 {
|
||||
let scale = X11_SCALE.with(|last| match x11_scale() {
|
||||
Ok(scale) => {
|
||||
last.set(Some(scale));
|
||||
scale
|
||||
}
|
||||
Err(err) => {
|
||||
// XSETTINGS is optional; warn once per failure streak without
|
||||
// turning valid XFixes cursor updates into service errors.
|
||||
if last.replace(None).is_some() {
|
||||
log::warn!("Failed to read XSETTINGS cursor density; using unknown density: {err}");
|
||||
}
|
||||
0.0
|
||||
}
|
||||
});
|
||||
cache_id(id, scale)
|
||||
}
|
||||
|
||||
pub(super) fn x11_cursor_scale() -> f64 {
|
||||
// The cursor service reads the ID and bitmap on the same thread. Reuse
|
||||
// that poll's density even if the settings manager changes between them.
|
||||
X11_SCALE.with(|last| last.get().unwrap_or(0.0))
|
||||
}
|
||||
|
||||
pub(super) fn x11_scale() -> ResultType<f64> {
|
||||
if !super::is_x11() {
|
||||
return Ok(0.0);
|
||||
|
||||
171
src/platform/linux/cursor/x11_tests.rs
Normal file
171
src/platform/linux/cursor/x11_tests.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use super::super::{get_cursor, get_cursor_data};
|
||||
use hbb_common::ResultType;
|
||||
use x11rb::{
|
||||
connection::Connection, protocol::xproto::*, rust_connection::RustConnection,
|
||||
wrapper::ConnectionExt as _, CURRENT_TIME,
|
||||
};
|
||||
|
||||
struct Settings {
|
||||
connection: RustConnection,
|
||||
owner: Window,
|
||||
property: Atom,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
fn new() -> ResultType<Self> {
|
||||
// This test replaces the settings manager; never run on a real desktop.
|
||||
assert_eq!(
|
||||
std::env::var("RUSTDESK_X11_CURSOR_TEST").as_deref(),
|
||||
Ok("1")
|
||||
);
|
||||
let (connection, screen) = x11rb::connect(None)?;
|
||||
let selection = connection
|
||||
.intern_atom(false, format!("_XSETTINGS_S{screen}").as_bytes())?
|
||||
.reply()?
|
||||
.atom;
|
||||
let property = connection
|
||||
.intern_atom(false, b"_XSETTINGS_SETTINGS")?
|
||||
.reply()?
|
||||
.atom;
|
||||
let owner = connection.generate_id()?;
|
||||
connection
|
||||
.create_window(
|
||||
0,
|
||||
owner,
|
||||
connection.setup().roots[screen].root,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
WindowClass::INPUT_OUTPUT,
|
||||
0,
|
||||
&CreateWindowAux::new(),
|
||||
)?
|
||||
.check()?;
|
||||
connection
|
||||
.set_selection_owner(owner, selection, CURRENT_TIME)?
|
||||
.check()?;
|
||||
Ok(Self {
|
||||
connection,
|
||||
owner,
|
||||
property,
|
||||
})
|
||||
}
|
||||
|
||||
fn property(&self, bytes: &[u8]) {
|
||||
self.connection
|
||||
.change_property8(
|
||||
PropMode::REPLACE,
|
||||
self.owner,
|
||||
self.property,
|
||||
self.property,
|
||||
bytes,
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn scale(&self, value: i32) {
|
||||
const ALIGNMENT: usize = 4;
|
||||
let name = b"Gdk/WindowScalingFactor";
|
||||
let mut bytes = vec![0, 0, 0, 0];
|
||||
bytes.extend(1_u32.to_le_bytes()); // Serial.
|
||||
bytes.extend(1_u32.to_le_bytes()); // One integer setting.
|
||||
bytes.extend([0, 0]);
|
||||
bytes.extend((name.len() as u16).to_le_bytes());
|
||||
bytes.extend(name);
|
||||
bytes.resize(bytes.len().div_ceil(ALIGNMENT) * ALIGNMENT, 0);
|
||||
bytes.extend(1_u32.to_le_bytes());
|
||||
bytes.extend(value.to_le_bytes());
|
||||
self.property(&bytes);
|
||||
}
|
||||
|
||||
fn arrow(&self) {
|
||||
const LEFT_PTR: u16 = 68;
|
||||
const WHITE: u16 = u16::MAX;
|
||||
let font = self.connection.generate_id().unwrap();
|
||||
let cursor = self.connection.generate_id().unwrap();
|
||||
self.connection
|
||||
.open_font(font, b"cursor")
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
self.connection
|
||||
.create_glyph_cursor(
|
||||
cursor,
|
||||
font,
|
||||
font,
|
||||
LEFT_PTR,
|
||||
LEFT_PTR + 1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
WHITE,
|
||||
WHITE,
|
||||
WHITE,
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
self.connection
|
||||
.change_window_attributes(
|
||||
self.connection.setup().roots[0].root,
|
||||
&ChangeWindowAttributesAux::new().cursor(cursor),
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor(scale: f64) -> u64 {
|
||||
let id = get_cursor().unwrap().expect("Xvfb must have a cursor");
|
||||
let data = get_cursor_data(id).unwrap();
|
||||
assert_eq!((data.id, data.scale), (id, scale));
|
||||
assert!(data.width > 0 && data.height > 0 && !data.colors.is_empty());
|
||||
id
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires isolated Xvfb and RUSTDESK_FORCED_DISPLAY_SERVER=x11"]
|
||||
fn x11_metadata_errors_preserve_cursor_delivery_and_recover() {
|
||||
let settings = Settings::new().unwrap();
|
||||
let unknown = cursor(0.0); // Selection owner exists but has no property.
|
||||
settings.scale(2);
|
||||
let known = cursor(2.0);
|
||||
assert_ne!(known, unknown);
|
||||
settings.property(&[0]); // Truncated header.
|
||||
assert_eq!(cursor(0.0), unknown);
|
||||
settings.scale(0); // Invalid density.
|
||||
assert_eq!(cursor(0.0), unknown);
|
||||
settings.scale(2);
|
||||
assert_eq!(cursor(2.0), known);
|
||||
settings.property(&[0]);
|
||||
settings.arrow();
|
||||
assert_ne!(cursor(0.0), unknown); // New shapes still arrive during failure.
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires isolated Xvfb and RUSTDESK_FORCED_DISPLAY_SERVER=x11"]
|
||||
fn x11_metadata_change_between_id_and_bitmap_keeps_snapshot() {
|
||||
let settings = Settings::new().unwrap();
|
||||
settings.scale(2);
|
||||
let id = get_cursor().unwrap().unwrap();
|
||||
settings
|
||||
.connection
|
||||
.destroy_window(settings.owner)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
let data = get_cursor_data(id).unwrap();
|
||||
assert_eq!((data.id, data.scale), (id, 2.0));
|
||||
let unknown = get_cursor().unwrap().unwrap();
|
||||
assert_ne!(unknown, id);
|
||||
let recovered = Settings::new().unwrap();
|
||||
recovered.scale(2);
|
||||
let data = get_cursor_data(unknown).unwrap();
|
||||
assert_eq!((data.id, data.scale), (unknown, 0.0));
|
||||
assert_eq!(cursor(2.0), id);
|
||||
}
|
||||
Reference in New Issue
Block a user