Skip to content

Commit 6da8168

Browse files
committed
MSS: the implementation is now thread-safe on all OSes
1 parent c15ac7f commit 6da8168

File tree

11 files changed

+181
-193
lines changed

11 files changed

+181
-193
lines changed

CHANGELOG

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ History:
22

33
<see Git checking messages for history>
44

5-
5.1.1 2020/xx/xx
5+
6.0.0 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)
8+
- MSS: the implementation is now thread-safe on all OSes (fixes #169)
99
- :heart: contributors: @
1010

1111
5.1.0 2020/04/30

CHANGES.rst

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,30 @@
1-
5.1.1 (2020-xx-xx)
1+
6.0.0 (2020-xx-xx)
22
==================
33

4+
base.py
5+
-------
6+
- Added ``lock``
7+
- Added ``MSS._grab_impl()`` (abstract method)
8+
- Added ``MSS._monitors_impl()`` (abstract method)
9+
- ``MSS.grab()`` is no more an abstract method
10+
- ``MSS.monitors`` is no more an abstract property
11+
12+
darwin.py
13+
---------
14+
- Renamed ``MSS.grab()`` to ``MSS._grab_impl()``
15+
- Renamed ``MSS.monitors`` to ``MSS._monitors_impl()``
416

17+
linux.py
18+
--------
19+
- Renamed ``MSS.grab()`` to ``MSS._grab_impl()``
20+
- Renamed ``MSS.monitors`` to ``MSS._monitors_impl()``
21+
22+
windows.py
23+
----------
24+
- Removed ``MSS._lock``
25+
- Renamed ``MSS.srcdc_dict`` to ``MSS._srcdc_dict``
26+
- Renamed ``MSS.grab()`` to ``MSS._grab_impl()``
27+
- Renamed ``MSS.monitors`` to ``MSS._monitors_impl()``
528

629

730
5.1.0 (2020-04-30)

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Python MSS
2222
2323
An ultra fast cross-platform multiple screenshots module in pure python using ctypes.
2424

25-
- **Python 3.5+** and PEP8 compliant, no dependency;
25+
- **Python 3.5+** and PEP8 compliant, no dependency, thread-safe;
2626
- very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file;
2727
- but you can use PIL and benefit from all its formats (or add yours directly);
2828
- integrate well with Numpy and OpenCV;

docs/source/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
# built documents.
2828
#
2929
# The short X.Y version.
30-
version = "5.1.1"
30+
version = "6.0.0"
3131

3232
# The full version, including alpha/beta/rc tags.
3333
release = "latest"

docs/source/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Welcome to Python MSS's documentation!
1212
1313
An ultra fast cross-platform multiple screenshots module in pure python using ctypes.
1414

15-
- **Python 3.5+** and :pep:`8` compliant, no dependency;
15+
- **Python 3.5+** and :pep:`8` compliant, no dependency, thread-safe;
1616
- very basic, it will grab one screen shot by monitor or a screen shot of all monitors and save it to a PNG file;
1717
- but you can use PIL and benefit from all its formats (or add yours directly);
1818
- integrate well with Numpy and OpenCV;

mss/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from .exception import ScreenShotError
1313
from .factory import mss
1414

15-
__version__ = "5.1.1"
15+
__version__ = "6.0.0"
1616
__author__ = "Mickaël 'Tiger-222' Schoentgen"
1717
__copyright__ = """
1818
Copyright (c) 2013-2020, Mickaël 'Tiger-222' Schoentgen

mss/base.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from abc import ABCMeta, abstractmethod
77
from datetime import datetime
88
from typing import TYPE_CHECKING
9+
from threading import Lock
910

1011
from .exception import ScreenShotError
1112
from .screenshot import ScreenShot
@@ -17,6 +18,9 @@
1718
from .models import Monitor, Monitors # noqa
1819

1920

21+
lock = Lock()
22+
23+
2024
class MSSBase(metaclass=ABCMeta):
2125
""" This class will be overloaded by a system specific one. """
2226

@@ -38,23 +42,51 @@ def __exit__(self, *_):
3842

3943
self.close()
4044

45+
@abstractmethod
46+
def _grab_impl(self, monitor):
47+
# type: (Monitor) -> ScreenShot
48+
"""
49+
Retrieve all pixels from a monitor. Pixels have to be RGB.
50+
That method has to be run using a threading lock.
51+
"""
52+
53+
@abstractmethod
54+
def _monitors_impl(self):
55+
# type: () -> None
56+
"""
57+
Get positions of monitors (has to be run using a threading lock).
58+
It must populate self._monitors.
59+
"""
60+
4161
def close(self):
4262
# type: () -> None
4363
""" Clean-up. """
4464

45-
@abstractmethod
4665
def grab(self, monitor):
4766
# type: (Monitor) -> ScreenShot
4867
"""
4968
Retrieve screen pixels for a given monitor.
5069
70+
Note: *monitor* can be a tuple like PIL.Image.grab() accepts.
71+
5172
:param monitor: The coordinates and size of the box to capture.
5273
See :meth:`monitors <monitors>` for object details.
5374
:return :class:`ScreenShot <ScreenShot>`.
5475
"""
5576

77+
# Convert PIL bbox style
78+
if isinstance(monitor, tuple):
79+
monitor = {
80+
"left": monitor[0],
81+
"top": monitor[1],
82+
"width": monitor[2] - monitor[0],
83+
"height": monitor[3] - monitor[1],
84+
}
85+
86+
with lock:
87+
return self._grab_impl(monitor)
88+
5689
@property
57-
@abstractmethod
5890
def monitors(self):
5991
# type: () -> Monitors
6092
"""
@@ -74,11 +106,14 @@ def monitors(self):
74106
'width': the width,
75107
'height': the height
76108
}
77-
78-
Note: monitor can be a tuple like PIL.Image.grab() accepts,
79-
it must be converted to the appropriate dict.
80109
"""
81110

111+
if not self._monitors:
112+
with lock:
113+
self._monitors_impl()
114+
115+
return self._monitors
116+
82117
def save(self, mon=0, output="monitor-{mon}.png", callback=None):
83118
# type: (int, str, Callable[[str], None]) -> Iterator[str]
84119
"""

