From 722f6b45bc43db26b4d8541803164143845bb871 Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Tue, 23 Jun 2026 01:56:39 -0700 Subject: [PATCH 1/7] fix(cua-driver/linux): libxcb fallback for X11 connect so list_windows works where xdotool does (#1978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit x11rb's pure-Rust RustConnection::connect does strict Xauthority family/address cookie matching and returns Err in some setups (XFCE/lightdm, hostname-mismatched auth entries) where libxcb-based tools (xdotool, pyatspi) connect fine. list_windows then silently returned 0 windows and the health report said "X11 is not reachable", despite a working X session. - Try RustConnection first, fall back to the libxcb XCBConnection (enabled via x11rb allow-unsafe-code) — same client lib xdotool/pyatspi use. - Make the window-enumeration helpers generic over the connection type. - When BOTH connects fail, surface a descriptive error (DISPLAY/XAUTHORITY + both underlying errors) instead of collapsing to an empty list. - Mirror the fallback in the doctor's probe_x11_connect so the verdict matches. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01KMXCW4M5uK1HRGjjH4wueZ --- .../rust/crates/platform-linux/Cargo.toml | 5 +- .../platform-linux/src/health_report.rs | 4 ++ .../rust/crates/platform-linux/src/x11/mod.rs | 54 ++++++++++++++----- 3 files changed, 49 insertions(+), 14 deletions(-) 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..14e686957b 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 { @@ -51,27 +52,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 +140,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 +184,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 +268,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 +276,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 +308,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, From 7ed727e8250928f1d227c43a5c2572e03446ef26 Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Tue, 23 Jun 2026 19:21:11 -0700 Subject: [PATCH 2/7] fix(cua-driver/linux): vendor as-raw-xcb-connection in Cargo.lock for x11rb xcb-ffi (#1978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enabling the x11rb `allow-unsafe-code` feature pulls in the `xcb_ffi` module, which depends on `as-raw-xcb-connection` (+ libc) — crates that were not in the committed Cargo.lock. The earlier VM build that validated this change ran online, so cargo fetched the new crate transparently; CI's Nix build runs `cargo build --offline` against a vendored dir and failed with "no matching package named `as-raw-xcb-connection` found", breaking every Linux integration job (build-time, before any test ran). Add the package + the two new x11rb dependency edges to Cargo.lock so the offline/vendored build resolves. Minimal in-place update (cargo metadata), no unrelated version churn. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01KMXCW4M5uK1HRGjjH4wueZ --- libs/cua-driver/rust/Cargo.lock | 8 ++++++++ 1 file changed, 8 insertions(+) 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", ] From 4075e783a68901569205414a0ff4a79842e8ed08 Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Tue, 23 Jun 2026 19:36:39 -0700 Subject: [PATCH 3/7] fix(cua-driver/linux): link libxcb in nix + CD builds for x11rb xcb-ffi (#1978) The x11rb `allow-unsafe-code` feature compiles its `xcb_ffi` module (the libxcb-backed XCBConnection fallback for strict-Xauthority setups, #1978), which emits `cargo:rustc-link-lib=xcb`. The cua-driver binary therefore now hard-links libxcb, but none of the build manifests provided it: - nix/cua-driver/package.nix: link failed with `-lxcb` and no matching `-L` ("collect2: ld returned 1 exit status") in every nix VM test. Add `libxcb` to buildInputs; update the stale "x11rb -> no libxcb C binding" comment. - cd-rust-cua-driver.yml: the debian:11 release build would fail the same way. Add `libxcb1-dev`. - ci-distro-compat-cua-driver.yml: the released binary now needs the libxcb runtime lib at load time; add `libxcb1` (deb) / `libxcb` (rpm) to every distro's runtime install so --version doesn't exit 127 on an ABI error. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01KMXCW4M5uK1HRGjjH4wueZ --- .github/workflows/cd-rust-cua-driver.yml | 2 +- .github/workflows/ci-distro-compat-cua-driver.yml | 12 ++++++------ nix/cua-driver/package.nix | 12 +++++++++++- 3 files changed, 18 insertions(+), 8 deletions(-) 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/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. From 2184d9adb3c09d4a20f0bf2a235998608f3d322e Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Sun, 13 Sep 2026 04:17:12 -0500 Subject: [PATCH 4/7] fix(cua-driver/linux): use XCB fallback for X11 ownership checks Apply the same connection fallback when get_window_state validates an X11 window owner, so a strict Xauthority mismatch cannot make a freshly listed target look stale. Co-authored-by: Claude Opus 4.8 --- libs/cua-driver/rust/crates/platform-linux/src/x11/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 14e686957b..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 @@ -41,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) From af96582337e3f6c9ba6b655caad0233e4d5052d3 Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Sun, 13 Sep 2026 04:23:56 -0500 Subject: [PATCH 5/7] test(cua-driver): include libxcb in bootstrap contract --- .github/scripts/tests/test_driver_linux_bootstrap.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/scripts/tests/test_driver_linux_bootstrap.py b/.github/scripts/tests/test_driver_linux_bootstrap.py index e869e516ab..e08eda28af 100644 --- a/.github/scripts/tests/test_driver_linux_bootstrap.py +++ b/.github/scripts/tests/test_driver_linux_bootstrap.py @@ -77,7 +77,8 @@ 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 ")) From 39bfeae81af37d3dfd7f1841659dc2f5141f9ad3 Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Sun, 13 Sep 2026 04:24:14 -0500 Subject: [PATCH 6/7] style(cua-driver): align bootstrap dependency set --- .github/scripts/tests/test_driver_linux_bootstrap.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/scripts/tests/test_driver_linux_bootstrap.py b/.github/scripts/tests/test_driver_linux_bootstrap.py index e08eda28af..9678814e05 100644 --- a/.github/scripts/tests/test_driver_linux_bootstrap.py +++ b/.github/scripts/tests/test_driver_linux_bootstrap.py @@ -77,8 +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", - "libxcb1-dev", + "libxkbcommon-dev", "libxcb1-dev", }, ) self.assertLess(self.bootstrap.index("\nEOF\n"), self.bootstrap.index("apt-get ")) From 5ec39b0b3cb136e053b6007d9b5bd5434049b74d Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Sun, 13 Sep 2026 04:45:04 -0500 Subject: [PATCH 7/7] fix(cua-driver): link libxcb in nix rust unit tests --- nix/cua-driver/tests/rust-unit.nix | 1 + 1 file changed, 1 insertion(+) 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 ];