Merge PR #7086: fix(natives): failed fast on uncapturable rootless xwayland root (@roboomp)
This commit is contained in:
@@ -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")]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user