Skip to content

Commit 8575606

Browse files
authored
linux: add an XCB-based backend (#426)
* Working implementation of XCB-based screenshots This version works fine, but needs improvements, especially to the testing. What has happened: * A ctypes-based module to use XCB has been added. * The mss.linux.MSS constructor is now a factory, which takes an optional `backend` keyword parameter. * The previous mss.linux implementation has now been designated as the "xlib" backend. Per #425, this will have "legacy" status, and new functionality like XShmGetImage won't be implemented there. * A new XCB-based backend, named "getimage", has been added. This is functionally the same as the existing "xlib" backend, in almost all respects, but has a different implementation. * The GNU/Linux-specific tests that could easily be made to work with the new backend have been adjusted. Notable improvement: I previously was seeing periodic thread failures in `test_thread_safety`. In one scenario (Docker container, Python 3.9, Xvfb), I tested 84% failures in `test_thread_safety`. With the XCB-based backend, I saw no failures. (See also #251 and the test failure I mentioned in #421.) The performance is, in my brief testing, the same as the existing xlib implementation. This PR is about making clearer code and improving thread safety, to pave the way for future improvements. What's left to do (some before merging, some possibly later): * The way the libraries are initialized needs to be rethought; I'll want to talk with @BoboTiG about that. * Many tests in `test_gnu_linux` used monkey-patches that are specific to the Xlib implementation. I've arranged for those to only use the Xlib implementation; I need to add corresponding tests for the XCB implementation. * The tests outside of `test_gnu_linux` need to test both backends, as well as the different backends we may add to other OSs. I need to add the right Pytest code to parameterize these. * This code introduces new automatic memory management aspects that need tests assigned to them. * The bulk of the `xcb.py` code is just hand-translations of the [XCB protocol specification XML files](https://gitlab.freedesktop.org/xorg/proto/xcbproto). Hand-translation can be error-prone (see #418). Since the XCB specs are in XML, then the bulk of this code can be auto-generated, like xcffib does. This can be done manually as needed, before shipping the distribution, so end users won't have to do this. (See #425 for a discussion of why we're not using xcffib.) * Even if we don't auto-generate the code, some of the repetitive parts can still be hoisted into higher-order functional programming, to reduce the risk of making errors in copying. * There are some parts that I worsened, to satisfy ruff. I want to revisit those. * The proof-of-concept code in `xcb.py:main` includes some additional functionality should be integrated into the MSS class, either in this PR or in separate branches. We also should delete that code before merging. * Additional testing on the X11 visual, to make sure it's really in the format we expect. (This is probably the only part we'll merge in with this PR.) * Support for capturing an individual window, without needing its location. * Identifying whether the alpha channel is meaningful or not. (This is only relevant with individual-window capture; there aren't many screens that can turn transparent!) Fixes: #425 * Improve test coverage Some tests now take a "backend" parameter. A fixture will iterate that through the backends implemented for that OS. Many tests called mss(display=os.getenv("DISPLAY")). Rather than add "backend" parameters to each of them, and maybe need to make other changes in the future, these now take an "mss_impl" fixture. This is a callable that will return the appropriate MSS object. One reason for this is so that we might add a pyvirtualdisplay or similar mechanism to these functions, when it's available. This may enable some tests to run in conditions they might not otherwise. It also may help standardize some of the testing. * Ruff cleanups * Small bug fixes to get the tests back to doing meaningful testing * XCB: Verify visual, and improve error messages The MSS object, when it's created, will now ensure that the X11 visual's format is exactly what we think it is: 32bpp, as a 24-bit BGRx or 32-bit BGRA image, in that order, with no extra scanline padding, color indexing, etc. This is true of all modern X servers in real-world situations, but it's not guaranteed. For instance, VNC-based servers can be run in depth 16 with RGB565 formats. The error messages for protocol-level errors (such as bad coordinates) are only available if the xcb-util-errors library is installed. (That's libxcb-errors.so.0, in the libxcb-errors0 package on Debian/Ubuntu.) This isn't widely installed, and is mostly useful to developers, so it's optional. The connection-level errors (like the hint to check $DISPLAY) are available everywhere. * Move the library globals to a smart container class * Improve testing Test the cases of missing libraries when using XCB. Add fixtures to reset the XCB library loader after each testcase. Add fixtures to check for Xlib errors after each testcase. * Use generated code to interface with XCB instead of hand-written code * Fix bug in multithreaded initialization guard * Improve testing and comments * Add src/xcbproto/README.md * Fix distribution list to include README.md
1 parent 2dfc726 commit 8575606

29 files changed

+11323
-152
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ See Git checking messages for full history.
55
## 10.1.1.dev0 (2025-xx-xx)
66
- Linux: check the server for Xrandr support version (#417)
77
- Linux: improve typing and error messages for X libraries (#418)
8+
- Linux: add a new XCB backend for better thread safety, error-checking, and future development (#425)
89
- :heart: contributors: @jholveck
910

1011
## 10.1.0 (2025-08-16)

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ mss = "mss.__main__:main"
7272
[project.optional-dependencies]
7373
dev = [
7474
"build==1.3.0",
75+
"lxml==6.0.2",
7576
"mypy==1.18.2",
7677
"ruff==0.14.5",
7778
"twine==6.2.0",
@@ -134,6 +135,7 @@ strict_equality = true
134135

135136
[tool.pytest.ini_options]
136137
pythonpath = "src"
138+
markers = ["without_libraries"]
137139
addopts = """
138140
--showlocals
139141
--strict-markers
@@ -187,3 +189,6 @@ ignore = [
187189
"S607", # `subprocess` call without explicit paths
188190
"SLF001", # private member accessed
189191
]
192+
193+
[tool.ruff.per-file-target-version]
194+
"src/xcbproto/*" = "py312"

src/mss/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def __init__(
4040
self,
4141
/,
4242
*,
43+
backend: str = "default",
4344
compression_level: int = 6,
4445
with_cursor: bool = False,
4546
# Linux only
@@ -51,6 +52,11 @@ def __init__(
5152
self.compression_level = compression_level
5253
self.with_cursor = with_cursor
5354
self._monitors: Monitors = []
55+
# If there isn't a factory that removed the "backend" argument, make sure that it was set to "default".
56+
# Factories that do backend-specific dispatch should remove that argument.
57+
if backend != "default":
58+
msg = 'The only valid backend on this platform is "default".'
59+
raise ScreenShotError(msg)
5460

5561
def __enter__(self) -> MSSBase: # noqa:PYI034
5662
"""For the cool call `with MSS() as mss:`."""

src/mss/linux/__init__.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Any
2+
3+
from mss.base import MSSBase
4+
from mss.exception import ScreenShotError
5+
6+
7+
def mss(backend: str = "default", **kwargs: Any) -> MSSBase:
8+
backend = backend.lower()
9+
if backend in {"default", "xlib"}:
10+
from . import xlib # noqa: PLC0415
11+
12+
return xlib.MSS(**kwargs)
13+
if backend == "xgetimage":
14+
from . import xgetimage # noqa: PLC0415
15+
16+
return xgetimage.MSS(**kwargs)
17+
msg = f"Backend {backend!r} not (yet?) implemented."
18+
raise ScreenShotError(msg)
19+
20+
21+
# Alias in upper-case for backward compatibility. This is a supported name in the docs.
22+
def MSS(*args, **kwargs) -> MSSBase: # type: ignore[no-untyped-def] # noqa: N802, ANN002, ANN003
23+
return mss(*args, **kwargs)

src/mss/linux/xcb.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
from __future__ import annotations
2+
3+
from ctypes import Structure, c_int, c_uint8, c_uint16, c_uint32
4+
5+
from . import xcbgen
6+
7+
# We import these just so they're re-exported to our users.
8+
# ruff: noqa: F401
9+
from .xcbgen import (
10+
RANDR_MAJOR_VERSION,
11+
RANDR_MINOR_VERSION,
12+
RENDER_MAJOR_VERSION,
13+
RENDER_MINOR_VERSION,
14+
XFIXES_MAJOR_VERSION,
15+
XFIXES_MINOR_VERSION,
16+
Atom,
17+
BackingStore,
18+
Colormap,
19+
Depth,
20+
DepthIterator,
21+
Drawable,
22+
Format,
23+
GetGeometryReply,
24+
GetImageReply,
25+
GetPropertyReply,
26+
ImageFormat,
27+
ImageOrder,
28+
Keycode,
29+
Pixmap,
30+
RandrCrtc,
31+
RandrGetCrtcInfoReply,
32+
RandrGetScreenResourcesCurrentReply,
33+
RandrGetScreenResourcesReply,
34+
RandrMode,
35+
RandrModeInfo,
36+
RandrOutput,
37+
RandrQueryVersionReply,
38+
RandrSetConfig,
39+
RenderDirectformat,
40+
RenderPictdepth,
41+
RenderPictdepthIterator,
42+
RenderPictformat,
43+
RenderPictforminfo,
44+
RenderPictscreen,
45+
RenderPictscreenIterator,
46+
RenderPictType,
47+
RenderPictvisual,
48+
RenderQueryPictFormatsReply,
49+
RenderQueryVersionReply,
50+
RenderSubPixel,
51+
Screen,
52+
ScreenIterator,
53+
Setup,
54+
SetupIterator,
55+
Timestamp,
56+
VisualClass,
57+
Visualid,
58+
Visualtype,
59+
Window,
60+
XfixesGetCursorImageReply,
61+
XfixesQueryVersionReply,
62+
depth_visuals,
63+
get_geometry,
64+
get_image,
65+
get_image_data,
66+
get_property,
67+
get_property_value,
68+
no_operation,
69+
randr_get_crtc_info,
70+
randr_get_crtc_info_outputs,
71+
randr_get_crtc_info_possible,
72+
randr_get_screen_resources,
73+
randr_get_screen_resources_crtcs,
74+
randr_get_screen_resources_current,
75+
randr_get_screen_resources_current_crtcs,
76+
randr_get_screen_resources_current_modes,
77+
randr_get_screen_resources_current_names,
78+
randr_get_screen_resources_current_outputs,
79+
randr_get_screen_resources_modes,
80+
randr_get_screen_resources_names,
81+
randr_get_screen_resources_outputs,
82+
randr_query_version,
83+
render_pictdepth_visuals,
84+
render_pictscreen_depths,
85+
render_query_pict_formats,
86+
render_query_pict_formats_formats,
87+
render_query_pict_formats_screens,
88+
render_query_pict_formats_subpixels,
89+
render_query_version,
90+
screen_allowed_depths,
91+
setup_pixmap_formats,
92+
setup_roots,
93+
setup_vendor,
94+
xfixes_get_cursor_image,
95+
xfixes_get_cursor_image_cursor_image,
96+
xfixes_query_version,
97+
)
98+
99+
# These are also here to re-export.
100+
from .xcbhelpers import LIB, Connection, XError
101+
102+
XCB_CONN_ERROR = 1
103+
XCB_CONN_CLOSED_EXT_NOTSUPPORTED = 2
104+
XCB_CONN_CLOSED_MEM_INSUFFICIENT = 3
105+
XCB_CONN_CLOSED_REQ_LEN_EXCEED = 4
106+
XCB_CONN_CLOSED_PARSE_ERR = 5
107+
XCB_CONN_CLOSED_INVALID_SCREEN = 6
108+
XCB_CONN_CLOSED_FDPASSING_FAILED = 7
109+
110+
# I don't know of error descriptions for the XCB connection errors being accessible through a library (a la strerror),
111+
# and the ones in xcb.h's comments aren't too great, so I wrote these.
112+
XCB_CONN_ERRMSG = {
113+
XCB_CONN_ERROR: "connection lost or could not be established",
114+
XCB_CONN_CLOSED_EXT_NOTSUPPORTED: "extension not supported",
115+
XCB_CONN_CLOSED_MEM_INSUFFICIENT: "memory exhausted",
116+
XCB_CONN_CLOSED_REQ_LEN_EXCEED: "request length longer than server accepts",
117+
XCB_CONN_CLOSED_PARSE_ERR: "display is unset or invalid (check $DISPLAY)",
118+
XCB_CONN_CLOSED_INVALID_SCREEN: "server does not have a screen matching the requested display",
119+
XCB_CONN_CLOSED_FDPASSING_FAILED: "could not pass file descriptor",
120+
}
121+
122+
123+
def initialize() -> None:
124+
LIB.initialize(callbacks=[xcbgen.initialize])
125+
126+
127+
def connect(display: str | bytes | None = None) -> tuple[Connection, int]:
128+
if isinstance(display, str):
129+
display = display.encode("utf-8")
130+
131+
initialize()
132+
pref_screen_num = c_int()
133+
conn_p = LIB.xcb.xcb_connect(display, pref_screen_num)
134+
135+
# We still get a connection object even if the connection fails.
136+
conn_err = LIB.xcb.xcb_connection_has_error(conn_p)
137+
if conn_err != 0:
138+
# XCB won't free its connection structures until we disconnect, even in the event of an error.
139+
LIB.xcb.xcb_disconnect(conn_p)
140+
msg = "Cannot connect to display: "
141+
conn_errmsg = XCB_CONN_ERRMSG.get(conn_err)
142+
if conn_errmsg:
143+
msg += conn_errmsg
144+
else:
145+
msg += f"error code {conn_err}"
146+
raise XError(msg)
147+
148+
return conn_p.contents, pref_screen_num.value
149+
150+
151+
def disconnect(conn: Connection) -> None:
152+
conn_err = LIB.xcb.xcb_connection_has_error(conn)
153+
# XCB won't free its connection structures until we disconnect, even in the event of an error.
154+
LIB.xcb.xcb_disconnect(conn)
155+
if conn_err != 0:
156+
msg = "Connection to X server closed: "
157+
conn_errmsg = XCB_CONN_ERRMSG.get(conn_err)
158+
if conn_errmsg:
159+
msg += conn_errmsg
160+
else:
161+
msg += f"error code {conn_err}"
162+
raise XError(msg)

0 commit comments

Comments
 (0)