Merge PR #7086: fix(natives): failed fast on uncapturable rootless xwayland root (@roboomp)

This commit is contained in:
can1357
2026-07-30 16:14:03 +02:00
3 changed files with 98 additions and 9 deletions
+88 -4
View File
@@ -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")]
+6 -5
View File
@@ -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.
+4
View File
@@ -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