diff --git a/.github/scripts/tests/test_driver_linux_bootstrap.py b/.github/scripts/tests/test_driver_linux_bootstrap.py index e869e516ab..9678814e05 100644 --- a/.github/scripts/tests/test_driver_linux_bootstrap.py +++ b/.github/scripts/tests/test_driver_linux_bootstrap.py @@ -77,7 +77,7 @@ def test_apt_uses_only_snapshot_sources_and_full_dependencies(self) -> None: { "git", "ca-certificates", "curl", "python3", "build-essential", "pkg-config", "libx11-dev", "libxi-dev", "libxtst-dev", "libxext-dev", "libwayland-dev", - "libxkbcommon-dev", + "libxkbcommon-dev", "libxcb1-dev", }, ) self.assertLess(self.bootstrap.index("\nEOF\n"), self.bootstrap.index("apt-get ")) diff --git a/.github/workflows/cd-rust-cua-driver.yml b/.github/workflows/cd-rust-cua-driver.yml index cb74953ae9..f041e6a87f 100644 --- a/.github/workflows/cd-rust-cua-driver.yml +++ b/.github/workflows/cd-rust-cua-driver.yml @@ -193,7 +193,7 @@ jobs: apt-get -o Dir::Etc::sourceparts=- install -y --no-install-recommends \ git ca-certificates curl python3 build-essential pkg-config \ libx11-dev libxi-dev libxtst-dev libxext-dev libwayland-dev \ - libxkbcommon-dev + libxkbcommon-dev libxcb1-dev - uses: actions/checkout@v4 with: ref: ${{ inputs.source_ref || github.event_name == 'workflow_dispatch' && inputs.publish && format('refs/tags/cua-driver-rs-v{0}', inputs.version) || github.ref }} diff --git a/.github/workflows/ci-distro-compat-cua-driver.yml b/.github/workflows/ci-distro-compat-cua-driver.yml index 5b16181738..c852b441ee 100644 --- a/.github/workflows/ci-distro-compat-cua-driver.yml +++ b/.github/workflows/ci-distro-compat-cua-driver.yml @@ -161,7 +161,7 @@ jobs: matrix: include: # Debian family - # X11 runtime libs (libx11-6 libxi6 libxtst6 libxext6), the + # X11 runtime libs (libx11-6 libxi6 libxtst6 libxext6 libxcb1), the # Wayland client lib (libwayland-client0), and libxkbcommon0 are # required because # cua-driver is dynamically linked against the X11 input stack and @@ -176,15 +176,15 @@ jobs: - distro: "debian:12" image: "debian:12" glibc_version: "2.36" - pkg_install: "apt-get update -qq && apt-get install -y --no-install-recommends curl ca-certificates libx11-6 libxi6 libxtst6 libxext6 libwayland-client0 libxkbcommon0" + pkg_install: "apt-get update -qq && apt-get install -y --no-install-recommends curl ca-certificates libx11-6 libxi6 libxtst6 libxext6 libxcb1 libwayland-client0 libxkbcommon0" - distro: "ubuntu:22.04" image: "ubuntu:22.04" glibc_version: "2.35" - pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates libx11-6 libxi6 libxtst6 libxext6 libwayland-client0 libxkbcommon0" + pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates libx11-6 libxi6 libxtst6 libxext6 libxcb1 libwayland-client0 libxkbcommon0" - distro: "ubuntu:24.04" image: "ubuntu:24.04" glibc_version: "2.39" - pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates libx11-6 libxi6 libxtst6 libxext6 libwayland-client0 libxkbcommon0" + pkg_install: "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl ca-certificates libx11-6 libxi6 libxtst6 libxext6 libxcb1 libwayland-client0 libxkbcommon0" # RPM family # Rocky Linux 9 ships curl-minimal in the base image which conflicts # with the full curl package. Use --allowerasing to let dnf replace @@ -194,11 +194,11 @@ jobs: - distro: "rockylinux:9" image: "rockylinux:9" glibc_version: "2.34" - pkg_install: "dnf install -y --setopt=install_weak_deps=False --allowerasing curl ca-certificates libX11 libXi libXtst libXext libwayland-client libxkbcommon" + pkg_install: "dnf install -y --setopt=install_weak_deps=False --allowerasing curl ca-certificates libX11 libXi libXtst libXext libxcb libwayland-client libxkbcommon" - distro: "fedora:41" image: "fedora:41" glibc_version: "2.40" - pkg_install: "dnf install -y --setopt=install_weak_deps=False curl ca-certificates libX11 libXi libXtst libXext libwayland-client libxkbcommon" + pkg_install: "dnf install -y --setopt=install_weak_deps=False curl ca-certificates libX11 libXi libXtst libXext libxcb libwayland-client libxkbcommon" steps: - name: Skip if no release binary diff --git a/libs/cua-driver/rust/Cargo.lock b/libs/cua-driver/rust/Cargo.lock index 1a572080f5..39f3971305 100644 --- a/libs/cua-driver/rust/Cargo.lock +++ b/libs/cua-driver/rust/Cargo.lock @@ -175,6 +175,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + [[package]] name = "ashpd" version = "0.13.11" @@ -5892,7 +5898,9 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" dependencies = [ + "as-raw-xcb-connection", "gethostname", + "libc", "rustix", "x11rb-protocol", ] diff --git a/libs/cua-driver/rust/crates/platform-linux/Cargo.toml b/libs/cua-driver/rust/crates/platform-linux/Cargo.toml index f1a3d305b4..69785348ab 100644 --- a/libs/cua-driver/rust/crates/platform-linux/Cargo.toml +++ b/libs/cua-driver/rust/crates/platform-linux/Cargo.toml @@ -25,8 +25,9 @@ tiny-skia = { version = "0.11", default-features = false, features = ["std"] } [target.'cfg(target_os = "linux")'.dependencies] clipboard-rs = { version = "0.3.5", features = ["wayland"] } keyring = { version = "4.1.6", default-features = false, features = ["v1"] } -# X11 background input + window enumeration + MIT-SHM capture -x11rb = { version = "0.13", features = ["xinput", "randr", "xfixes", "composite", "shape", "xtest", "shm"] } +# X11 background input + window enumeration + MIT-SHM capture. `allow-unsafe-code` +# enables the libxcb-backed XCBConnection fallback for strict Xauthority setups. +x11rb = { version = "0.13", features = ["xinput", "randr", "xfixes", "composite", "shape", "xtest", "shm", "allow-unsafe-code"] } x11 = { version = "2.21", features = ["xlib", "xinput", "xtest"] } base64 = { workspace = true } image = { workspace = true } diff --git a/libs/cua-driver/rust/crates/platform-linux/src/health_report.rs b/libs/cua-driver/rust/crates/platform-linux/src/health_report.rs index d92013910f..c7b48599df 100644 --- a/libs/cua-driver/rust/crates/platform-linux/src/health_report.rs +++ b/libs/cua-driver/rust/crates/platform-linux/src/health_report.rs @@ -429,7 +429,11 @@ fn classify_wayland_backend( #[cfg(target_os = "linux")] fn probe_x11_connect() -> bool { + // Mirror the window-enumeration path (#1978): a connection counts as + // reachable if EITHER the pure-Rust client or the libxcb fallback + // connects, so the doctor verdict matches what `list_windows` can do. x11rb::rust_connection::RustConnection::connect(None).is_ok() + || x11rb::xcb_ffi::XCBConnection::connect(None).is_ok() } #[cfg(not(target_os = "linux"))] diff --git a/libs/cua-driver/rust/crates/platform-linux/src/x11/mod.rs b/libs/cua-driver/rust/crates/platform-linux/src/x11/mod.rs index 11f3b83588..3db5b37f3b 100644 --- a/libs/cua-driver/rust/crates/platform-linux/src/x11/mod.rs +++ b/libs/cua-driver/rust/crates/platform-linux/src/x11/mod.rs @@ -3,10 +3,11 @@ //! Uses _NET_CLIENT_LIST_STACKING to get the list of top-level windows, //! then reads WM_NAME/_NET_WM_NAME, _NET_WM_PID, and geometry per window. -use anyhow::Result; +use anyhow::{anyhow, Result}; use x11rb::connection::Connection; use x11rb::protocol::xproto::*; use x11rb::rust_connection::RustConnection; +use x11rb::xcb_ffi::XCBConnection; #[derive(Debug, Clone)] pub struct WindowInfo { @@ -40,7 +41,10 @@ pub fn window_belongs_to_pid(xid: u64, pid: u32) -> bool { let Ok(xid) = u32::try_from(xid) else { return false; }; - let Ok((conn, _)) = RustConnection::connect(None) else { + if let Ok((conn, _)) = RustConnection::connect(None) { + return window_owner_matches(get_window_pid(&conn, xid).ok().flatten(), pid); + } + let Ok((conn, _)) = XCBConnection::connect(None) else { return false; }; window_owner_matches(get_window_pid(&conn, xid).ok().flatten(), pid) @@ -51,27 +55,53 @@ fn window_owner_matches(owner: Option, requested_pid: u32) -> bool { } fn list_windows_inner(filter_pid: Option) -> Result> { - let (conn, screen_num) = RustConnection::connect(None)?; + match RustConnection::connect(None) { + Ok((conn, screen_num)) => enumerate_windows(&conn, screen_num, filter_pid), + Err(rust_err) => match XCBConnection::connect(None) { + Ok((conn, screen_num)) => enumerate_windows(&conn, screen_num, filter_pid), + Err(xcb_err) => Err(x11_connect_error(rust_err, xcb_err)), + }, + } +} + +fn x11_connect_error( + rust_err: impl std::fmt::Display, + xcb_err: impl std::fmt::Display, +) -> anyhow::Error { + anyhow!( + "X11 connect failed (DISPLAY={:?}, XAUTHORITY={:?}): rust-connection: {}; libxcb: {}", + std::env::var("DISPLAY").ok(), + std::env::var("XAUTHORITY").ok(), + rust_err, + xcb_err, + ) +} + +fn enumerate_windows( + conn: &C, + screen_num: usize, + filter_pid: Option, +) -> Result> { let screen = &conn.setup().roots[screen_num]; let root = screen.root; // Get _NET_CLIENT_LIST_STACKING (or fallback to _NET_CLIENT_LIST). - let windows = get_window_list(&conn, root)?; + let windows = get_window_list(conn, root)?; let mut result = Vec::new(); for (z_index, xid) in windows.into_iter().enumerate() { - let pid = get_window_pid(&conn, xid).ok().flatten(); + let pid = get_window_pid(conn, xid).ok().flatten(); if let Some(fp) = filter_pid { if pid != Some(fp) { continue; } } - let title = get_window_title(&conn, xid).unwrap_or_default(); + let title = get_window_title(conn, xid).unwrap_or_default(); if title.trim().is_empty() { continue; } - let app_name = get_window_class(&conn, xid) + let app_name = get_window_class(conn, xid) .map(|(instance, class)| if class.is_empty() { instance } else { class }) .unwrap_or_default(); let is_on_screen = conn @@ -113,7 +143,7 @@ fn z_index_from_bottom_to_top(position: usize) -> usize { position } -fn get_window_list(conn: &RustConnection, root: Window) -> Result> { +fn get_window_list(conn: &C, root: Window) -> Result> { let atom_names = ["_NET_CLIENT_LIST_STACKING", "_NET_CLIENT_LIST"]; for name in &atom_names { if let Ok(atom) = get_atom(conn, name) { @@ -157,7 +187,7 @@ fn fallback_window_is_listable(map_state: MapState) -> bool { map_state == MapState::VIEWABLE } -fn get_atom(conn: &RustConnection, name: &str) -> Result { +fn get_atom(conn: &C, name: &str) -> Result { Ok(conn.intern_atom(false, name.as_bytes())?.reply()?.atom) } @@ -241,7 +271,7 @@ fn moveresize_window_flags() -> u32 { STATIC_GRAVITY | X_PRESENT | Y_PRESENT | WIDTH_PRESENT | HEIGHT_PRESENT } -fn get_window_pid(conn: &RustConnection, window: Window) -> Result> { +fn get_window_pid(conn: &C, window: Window) -> Result> { let atom = get_atom(conn, "_NET_WM_PID")?; let reply = conn .get_property(false, window, atom, AtomEnum::CARDINAL, 0, 1)? @@ -249,7 +279,7 @@ fn get_window_pid(conn: &RustConnection, window: Window) -> Result> Ok(reply.value32().and_then(|mut i| i.next())) } -fn get_window_title(conn: &RustConnection, window: Window) -> Result { +fn get_window_title(conn: &C, window: Window) -> Result { // Try _NET_WM_NAME (UTF-8) first. if let Ok(atom) = get_atom(conn, "_NET_WM_NAME") { if let Ok(utf8_atom) = get_atom(conn, "UTF8_STRING") { @@ -281,11 +311,14 @@ fn get_window_title(conn: &RustConnection, window: Window) -> Result { /// Returns `None` when no X connection is available, the window has no /// WM_CLASS atom set, or the property could not be read. pub fn wm_class_for_window(xid: u64) -> Option<(String, String)> { - let (conn, _) = RustConnection::connect(None).ok()?; + if let Ok((conn, _)) = RustConnection::connect(None) { + return get_window_class(&conn, xid as u32); + } + let (conn, _) = XCBConnection::connect(None).ok()?; get_window_class(&conn, xid as u32) } -fn get_window_class(conn: &RustConnection, xid: Window) -> Option<(String, String)> { +fn get_window_class(conn: &C, xid: Window) -> Option<(String, String)> { let reply = conn .get_property( false, diff --git a/nix/cua-driver/package.nix b/nix/cua-driver/package.nix index 6a18a05a83..5c6352656a 100644 --- a/nix/cua-driver/package.nix +++ b/nix/cua-driver/package.nix @@ -49,7 +49,14 @@ pkgs.rustPlatform.buildRustPackage { cargoTestFlags = [ "-p" "cua-driver" "--features" "portal-input,portal-capture" ]; # Mostly pure Rust: - # x11rb -> RustConnection (no libxcb C binding) + # x11rb -> RustConnection (pure Rust) by default, but the + # `allow-unsafe-code` feature also compiles its `xcb_ffi` + # module (the libxcb-backed XCBConnection used as a fallback + # when strict Xauthority cookie matching fails — #1978). That + # module emits `cargo:rustc-link-lib=xcb`, so the binary now + # hard-links libxcb and the derivation must provide it + # (libxcb below); without it the link fails with `-lxcb` and + # no matching `-L` path. # ureq -> rustls (no openssl) # tiny-skia -> pure Rust 2D graphics # ring -> compiles own C/asm via stdenv's cc @@ -72,6 +79,9 @@ pkgs.rustPlatform.buildRustPackage { libxi libxtst libxext + # x11rb's `allow-unsafe-code` feature links the libxcb-backed + # XCBConnection fallback (#1978); the binary now needs libxcb at link time. + libxcb # Wayland-parity additions: PipeWire is needed by the portal # ScreenCast capture path (wayland::portal_screencast). pipewire # already pulls libspa transitively in nixpkgs. diff --git a/nix/cua-driver/tests/rust-unit.nix b/nix/cua-driver/tests/rust-unit.nix index 1aef6e580d..665a1af421 100644 --- a/nix/cua-driver/tests/rust-unit.nix +++ b/nix/cua-driver/tests/rust-unit.nix @@ -44,6 +44,7 @@ pkgs.rustPlatform.buildRustPackage { libxi libxtst libxext + libxcb pipewire libei ];