mss/darwin.py

Lines changed: 49 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -120,75 +120,60 @@ def cfactory(func, argtypes, restype):
120120
cfactory(func="CGDataProviderRelease", argtypes=[void], restype=void)
121121
cfactory(func="CFRelease", argtypes=[void], restype=void)
122122

123-
@property
124-
def monitors(self):
125-
# type: () -> Monitors
126-
""" Get positions of monitors (see parent class). """
127-
128-
if not self._monitors:
129-
int_ = int
130-
core = self.core
131-
132-
# All monitors
133-
# We need to update the value with every single monitor found
134-
# using CGRectUnion. Else we will end with infinite values.
135-
all_monitors = CGRect()
136-
self._monitors.append({})
137-
138-
# Each monitors
139-
display_count = ctypes.c_uint32(0)
140-
active_displays = (ctypes.c_uint32 * self.max_displays)()
141-
core.CGGetActiveDisplayList(
142-
self.max_displays, active_displays, ctypes.byref(display_count)
123+
def _monitors_impl(self):
124+
# type: () -> None
125+
""" Get positions of monitors. It will populate self._monitors. """
126+
127+
int_ = int
128+
core = self.core
129+
130+
# All monitors
131+
# We need to update the value with every single monitor found
132+
# using CGRectUnion. Else we will end with infinite values.
133+
all_monitors = CGRect()
134+
self._monitors.append({})
135+
136+
# Each monitors
137+
display_count = ctypes.c_uint32(0)
138+
active_displays = (ctypes.c_uint32 * self.max_displays)()
139+
core.CGGetActiveDisplayList(
140+
self.max_displays, active_displays, ctypes.byref(display_count)
141+
)
142+
rotations = {0.0: "normal", 90.0: "right", -90.0: "left"}
143+
for idx in range(display_count.value):
144+
display = active_displays[idx]
145+
rect = core.CGDisplayBounds(display)
146+
rect = core.CGRectStandardize(rect)
147+
width, height = rect.size.width, rect.size.height
148+
rot = core.CGDisplayRotation(display)
149+
if rotations[rot] in ["left", "right"]:
150+
width, height = height, width
151+
self._monitors.append(
152+
{
153+
"left": int_(rect.origin.x),
154+
"top": int_(rect.origin.y),
155+
"width": int_(width),
156+
"height": int_(height),
157+
}
143158
)
144-
rotations = {0.0: "normal", 90.0: "right", -90.0: "left"}
145-
for idx in range(display_count.value):
146-
display = active_displays[idx]
147-
rect = core.CGDisplayBounds(display)
148-
rect = core.CGRectStandardize(rect)
149-
width, height = rect.size.width, rect.size.height
150-
rot = core.CGDisplayRotation(display)
151-
if rotations[rot] in ["left", "right"]:
152-
width, height = height, width
153-
self._monitors.append(
154-
{
155-
"left": int_(rect.origin.x),
156-
"top": int_(rect.origin.y),
157-
"width": int_(width),
158-
"height": int_(height),
159-
}
160-
)
161-
162-
# Update AiO monitor's values
163-
all_monitors = core.CGRectUnion(all_monitors, rect)
164-
165-
# Set the AiO monitor's values
166-
self._monitors[0] = {
167-
"left": int_(all_monitors.origin.x),
168-
"top": int_(all_monitors.origin.y),
169-
"width": int_(all_monitors.size.width),
170-
"height": int_(all_monitors.size.height),
171-
}
172-
173-
return self._monitors
174-
175-
def grab(self, monitor):
159+
160+
# Update AiO monitor's values
161+
all_monitors = core.CGRectUnion(all_monitors, rect)
162+
163+
# Set the AiO monitor's values
164+
self._monitors[0] = {
165+
"left": int_(all_monitors.origin.x),
166+
"top": int_(all_monitors.origin.y),
167+
"width": int_(all_monitors.size.width),
168+
"height": int_(all_monitors.size.height),
169+
}
170+
171+
def _grab_impl(self, monitor):
176172
# type: (Monitor) -> ScreenShot
177-
"""
178-
See :meth:`MSSBase.grab <mss.base.MSSBase.grab>` for full details.
179-
"""
173+
""" Retrieve all pixels from a monitor. Pixels have to be RGB. """
180174

181175
# pylint: disable=too-many-locals
182176

183-
# Convert PIL bbox style
184-
if isinstance(monitor, tuple):
185-
monitor = {
186-
"left": monitor[0],
187-
"top": monitor[1],
188-
"width": monitor[2] - monitor[0],
189-
"height": monitor[3] - monitor[1],
190-
}
191-
192177
core = self.core
193178
rect = CGRect(
194179
(monitor["left"], monitor["top"]), (monitor["width"], monitor["height"])

mss/linux.py

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,6 @@ class MSS(MSSBase):
191191
# A dict to maintain *display* values created by multiple threads.
192192
_display_dict = {} # type: Dict[threading.Thread, int]
193193

194-
# A threading lock to lock resources.
195-
_lock = threading.Lock()
196-
197194
def __init__(self, display=None):
198195
# type: (Optional[Union[bytes, str]]) -> None
199196
""" GNU/Linux initialisations. """
@@ -359,10 +356,7 @@ def get_error_details(self):
359356

360357
def _monitors_impl(self):
361358
# type: () -> None
362-
"""
363-
Get positions of monitors (has to be run using a threading lock).
364-
It will populate self._monitors.
365-
"""
359+
""" Get positions of monitors. It will populate self._monitors. """
366360

367361
display = self._get_display()
368362
int_ = int
@@ -400,32 +394,9 @@ def _monitors_impl(self):
400394
xrandr.XRRFreeCrtcInfo(crtc)
401395
xrandr.XRRFreeScreenResources(mon)
402396

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()
411-
412-
return self._monitors
413-
414397
def _grab_impl(self, monitor):
415398
# type: (Monitor) -> ScreenShot
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-
"""
420-
421-
# Convert PIL bbox style
422-
if isinstance(monitor, tuple):
423-
monitor = {
424-
"left": monitor[0],
425-
"top": monitor[1],
426-
"width": monitor[2] - monitor[0],
427-
"height": monitor[3] - monitor[1],
428-
}
399+
""" Retrieve all pixels from a monitor. Pixels have to be RGB. """
429400

430401
ximage = self.xlib.XGetImage(
431402
self._get_display(),
@@ -459,10 +430,3 @@ def _grab_impl(self, monitor):
459430
self.xlib.XDestroyImage(ximage)
460431

461432
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

Comments
 (0)