Skip to content

Commit d786046

Browse files
authored
linux: X11 typing and error message improvements (#418)
* Improve typing Several places, a POINTER(Display) was declared, when the actual object was an XID (opaque identifier). In one place, a POINTER(XWindowAttributes) was declared, instead of an XID. This is also why self._handles.drawable had to be cast. Also, the XErrorEvent structure had its fields in the wrong order. This meant that error messages in exception details were incorrect. I'm keeping self._handles.drawable, even though it's currently the same as self._handles.root, in anticipation that we may later support capturing individual windows. * Decode X11 error messages in the correct locale XGetErrorText returns strings encoded in the current locale (based on LC_CTYPE), which is not always UTF-8. * Display more complete X11 error messages The current error message for errors from X11 only gives the name of the failing function. It is often useful to also see the information contained in the error details. This is an incomplete implementation - it doesn't handle extensions well - but it's an improvement. In the future, we may move to xcffib, which would mean reworking a lot of this error handling anyway. * Don't use the same inner and outer quotes in f-strings This was not allowed prior to Python 3.12. * Ruff cleanups * More minor typing cleanups Verify all structures against the headers. Add comment links to X.Org's header files, and the Xlib Programming Manual where possible, for all structures. Note where opaque or unneeded fields are in the C definition but not reflected in the Python code. Fix a few other pointers and identifiers to XIDs. Change some int32s in XWindowAttribute to the platform's int, which is what's in the headers. None of these issues affect the correctness of the existing code; this change is only about clarity. * Add a CHANGELOG entry * Remove test_bad_display_structure This test is no longer needed, and can be removed. * Don't assume all errors come with details Sometimes, an Xlib call might fail but there's nothing in the _ERROR stack. This happens if the error was something detected internally by Xlib, rather than coming from the server. Make sure that we don't try to read the details in that case. * locale.getencoding wasn't added until Python 3.11. * Improve return value types and error checking Previously, we would always check to see if the return value was not equal to the integer 0. We didn't check for NULL pointers at all: a ctypes _Pointer holding a NULL value is not equal to the integer 0. Usually, the X server's error event would cue us into the error anyway, but this is not always guaranteed. Some integer return values that we didn't need (or want) to check, such as XCloseDisplay and XDestroyImage, were declared to return c_void_p, which is never equal to 0. The new version has the correct return type declarations, and logic to check whether a call is considered a success or not based on the type. This fixes some segfaults in older versions of Python that were introduced by earlier type fixes.
1 parent fd67d3b commit d786046

File tree

3 files changed

+177
-58
lines changed

3 files changed

+177
-58
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ See Git checking messages for full history.
44

55
## 10.1.1.dev0 (2025-xx-xx)
66
- Linux: check the server for Xrandr support version (#417)
7+
- Linux: improve typing and error messages for X libraries (#418)
78
- :heart: contributors: @jholveck
89

910
## 10.1.0 (2025-08-16)

src/mss/linux.py

Lines changed: 166 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,20 @@
44

55
from __future__ import annotations
66

7+
import locale
78
import os
89
from contextlib import suppress
910
from ctypes import (
1011
CFUNCTYPE,
1112
POINTER,
1213
Structure,
14+
_Pointer,
1315
byref,
1416
c_char_p,
1517
c_int,
16-
c_int32,
17-
c_long,
1818
c_short,
1919
c_ubyte,
2020
c_uint,
21-
c_uint32,
2221
c_ulong,
2322
c_ushort,
2423
c_void_p,
@@ -40,6 +39,7 @@
4039
__all__ = ("MSS",)
4140

4241

42+
X_FIRST_EXTENSION_OPCODE = 128
4343
PLAINMASK = 0x00FFFFFF
4444
ZPIXMAP = 2
4545
BITS_PER_PIXELS_32 = 32
@@ -48,26 +48,79 @@
4848
}
4949

5050

51+
class XID(c_ulong):
52+
"""X11 generic resource ID
53+
https://tronche.com/gui/x/xlib/introduction/generic.html
54+
https://gitlab.freedesktop.org/xorg/proto/xorgproto/-/blob/master/include/X11/X.h#L66
55+
"""
56+
57+
58+
class XStatus(c_int):
59+
"""Xlib common return code type
60+
This is Status in Xlib, but XStatus here to prevent ambiguity.
61+
Zero is an error, non-zero is success.
62+
https://tronche.com/gui/x/xlib/introduction/errors.html
63+
https://gitlab.freedesktop.org/xorg/lib/libx11/-/blob/master/include/X11/Xlib.h#L79
64+
"""
65+
66+
67+
class XBool(c_int):
68+
"""Xlib boolean type
69+
This is Bool in Xlib, but XBool here to prevent ambiguity.
70+
0 is False, 1 is True.
71+
https://tronche.com/gui/x/xlib/introduction/generic.html
72+
https://gitlab.freedesktop.org/xorg/lib/libx11/-/blob/master/include/X11/Xlib.h#L78
73+
"""
74+
75+
5176
class Display(Structure):
5277
"""Structure that serves as the connection to the X server
5378
and that contains all the information about that X server.
79+
The contents of this structure are implementation dependent.
80+
A Display should be treated as opaque by application code.
81+
https://tronche.com/gui/x/xlib/display/display-macros.html
82+
https://gitlab.freedesktop.org/xorg/lib/libx11/-/blob/master/include/X11/Xlib.h#L477
5483
https://github.com/garrybodsworth/pyxlib-ctypes/blob/master/pyxlib/xlib.py#L831.
5584
"""
5685

86+
# Opaque data
87+
88+
89+
class Visual(Structure):
90+
"""Visual structure; contains information about colormapping possible.
91+
https://tronche.com/gui/x/xlib/window/visual-types.html
92+
https://gitlab.freedesktop.org/xorg/lib/libx11/-/blob/master/include/X11/Xlib.hheads#L220
93+
https://github.com/garrybodsworth/pyxlib-ctypes/blob/master/pyxlib/xlib.py#302.
94+
"""
95+
96+
# Opaque data (per Tronche)
97+
98+
99+
class Screen(Structure):
100+
"""Information about the screen.
101+
The contents of this structure are implementation dependent. A
102+
Screen should be treated as opaque by application code.
103+
https://tronche.com/gui/x/xlib/display/screen-information.html
104+
https://gitlab.freedesktop.org/xorg/lib/libx11/-/blob/master/include/X11/Xlib.h#L253
105+
"""
106+
107+
# Opaque data
108+
57109

58110
class XErrorEvent(Structure):
59111
"""XErrorEvent to debug eventual errors.
60112
https://tronche.com/gui/x/xlib/event-handling/protocol-errors/default-handlers.html.
113+
https://gitlab.freedesktop.org/xorg/lib/libx11/-/blob/master/include/X11/Xlib.h#L920
61114
"""
62115

63116
_fields_ = (
64117
("type", c_int),
65118
("display", POINTER(Display)), # Display the event was read from
119+
("resourceid", XID), # resource ID
66120
("serial", c_ulong), # serial number of failed request
67121
("error_code", c_ubyte), # error code of failed request
68122
("request_code", c_ubyte), # major op-code of failed request
69123
("minor_code", c_ubyte), # minor op-code of failed request
70-
("resourceid", c_void_p), # resource ID
71124
)
72125

73126

@@ -93,7 +146,8 @@ class XFixesCursorImage(Structure):
93146

94147
class XImage(Structure):
95148
"""Description of an image as it exists in the client's memory.
96-
https://tronche.com/gui/x/xlib/graphics/images.html.
149+
https://tronche.com/gui/x/xlib/graphics/images.html
150+
https://gitlab.freedesktop.org/xorg/lib/libx11/-/blob/master/include/X11/Xlib.h#L353
97151
"""
98152

99153
_fields_ = (
@@ -113,6 +167,7 @@ class XImage(Structure):
113167
("green_mask", c_ulong), # bits in z arrangement
114168
("blue_mask", c_ulong), # bits in z arrangement
115169
)
170+
# Other opaque fields follow for Xlib's internal use.
116171

117172

118173
class XRRCrtcInfo(Structure):
@@ -126,19 +181,21 @@ class XRRCrtcInfo(Structure):
126181
("y", c_int),
127182
("width", c_uint),
128183
("height", c_uint),
129-
("mode", c_long),
130-
("rotation", c_int),
184+
("mode", XID),
185+
("rotation", c_ushort),
131186
("noutput", c_int),
132-
("outputs", POINTER(c_long)),
187+
("outputs", POINTER(XID)),
133188
("rotations", c_ushort),
134189
("npossible", c_int),
135-
("possible", POINTER(c_long)),
190+
("possible", POINTER(XID)),
136191
)
137192

138193

139194
class XRRModeInfo(Structure):
140195
"""https://gitlab.freedesktop.org/xorg/lib/libxrandr/-/blob/master/include/X11/extensions/Xrandr.h#L248."""
141196

197+
# The fields aren't needed
198+
142199

143200
class XRRScreenResources(Structure):
144201
"""Structure that contains arrays of XIDs that point to the
@@ -150,41 +207,44 @@ class XRRScreenResources(Structure):
150207
("timestamp", c_ulong),
151208
("configTimestamp", c_ulong),
152209
("ncrtc", c_int),
153-
("crtcs", POINTER(c_long)),
210+
("crtcs", POINTER(XID)),
154211
("noutput", c_int),
155-
("outputs", POINTER(c_long)),
212+
("outputs", POINTER(XID)),
156213
("nmode", c_int),
157214
("modes", POINTER(XRRModeInfo)),
158215
)
159216

160217

161218
class XWindowAttributes(Structure):
162-
"""Attributes for the specified window."""
219+
"""Attributes for the specified window.
220+
https://tronche.com/gui/x/xlib/window-information/XGetWindowAttributes.html
221+
https://gitlab.freedesktop.org/xorg/lib/libx11/-/blob/master/include/X11/Xlib.h#L304
222+
"""
163223

164224
_fields_ = (
165-
("x", c_int32), # location of window
166-
("y", c_int32), # location of window
167-
("width", c_int32), # width of window
168-
("height", c_int32), # height of window
169-
("border_width", c_int32), # border width of window
170-
("depth", c_int32), # depth of window
171-
("visual", c_ulong), # the associated visual structure
172-
("root", c_ulong), # root of screen containing window
173-
("class", c_int32), # InputOutput, InputOnly
174-
("bit_gravity", c_int32), # one of bit gravity values
175-
("win_gravity", c_int32), # one of the window gravity values
176-
("backing_store", c_int32), # NotUseful, WhenMapped, Always
225+
("x", c_int), # location of window
226+
("y", c_int), # location of window
227+
("width", c_int), # width of window
228+
("height", c_int), # height of window
229+
("border_width", c_int), # border width of window
230+
("depth", c_int), # depth of window
231+
("visual", POINTER(Visual)), # the associated visual structure
232+
("root", XID), # root of screen containing window
233+
("class", c_int), # InputOutput, InputOnly
234+
("bit_gravity", c_int), # one of bit gravity values
235+
("win_gravity", c_int), # one of the window gravity values
236+
("backing_store", c_int), # NotUseful, WhenMapped, Always
177237
("backing_planes", c_ulong), # planes to be preserved if possible
178238
("backing_pixel", c_ulong), # value to be used when restoring planes
179-
("save_under", c_int32), # boolean, should bits under be saved?
180-
("colormap", c_ulong), # color map to be associated with window
181-
("mapinstalled", c_uint32), # boolean, is color map currently installed
182-
("map_state", c_uint32), # IsUnmapped, IsUnviewable, IsViewable
239+
("save_under", XBool), # boolean, should bits under be saved?
240+
("colormap", XID), # color map to be associated with window
241+
("mapinstalled", XBool), # boolean, is color map currently installed
242+
("map_state", c_uint), # IsUnmapped, IsUnviewable, IsViewable
183243
("all_event_masks", c_ulong), # set of events all people have interest in
184244
("your_event_mask", c_ulong), # my event mask
185245
("do_not_propagate_mask", c_ulong), # set of events that should not propagate
186-
("override_redirect", c_int32), # boolean value for override-redirect
187-
("screen", c_ulong), # back pointer to correct screen
246+
("override_redirect", XBool), # boolean value for override-redirect
247+
("screen", POINTER(Screen)), # back pointer to correct screen
188248
)
189249

190250

@@ -194,6 +254,29 @@ class XWindowAttributes(Structure):
194254
_XRANDR = find_library("Xrandr")
195255

196256

257+
class XError(ScreenShotError):
258+
def __str__(self) -> str:
259+
msg = super().__str__()
260+
# The details only get populated if the X11 error handler is invoked, but not if a function simply returns
261+
# a failure status.
262+
if self.details:
263+
# We use something similar to the default Xlib error handler's format, since that's quite well-understood.
264+
# The original code is in
265+
# https://gitlab.freedesktop.org/xorg/lib/libx11/-/blob/master/src/XlibInt.c?ref_type=heads#L1313
266+
# but we don't try to implement most of it.
267+
msg += (
268+
f"\nX Error of failed request: {self.details['error']}"
269+
f"\n Major opcode of failed request: {self.details['request_code']} ({self.details['request']})"
270+
)
271+
if self.details["request_code"] >= X_FIRST_EXTENSION_OPCODE:
272+
msg += f"\n Minor opcode of failed request: {self.details['minor_code']}"
273+
msg += (
274+
f"\n Resource id in failed request: {self.details['resourceid']}"
275+
f"\n Serial number of failed request: {self.details['serial']}"
276+
)
277+
return msg
278+
279+
197280
@CFUNCTYPE(c_int, POINTER(Display), POINTER(XErrorEvent))
198281
def _error_handler(display: Display, event: XErrorEvent) -> int:
199282
"""Specifies the program's supplied error handler."""
@@ -202,32 +285,65 @@ def _error_handler(display: Display, event: XErrorEvent) -> int:
202285
get_error = xlib.XGetErrorText
203286
get_error.argtypes = [POINTER(Display), c_int, c_char_p, c_int]
204287
get_error.restype = c_void_p
288+
get_error_database = xlib.XGetErrorDatabaseText
289+
get_error_database.argtypes = [POINTER(Display), c_char_p, c_char_p, c_char_p, c_char_p, c_int]
290+
get_error_database.restype = c_int
205291

206292
evt = event.contents
207293
error = create_string_buffer(1024)
208294
get_error(display, evt.error_code, error, len(error))
209-
295+
request = create_string_buffer(1024)
296+
get_error_database(display, b"XRequest", b"%i" % evt.request_code, b"Extension-specific", request, len(request))
297+
# We don't try to get the string forms of the extension name or minor code currently. Those are important
298+
# fields for debugging, but getting the strings is difficult. The call stack of the exception gives pretty
299+
# useful similar information, though; most of the requests we use are synchronous, so the failing request is
300+
# usually the function being called.
301+
302+
encoding = (
303+
locale.getencoding() if hasattr(locale, "getencoding") else locale.getpreferredencoding(do_setlocale=False)
304+
)
210305
_ERROR[current_thread()] = {
211-
"error": error.value.decode("utf-8"),
306+
"error": error.value.decode(encoding, errors="replace"),
212307
"error_code": evt.error_code,
213308
"minor_code": evt.minor_code,
309+
"request": request.value.decode(encoding, errors="replace"),
214310
"request_code": evt.request_code,
215311
"serial": evt.serial,
312+
"resourceid": evt.resourceid,
216313
"type": evt.type,
217314
}
218315

219316
return 0
220317

221318

222-
def _validate(retval: int, func: Any, args: tuple[Any, Any], /) -> tuple[Any, Any]:
223-
"""Validate the returned value of a C function call."""
319+
def _validate_x11(
320+
retval: _Pointer | None | XBool | XStatus | XID | int, func: Any, args: tuple[Any, Any], /
321+
) -> tuple[Any, Any]:
224322
thread = current_thread()
225-
if retval != 0 and thread not in _ERROR:
323+
324+
if retval is None:
325+
# A void return is always ok.
326+
is_ok = True
327+
elif isinstance(retval, (_Pointer, XBool, XStatus, XID)):
328+
# A pointer should be non-NULL. A boolean should be true. An Xlib Status should be non-zero.
329+
# An XID should not be None, which is a reserved ID used for certain APIs.
330+
is_ok = bool(retval)
331+
elif isinstance(retval, int):
332+
# There are currently two functions we call that return ints. XDestroyImage returns 1 always, and
333+
# XCloseDisplay returns 0 always. Neither can fail. Other Xlib functions might return ints with other
334+
# interpretations. If we didn't get an X error from the server, then we'll assume that they worked.
335+
is_ok = True
336+
else:
337+
msg = f"Internal error: cannot check return type {type(retval)}"
338+
raise AssertionError(msg)
339+
340+
# Regardless of the return value, raise an error if the thread got an Xlib error (possibly from an earlier call).
341+
if is_ok and thread not in _ERROR:
226342
return args
227343

228344
details = _ERROR.pop(thread, {})
229345
msg = f"{func.__name__}() failed"
230-
raise ScreenShotError(msg, details=details)
346+
raise XError(msg, details=details)
231347

232348

233349
# C functions that will be initialised later.
@@ -238,24 +354,24 @@ def _validate(retval: int, func: Any, args: tuple[Any, Any], /) -> tuple[Any, An
238354
# Note: keep it sorted by cfunction.
239355
CFUNCTIONS: CFunctions = {
240356
# Syntax: cfunction: (attr, argtypes, restype)
241-
"XCloseDisplay": ("xlib", [POINTER(Display)], c_void_p),
242-
"XDefaultRootWindow": ("xlib", [POINTER(Display)], POINTER(XWindowAttributes)),
243-
"XDestroyImage": ("xlib", [POINTER(XImage)], c_void_p),
357+
"XCloseDisplay": ("xlib", [POINTER(Display)], c_int),
358+
"XDefaultRootWindow": ("xlib", [POINTER(Display)], XID),
359+
"XDestroyImage": ("xlib", [POINTER(XImage)], c_int),
244360
"XFixesGetCursorImage": ("xfixes", [POINTER(Display)], POINTER(XFixesCursorImage)),
245361
"XGetImage": (
246362
"xlib",
247-
[POINTER(Display), POINTER(Display), c_int, c_int, c_uint, c_uint, c_ulong, c_int],
363+
[POINTER(Display), XID, c_int, c_int, c_uint, c_uint, c_ulong, c_int],
248364
POINTER(XImage),
249365
),
250-
"XGetWindowAttributes": ("xlib", [POINTER(Display), POINTER(XWindowAttributes), POINTER(XWindowAttributes)], c_int),
366+
"XGetWindowAttributes": ("xlib", [POINTER(Display), XID, POINTER(XWindowAttributes)], XStatus),
251367
"XOpenDisplay": ("xlib", [c_char_p], POINTER(Display)),
252-
"XQueryExtension": ("xlib", [POINTER(Display), c_char_p, POINTER(c_int), POINTER(c_int), POINTER(c_int)], c_uint),
253-
"XRRQueryVersion": ("xrandr", [POINTER(Display), POINTER(c_int), POINTER(c_int)], c_int),
254-
"XRRFreeCrtcInfo": ("xrandr", [POINTER(XRRCrtcInfo)], c_void_p),
255-
"XRRFreeScreenResources": ("xrandr", [POINTER(XRRScreenResources)], c_void_p),
256-
"XRRGetCrtcInfo": ("xrandr", [POINTER(Display), POINTER(XRRScreenResources), c_long], POINTER(XRRCrtcInfo)),
257-
"XRRGetScreenResources": ("xrandr", [POINTER(Display), POINTER(Display)], POINTER(XRRScreenResources)),
258-
"XRRGetScreenResourcesCurrent": ("xrandr", [POINTER(Display), POINTER(Display)], POINTER(XRRScreenResources)),
368+
"XQueryExtension": ("xlib", [POINTER(Display), c_char_p, POINTER(c_int), POINTER(c_int), POINTER(c_int)], XBool),
369+
"XRRQueryVersion": ("xrandr", [POINTER(Display), POINTER(c_int), POINTER(c_int)], XStatus),
370+
"XRRFreeCrtcInfo": ("xrandr", [POINTER(XRRCrtcInfo)], None),
371+
"XRRFreeScreenResources": ("xrandr", [POINTER(XRRScreenResources)], None),
372+
"XRRGetCrtcInfo": ("xrandr", [POINTER(Display), POINTER(XRRScreenResources), XID], POINTER(XRRCrtcInfo)),
373+
"XRRGetScreenResources": ("xrandr", [POINTER(Display), XID], POINTER(XRRScreenResources)),
374+
"XRRGetScreenResourcesCurrent": ("xrandr", [POINTER(Display), XID], POINTER(XRRScreenResources)),
259375
"XSetErrorHandler": ("xlib", [c_void_p], c_void_p),
260376
}
261377

@@ -323,11 +439,7 @@ def __init__(self, /, **kwargs: Any) -> None:
323439
msg = "Xrandr not enabled."
324440
raise ScreenShotError(msg)
325441

326-
self._handles.root = self.xlib.XDefaultRootWindow(self._handles.display)
327-
328-
# Fix for XRRGetScreenResources and XGetImage:
329-
# expected LP_Display instance instead of LP_XWindowAttributes
330-
self._handles.drawable = cast(self._handles.root, POINTER(Display))
442+
self._handles.drawable = self._handles.root = self.xlib.XDefaultRootWindow(self._handles.display)
331443

332444
def close(self) -> None:
333445
# Clean-up
@@ -380,7 +492,7 @@ def _set_cfunctions(self) -> None:
380492
}
381493
for func, (attr, argtypes, restype) in CFUNCTIONS.items():
382494
with suppress(AttributeError):
383-
errcheck = None if func == "XSetErrorHandler" else _validate
495+
errcheck = None if func == "XSetErrorHandler" else _validate_x11
384496
cfactory(attrs[attr], func, argtypes, restype, errcheck=errcheck)
385497

386498
def _monitors_impl(self) -> None:

0 commit comments

Comments
 (0)