Skip to content

Commit 59c2259

Browse files
committed
Linux: the implementation is now thread-safe (fixes BoboTiG#169)
1 parent 23037e2 commit 59c2259

File tree

3 files changed

+113
-41
lines changed

3 files changed

+113
-41
lines changed

CHANGELOG

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ History:
55
5.1.1 2020/xx/xx
66
- removed usage of deprecated "license_file" option for "license_files"
77
- fixed flake8 usage in pre-commit
8+
- Linux: the implementation is now thread-safe (fixes #169)
89
- :heart: contributors: @
910

1011
5.1.0 2020/04/30

mss/linux.py

Lines changed: 83 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import ctypes
77
import ctypes.util
88
import os
9+
import threading
910
from types import SimpleNamespace
1011
from typing import TYPE_CHECKING
1112

@@ -187,6 +188,12 @@ class MSS(MSSBase):
187188
# Instancied one time to prevent resource leak.
188189
display = None
189190

191+
# A dict to maintain *display* values created by multiple threads.
192+
_display_dict = {} # type: Dict[threading.Thread, int]
193+
194+
# A threading lock to lock resources.
195+
_lock = threading.Lock()
196+
190197
def __init__(self, display=None):
191198
# type: (Optional[Union[bytes, str]]) -> None
192199
""" GNU/Linux initialisations. """
@@ -221,14 +228,29 @@ def __init__(self, display=None):
221228

222229
self._set_cfunctions()
223230

224-
if not MSS.display:
225-
MSS.display = self.xlib.XOpenDisplay(display)
226-
self.root = self.xlib.XDefaultRootWindow(MSS.display)
231+
self.root = self.xlib.XDefaultRootWindow(self._get_display(display))
227232

228233
# Fix for XRRGetScreenResources and XGetImage:
229234
# expected LP_Display instance instead of LP_XWindowAttributes
230235
self.drawable = ctypes.cast(self.root, ctypes.POINTER(Display))
231236

237+
def _get_display(self, disp=None):
238+
"""
239+
Retrieve a thread-safe display from XOpenDisplay().
240+
In multithreading, if the thread who creates *display* is dead, *display* will
241+
no longer be valid to grab the screen. The *display* attribute is replaced
242+
with *_display_dict* to maintain the *display* values in multithreading.
243+
Since the current thread and main thread are always alive, reuse their
244+
*display* value first.
245+
"""
246+
cur_thread, main_thread = threading.current_thread(), threading.main_thread()
247+
display = MSS._display_dict.get(cur_thread) or MSS._display_dict.get(
248+
main_thread
249+
)
250+
if not display:
251+
display = MSS._display_dict[cur_thread] = self.xlib.XOpenDisplay(disp)
252+
return display
253+
232254
def _set_cfunctions(self):
233255
"""
234256
Set all ctypes functions and attach them to attributes.
@@ -324,7 +346,7 @@ def get_error_details(self):
324346
ERROR.details = None
325347
xserver_error = ctypes.create_string_buffer(1024)
326348
self.xlib.XGetErrorText(
327-
MSS.display,
349+
self._get_display(),
328350
details.get("xerror_details", {}).get("error_code", 0),
329351
xserver_error,
330352
len(xserver_error),
@@ -335,53 +357,66 @@ def get_error_details(self):
335357

336358
return details
337359

338-
@property
339-
def monitors(self):
340-
# type: () -> Monitors
341-
""" Get positions of monitors (see parent class property). """
360+
def _monitors_impl(self):
361+
# type: () -> None
362+
"""
363+
Get positions of monitors (has to be run using a threading lock).
364+
It will populate self._monitors.
365+
"""
342366

343-
if not self._monitors:
344-
display = MSS.display
345-
int_ = int
346-
xrandr = self.xrandr
367+
display = self._get_display()
368+
int_ = int
369+
xrandr = self.xrandr
370+
371+
# All monitors
372+
gwa = XWindowAttributes()
373+
self.xlib.XGetWindowAttributes(display, self.root, ctypes.byref(gwa))
374+
self._monitors.append(
375+
{
376+
"left": int_(gwa.x),
377+
"top": int_(gwa.y),
378+
"width": int_(gwa.width),
379+
"height": int_(gwa.height),
380+
}
381+
)
382+
383+
# Each monitors
384+
mon = xrandr.XRRGetScreenResourcesCurrent(display, self.drawable).contents
385+
crtcs = mon.crtcs
386+
for idx in range(mon.ncrtc):
387+
crtc = xrandr.XRRGetCrtcInfo(display, mon, crtcs[idx]).contents
388+
if crtc.noutput == 0:
389+
xrandr.XRRFreeCrtcInfo(crtc)
390+
continue
347391

348-
# All monitors
349-
gwa = XWindowAttributes()
350-
self.xlib.XGetWindowAttributes(display, self.root, ctypes.byref(gwa))
351392
self._monitors.append(
352393
{
353-
"left": int_(gwa.x),
354-
"top": int_(gwa.y),
355-
"width": int_(gwa.width),
356-
"height": int_(gwa.height),
394+
"left": int_(crtc.x),
395+
"top": int_(crtc.y),
396+
"width": int_(crtc.width),
397+
"height": int_(crtc.height),
357398
}
358399
)
400+
xrandr.XRRFreeCrtcInfo(crtc)
401+
xrandr.XRRFreeScreenResources(mon)
359402

360-
# Each monitors
361-
mon = xrandr.XRRGetScreenResourcesCurrent(display, self.drawable).contents
362-
crtcs = mon.crtcs
363-
for idx in range(mon.ncrtc):
364-
crtc = xrandr.XRRGetCrtcInfo(display, mon, crtcs[idx]).contents
365-
if crtc.noutput == 0:
366-
xrandr.XRRFreeCrtcInfo(crtc)
367-
continue
368-
369-
self._monitors.append(
370-
{
371-
"left": int_(crtc.x),
372-
"top": int_(crtc.y),
373-
"width": int_(crtc.width),
374-
"height": int_(crtc.height),
375-
}
376-
)
377-
xrandr.XRRFreeCrtcInfo(crtc)
378-
xrandr.XRRFreeScreenResources(mon)
403+
@property
404+
def monitors(self):
405+
# type: () -> Monitors
406+
""" Get positions of monitors (see parent class property). """
407+
408+
if not self._monitors:
409+
with MSS._lock:
410+
self._monitors_impl()
379411

380412
return self._monitors
381413

382-
def grab(self, monitor):
414+
def _grab_impl(self, monitor):
383415
# type: (Monitor) -> ScreenShot
384-
""" Retrieve all pixels from a monitor. Pixels have to be RGB. """
416+
"""
417+
Retrieve all pixels from a monitor. Pixels have to be RGB.
418+
That method has to be run using a threading lock.
419+
"""
385420

386421
# Convert PIL bbox style
387422
if isinstance(monitor, tuple):
@@ -393,7 +428,7 @@ def grab(self, monitor):
393428
}
394429

395430
ximage = self.xlib.XGetImage(
396-
MSS.display,
431+
self._get_display(),
397432
self.drawable,
398433
monitor["left"],
399434
monitor["top"],
@@ -424,3 +459,10 @@ def grab(self, monitor):
424459
self.xlib.XDestroyImage(ximage)
425460

426461
return self.cls_image(data, monitor)
462+
463+
def grab(self, monitor):
464+
# type: (Monitor) -> ScreenShot
465+
""" Retrieve all pixels from a monitor. Pixels have to be RGB. """
466+
467+
with MSS._lock:
468+
return self._grab_impl(monitor)

mss/tests/test_implementation.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,32 @@ def test_grab_with_tuple_percents(sct, pixel_ratio):
168168
assert im.size == im2.size
169169
assert im.pos == im2.pos
170170
assert im.rgb == im2.rgb
171+
172+
173+
def test_thread_safety():
174+
"""Regression test for issue #169."""
175+
import threading
176+
import time
177+
178+
def record(check):
179+
"""Record for one second."""
180+
181+
start_time = time.time()
182+
while time.time() - start_time < 1:
183+
with mss.mss() as sct:
184+
sct.grab(sct.monitors[1])
185+
186+
check[threading.current_thread()] = True
187+
188+
checkpoint = {}
189+
t1 = threading.Thread(target=record, args=(checkpoint,))
190+
t2 = threading.Thread(target=record, args=(checkpoint,))
191+
192+
t1.start()
193+
time.sleep(0.5)
194+
t2.start()
195+
196+
t1.join()
197+
t2.join()
198+
199+
assert len(checkpoint) == 2

0 commit comments

Comments
 (0)