66import ctypes
77import ctypes .util
88import os
9+ import threading
910from types import SimpleNamespace
1011from 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 )
0 commit comments