Skip to content

Commit bfabc4d

Browse files
committed
fix(Linux): reset X server error handler on exit to prevent issues with Tk/Tkinter
1 parent 1c18df3 commit bfabc4d

File tree

4 files changed

+92
-48
lines changed

4 files changed

+92
-48
lines changed

CHANGELOG

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ History:
55
8.0.0 2023/0x/xx
66
- the whole source code was migrated to PEP 570 (Python Positional-Only Parameters)
77
- removed support for Python 3.7
8+
- Linux: reset X server error handler on exit to prevent issues with Tk/Tkinter (#235)
89
- Linux: added mouse support (#232)
910
- Linux: refactored how internal handles are stored to fix issues with multiple X servers, and TKinter.
1011
No more side effects, and when leaving the context manager, resources are all freed (#224, #234)

mss/linux.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -182,14 +182,32 @@ class XWindowAttributes(Structure):
182182

183183

184184
_ERROR = {}
185+
_X11 = find_library("X11")
186+
_XFIXES = find_library("Xfixes")
187+
_XRANDR = find_library("Xrandr")
188+
189+
190+
@CFUNCTYPE(c_int, POINTER(Display), POINTER(Event))
191+
def _default_error_handler(display: Display, event: Event) -> int:
192+
"""
193+
Specifies the default program's supplied error handler.
194+
It's useful when exiting MSS to prevent letting `_error_handler()` as default handler.
195+
Doing so would crash when using Tk/Tkinter, see issue #220.
196+
197+
Interesting technical stuff can be found here:
198+
https://core.tcl-lang.org/tk/file?name=generic/tkError.c&ci=a527ef995862cb50
199+
https://github.com/tcltk/tk/blob/b9cdafd83fe77499ff47fa373ce037aff3ae286a/generic/tkError.c
200+
"""
201+
# pylint: disable=unused-argument
202+
return 0 # pragma: nocover
185203

186204

187205
@CFUNCTYPE(c_int, POINTER(Display), POINTER(Event))
188206
def _error_handler(display: Display, event: Event) -> int:
189207
"""Specifies the program's supplied error handler."""
190208

191209
# Get the specific error message
192-
xlib = cdll.LoadLibrary(find_library("X11")) # type: ignore[arg-type]
210+
xlib = cdll.LoadLibrary(_X11) # type: ignore[arg-type]
193211
get_error = xlib.XGetErrorText
194212
get_error.argtypes = [POINTER(Display), c_int, c_char_p, c_int]
195213
get_error.restype = c_void_p
@@ -278,24 +296,21 @@ def __init__(self, /, **kwargs: Any) -> None:
278296
if b":" not in display:
279297
raise ScreenShotError(f"Bad display value: {display!r}.")
280298

281-
x11 = find_library("X11")
282-
if not x11:
299+
if not _X11:
283300
raise ScreenShotError("No X11 library found.")
284-
self.xlib = cdll.LoadLibrary(x11)
301+
self.xlib = cdll.LoadLibrary(_X11)
285302

286303
# Install the error handler to prevent interpreter crashes:
287304
# any error will raise a ScreenShotError exception.
288305
self.xlib.XSetErrorHandler(_error_handler)
289306

290-
xrandr = find_library("Xrandr")
291-
if not xrandr:
307+
if not _XRANDR:
292308
raise ScreenShotError("No Xrandr extension found.")
293-
self.xrandr = cdll.LoadLibrary(xrandr)
309+
self.xrandr = cdll.LoadLibrary(_XRANDR)
294310

295311
if self.with_cursor:
296-
xfixes = find_library("Xfixes")
297-
if xfixes:
298-
self.xfixes = cdll.LoadLibrary(xfixes)
312+
if _XFIXES:
313+
self.xfixes = cdll.LoadLibrary(_XFIXES)
299314
else:
300315
self.with_cursor = False
301316

@@ -314,10 +329,15 @@ def __init__(self, /, **kwargs: Any) -> None:
314329
self._handles.drawable = cast(self._handles.root, POINTER(Display))
315330

316331
def close(self) -> None:
332+
# Remove our error handler
333+
self.xlib.XSetErrorHandler(_default_error_handler)
334+
335+
# Clean-up
317336
if self._handles.display is not None:
318337
self.xlib.XCloseDisplay(self._handles.display)
319338
self._handles.display = None
320339

340+
# Also empty the error dict
321341
_ERROR.clear()
322342

323343
def _is_extension_enabled(self, name: str, /) -> bool:
@@ -350,7 +370,8 @@ def _set_cfunctions(self) -> None:
350370
}
351371
for func, (attr, argtypes, restype) in CFUNCTIONS.items():
352372
with suppress(AttributeError):
353-
cfactory(attrs[attr], func, argtypes, restype, errcheck=_validate)
373+
errcheck = None if func == "XSetErrorHandler" else _validate
374+
cfactory(attrs[attr], func, argtypes, restype, errcheck=errcheck)
354375

355376
def _monitors_impl(self) -> None:
356377
"""Get positions of monitors. It will populate self._monitors."""

mss/tests/test_gnu_linux.py

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
This is part of the MSS Python's module.
33
Source: https://github.com/BoboTiG/python-mss
44
"""
5-
import ctypes.util
65
import platform
76
from unittest.mock import Mock, patch
87

@@ -88,30 +87,18 @@ def test_bad_display_structure(monkeypatch):
8887
pass
8988

9089

90+
@patch("mss.linux._X11", new=None)
9191
def test_no_xlib_library():
92-
with patch("mss.linux.find_library", return_value=None):
93-
with pytest.raises(ScreenShotError):
94-
with mss.mss():
95-
pass
92+
with pytest.raises(ScreenShotError):
93+
with mss.mss():
94+
pass
9695

9796

97+
@patch("mss.linux._XRANDR", new=None)
9898
def test_no_xrandr_extension():
99-
x11 = ctypes.util.find_library("X11")
100-
101-
def find_lib_mocked(lib):
102-
"""
103-
Returns None to emulate no XRANDR library.
104-
Returns the previous found X11 library else.
105-
106-
It is a naive approach, but works for now.
107-
"""
108-
109-
return None if lib == "Xrandr" else x11
110-
111-
# No `Xrandr` library
112-
with patch("mss.linux.find_library", find_lib_mocked):
113-
with pytest.raises(ScreenShotError):
114-
mss.mss()
99+
with pytest.raises(ScreenShotError):
100+
with mss.mss():
101+
pass
115102

116103

117104
@patch("mss.linux.MSS._is_extension_enabled", new=Mock(return_value=False))
@@ -190,23 +177,11 @@ def test_with_cursor(display: str):
190177
assert set(screenshot_with_cursor.rgb) == {0, 255}
191178

192179

180+
@patch("mss.linux._XFIXES", new=None)
193181
def test_with_cursor_but_not_xfixes_extension_found(display: str):
194-
x11 = ctypes.util.find_library("X11")
195-
196-
def find_lib_mocked(lib):
197-
"""
198-
Returns None to emulate no XRANDR library.
199-
Returns the previous found X11 library else.
200-
201-
It is a naive approach, but works for now.
202-
"""
203-
204-
return None if lib == "Xfixes" else x11
205-
206-
with patch("mss.linux.find_library", find_lib_mocked):
207-
with mss.mss(display=display, with_cursor=True) as sct:
208-
assert not hasattr(sct, "xfixes")
209-
assert not sct.with_cursor
182+
with mss.mss(display=display, with_cursor=True) as sct:
183+
assert not hasattr(sct, "xfixes")
184+
assert not sct.with_cursor
210185

211186

212187
def test_with_cursor_failure(display: str):

mss/tests/test_issue_220.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""
2+
This is part of the MSS Python's module.
3+
Source: https://github.com/BoboTiG/python-mss
4+
"""
5+
import pytest
6+
7+
import mss
8+
9+
tkinter = pytest.importorskip("tkinter")
10+
root = tkinter.Tk()
11+
12+
13+
def take_screenshot():
14+
region = {"top": 370, "left": 1090, "width": 80, "height": 390}
15+
with mss.mss() as sct:
16+
sct.grab(region)
17+
18+
19+
def create_top_level_win():
20+
top_level_win = tkinter.Toplevel(root)
21+
22+
take_screenshot_btn = tkinter.Button(top_level_win, text="Take screenshot", command=take_screenshot)
23+
take_screenshot_btn.pack()
24+
25+
take_screenshot_btn.invoke()
26+
root.update_idletasks()
27+
root.update()
28+
29+
top_level_win.destroy()
30+
root.update_idletasks()
31+
root.update()
32+
33+
34+
def test_regression(capsys):
35+
btn = tkinter.Button(root, text="Open TopLevel", command=create_top_level_win)
36+
btn.pack()
37+
38+
# First screenshot: it works
39+
btn.invoke()
40+
41+
# Second screenshot: it should work too
42+
btn.invoke()
43+
44+
# Check there were no exceptions
45+
captured = capsys.readouterr()
46+
assert not captured.out
47+
assert not captured.err

0 commit comments

Comments
 (0)