From b059550f0b9d354f0493004c45296c08c0e45fe4 Mon Sep 17 00:00:00 2001 From: roboomp Date: Thu, 30 Jul 2026 12:38:43 +0000 Subject: [PATCH] fix(natives): failed fast on uncapturable rootless xwayland root Under a rootless XWayland session (the GNOME/KDE/sway default) the X11 root window has no backing pixmap, so core `GetImage` on the root returns `BadMatch`. The computer tool advertised Wayland support yet failed every screenshot with a raw X11 protocol dump, and coordinate actions stayed gated behind a capture that could never succeed. - `Monitor::all` now probes a 1x1 root `GetImage` at initialization and, on a `Match`/`Drawable` error, fails fast with an actionable `DESKTOP_BACKEND_UNAVAILABLE` message naming the rootless-XWayland constraint via the new `root_capture_error` classifier. - `capture_image` routes its `GetImage` failure through the same classifier so any surviving path yields the actionable message rather than a raw protocol dump; unrelated errors stay verbatim. - Corrected the module doc premise and `docs/computer-use.md` to list rootless XWayland as unsupported (capture needs a rooted/rootful X server, which only exposes X11 clients). Fixes #7085 --- crates/pi-natives/src/desktop_x11.rs | 92 ++++++++++++++++++++++++++-- docs/computer-use.md | 11 ++-- packages/natives/CHANGELOG.md | 4 ++ 3 files changed, 98 insertions(+), 9 deletions(-) diff --git a/crates/pi-natives/src/desktop_x11.rs b/crates/pi-natives/src/desktop_x11.rs index 3fe1131ce..b505b7f80 100644 --- a/crates/pi-natives/src/desktop_x11.rs +++ b/crates/pi-natives/src/desktop_x11.rs @@ -4,9 +4,13 @@ //! the core addon acquires no C GUI `DT_NEEDED` entries (libxcb, libpipewire, //! libxkbcommon, libwayland) and keeps `dlopen` working on headless servers. //! Capture uses core `GetImage` over the root window, monitor enumeration uses -//! RandR 1.5 monitors, and input is synthesized with XTest — which also works -//! under XWayland, where XTest coordinates land in the same X11 global space -//! `GetImage` composites from. +//! RandR 1.5 monitors, and input is synthesized with XTest. XTest input works +//! under XWayland — its coordinates land in the same X11 global space a rooted +//! server composites from — but root `GetImage` only succeeds when the X server +//! owns a root pixmap (a real X11 server, Xvfb, or a rootful XWayland). The +//! default rootless XWayland (GNOME/KDE/sway) keeps no root pixmap, so the +//! capture probe in `Monitor::all` fails fast there instead of emitting a raw +//! protocol error on the first screenshot. //! //! The types mirror the names, variants, and call shapes of the enigo/xcap //! surface `crate::desktop` compiles against on macOS and Windows, so the @@ -418,7 +422,9 @@ mod x11 { use image::RgbaImage; use x11rb::{ connection::Connection, + errors::ReplyError, protocol::{ + ErrorKind, randr::ConnectionExt as _, xproto::{ BUTTON_PRESS_EVENT, BUTTON_RELEASE_EVENT, ConnectionExt as _, ImageFormat, ImageOrder, @@ -470,6 +476,31 @@ mod x11 { X11Error(format!("X11 request failed: {error}")) } + /// Actionable message for a root capture that failed because the X11 root is + /// not a readable drawable — the signature of a rootless XWayland session + /// (the GNOME/KDE/sway default), whose compositor keeps no X11 root pixmap. + /// Pure Wayland capture (portal/PipeWire) is not implemented, so such a + /// session has no usable capture path at all. + const ROOTLESS_XWAYLAND_CAPTURE: &str = + "X11 root window is not a readable drawable; this is a rootless XWayland session \ + (the GNOME/KDE/sway default) whose compositor keeps no X11 root pixmap, so screen \ + capture through XWayland is impossible, and pure Wayland capture (portal/PipeWire) is \ + not implemented"; + + /// Translate a failed root `GetImage` reply into a capture error. A + /// `Match`/`Drawable` protocol error means the root has no backing pixmap + /// (rootless XWayland) and gets [`ROOTLESS_XWAYLAND_CAPTURE`]; every other + /// failure is surfaced verbatim so genuine capture faults stay visible. + fn root_capture_error(error: &ReplyError) -> X11Error { + if let ReplyError::X11Error(x11) = error + && matches!(x11.error_kind, ErrorKind::Match | ErrorKind::Drawable) + { + X11Error(ROOTLESS_XWAYLAND_CAPTURE.to_string()) + } else { + X11Error(format!("X11 GetImage failed: {error}")) + } + } + fn input_request_error(error: impl std::fmt::Display) -> X11InputError { X11InputError(format!("X11 input request failed: {error}")) } @@ -533,6 +564,18 @@ mod x11 { blue: root_visual.blue_mask, }; color_components(color_masks, 32).map_err(X11Error)?; + // A rootless XWayland root window has no backing pixmap, so root + // `GetImage` fails with a Match error even though `DISPLAY` is set and + // RandR enumerates monitors. Probe a 1x1 read up front and fail fast + // with an actionable message instead of emitting a raw protocol dump + // on the first screenshot. + if let Err(error) = conn + .get_image(ImageFormat::Z_PIXMAP, root, 0, 0, 1, 1, !0) + .map_err(capture_error)? + .reply() + { + return Err(root_capture_error(&error)); + } let reply = conn .randr_get_monitors(root, true) .map_err(capture_error)? @@ -636,7 +679,7 @@ mod x11 { .get_image(ImageFormat::Z_PIXMAP, self.root, x, y, width, height, !0) .map_err(capture_error)? .reply() - .map_err(|error| X11Error(format!("X11 GetImage failed: {error}")))?; + .map_err(|error| root_capture_error(&error))?; let setup = self.conn.setup(); let format = setup .pixmap_formats @@ -909,6 +952,47 @@ mod x11 { }); } } + + #[cfg(test)] + mod capture_probe_tests { + use x11rb::errors::ReplyError; + use x11rb::protocol::ErrorKind; + use x11rb::x11_utils::X11Error as WireError; + + use super::{ROOTLESS_XWAYLAND_CAPTURE, root_capture_error}; + + fn wire(kind: ErrorKind) -> ReplyError { + ReplyError::X11Error(WireError { + error_kind: kind, + error_code: 8, + sequence: 6, + bad_value: 850, + minor_opcode: 0, + major_opcode: 73, + extension_name: None, + request_name: Some("GetImage"), + }) + } + + #[test] + fn match_and_drawable_map_to_actionable_rootless_message() { + for kind in [ErrorKind::Match, ErrorKind::Drawable] { + let message = root_capture_error(&wire(kind)).to_string(); + assert_eq!(message, ROOTLESS_XWAYLAND_CAPTURE); + assert!(message.contains("rootless XWayland")); + assert!(message.contains("not a readable drawable")); + // The raw protocol dump must not leak into the actionable guidance. + assert!(!message.contains("X11Error {")); + } + } + + #[test] + fn unrelated_protocol_errors_stay_verbatim() { + let message = root_capture_error(&wire(ErrorKind::Value)).to_string(); + assert!(message.starts_with("X11 GetImage failed:")); + assert!(message.contains("Value")); + } + } } #[cfg(target_os = "linux")] diff --git a/docs/computer-use.md b/docs/computer-use.md index d7a363372..fc8465901 100644 --- a/docs/computer-use.md +++ b/docs/computer-use.md @@ -144,7 +144,7 @@ Use one display when: - a layout gap makes targets ambiguous; or - you want to isolate sensitive content on another monitor. -On Linux, Wayland sessions are captured and driven through XWayland: `DISPLAY` must point at the XWayland server, capture reads the X11 composite, and input is emitted as XTest events in the same X11 global coordinate space, so multi-display coordinate mapping is exact. Whether that input reaches native Wayland windows depends on the compositor's XWayland input bridging (modern GNOME and KDE support it). +On Linux, capture reads the X11 root window with core `GetImage` and input is emitted as XTest events in the same X11 global coordinate space, so multi-display coordinate mapping is exact. This requires an X server that owns a readable root pixmap — a real X11 session, Xvfb, or a rootful XWayland (`Xwayland -rootful`). The default **rootless** XWayland used by GNOME, KDE, and sway keeps no X11 root pixmap, so root `GetImage` fails; the tool detects this at initialization and reports `DESKTOP_BACKEND_UNAVAILABLE` instead of failing on the first screenshot. Pure Wayland capture (portal/PipeWire) is not implemented. ## Approval and safety precedence @@ -194,7 +194,7 @@ See [Tool approval mode](./approval-mode.md) for general policy resolution. |---|---|---| | macOS x64/arm64 | Bounded macOS `screencapture` service capture; Quartz/CGEvent and native input | Supported. Grant Screen Recording and Accessibility. Real remote desktop execution was verified on Apple hardware; see [Verification boundary](#verification-boundary). | | Linux x64/arm64, glibc/musl, X11 | Pure-Rust X11 capture and XTest input (`x11rb`), bundled in the core addon | Supported when a graphical session and `DISPLAY` are available. No GUI system libraries are required; the backend speaks the X protocol directly over the display socket. Requires the RandR and XTEST server extensions. | -| Linux x64/arm64, glibc/musl, Wayland | XWayland capture; XTest input bridged by the compositor | Supported with an active XWayland `DISPLAY`. Pure Wayland capture (portal/PipeWire) is not implemented. Input delivery to native Wayland windows depends on the compositor's XWayland input bridge. | +| Linux x64/arm64, glibc/musl, Wayland | XWayland capture; XTest input bridged by the compositor | **Unsupported on the default rootless XWayland** (GNOME/KDE/sway): its root window has no readable pixmap, so root `GetImage` fails and the tool reports `DESKTOP_BACKEND_UNAVAILABLE` at initialization. Capture needs a rooted X server (a real X11 session, Xvfb, or a rootful `Xwayland -rootful`), which exposes only X11 clients — native Wayland windows are invisible to X11. Pure Wayland capture (portal/PipeWire) is not implemented. | | Windows x64 | xcap capture; Win32 virtual-desktop pointer movement and native input | Implemented, including negative origins and secondary monitors. Not remotely exercised in this feature's verification. | | Other OS/architectures | none | Unsupported by the published native package matrix. | @@ -214,8 +214,8 @@ For X11, run OMP inside the target graphical session and ensure `DISPLAY` identi For Wayland: -- keep XWayland enabled and ensure `DISPLAY` is set; capture and input both go through it; and -- use a compositor that bridges XWayland XTest input to native windows (modern GNOME and KDE do). +- capture goes through XWayland, which can only read a **rooted** X server's root pixmap; the default rootless XWayland (GNOME/KDE/sway) has none, so capture fails at initialization with `DESKTOP_BACKEND_UNAVAILABLE`; and +- even a rooted/rootful XWayland exposes only X11 clients — native Wayland windows are structurally invisible to X11 — and pure Wayland capture (portal/PipeWire) is not implemented, so Wayland desktops have no usable capture path today. The desktop backend is always bundled in the core `pi-natives` addon on every published Linux target (x64/arm64, glibc/musl). It opens no display connection until the tool runs, so headless hosts are unaffected; without a reachable X server the tool reports `DESKTOP_BACKEND_UNAVAILABLE`. @@ -269,6 +269,7 @@ Computer backend errors begin with a stable code: Common exact failures: - `Wayland sessions require an active XWayland DISPLAY for native capture and input; pure Wayland capture is unavailable` → enable XWayland or use X11. +- `X11 root window is not a readable drawable; this is a rootless XWayland session …` → the compositor keeps no X11 root pixmap (the GNOME/KDE/sway default), so no capture path exists on this session; use a native X11 session. Portal/PipeWire capture is not implemented. - `X11/x11rb XTest absolute input cannot represent negative global desktop coordinates` → select a display whose origin is non-negative. - `X11/x11rb XTest absolute input is limited to global coordinates in 0..=32767` → select one display or a smaller layout. - `native action deadline exceeded; remaining batch actions were not executed` → split the batch into smaller calls and take a fresh screenshot. @@ -288,7 +289,7 @@ The native composite safety ceiling is 268,435,456 pixels. Normal defaults are f - Coordinate targets are valid only for the preceding frame and current display layout. - Screenshot composites may downscale small text to fit configured limits. - Gaps are visible but not valid input targets; overlapping non-mirrored layouts fail closed. -- Pure Wayland capture currently requires XWayland; the portal/PipeWire capture path is not implemented. +- Wayland capture works only under a rooted/rootful XWayland that owns a readable root pixmap and exposes X11 clients; the default rootless XWayland (GNOME/KDE/sway) has no capturable root and the portal/PipeWire path is not implemented, so native Wayland desktops are unsupported for capture. - On Wayland, XTest input reaching native windows depends on the compositor's XWayland input bridge. - Linux coordinate input fails closed for negative global display origins; select a display whose origin is non-negative. - X11/XTest coordinate input is limited to global positions through 32767 on each axis. diff --git a/packages/natives/CHANGELOG.md b/packages/natives/CHANGELOG.md index 1b54462fb..7a05755ce 100644 --- a/packages/natives/CHANGELOG.md +++ b/packages/natives/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed the `computer` tool advertising Wayland support that never worked: on the default rootless XWayland (GNOME/KDE/sway) the X11 root window has no readable pixmap, so root `GetImage` failed on every screenshot with a raw `BadMatch` protocol dump. `Monitor::all` now probes root drawability at initialization and fails fast with an actionable `DESKTOP_BACKEND_UNAVAILABLE` message naming the rootless-XWayland constraint, and `docs/computer-use.md` now lists rootless XWayland as unsupported ([#7085](https://github.com/can1357/oh-my-pi/issues/7085)). + ## [17.2.0] - 2026-07-30 ### Changed