Skip to content

Commit d88aa55

Browse files
committed
MSS: better handle resources to prevent leaks
1 parent da92737 commit d88aa55

File tree

8 files changed

+50
-135
lines changed

8 files changed

+50
-135
lines changed

CHANGELOG

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ History:
66
- removed support for Python 2.7
77
- MSS: improve type annotations and add CI check
88
- MSS: use __slots__ for better performances
9+
- MSS: better handle resources to prevent leaks
910
- Windows: use our own instances of GDI32 and User32 DLLs
1011
- doc: add project_urls to setup.cfg
1112
- doc: add an example using the multiprocessing module (closes #82)

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ darwin.py
88
linux.py
99
--------
1010
- Added `MSS.__slots__`
11+
- Deleted `MSS.close()`
1112
- Deleted ``LAST_ERROR`` constant. Use ``ERROR`` namespace instead, specially the ``ERROR.details`` attribute.
1213

1314
models.py
@@ -28,6 +29,7 @@ screenshot.py
2829
windows.py
2930
----------
3031
- Added `MSS.__slots__`
32+
- Deleted `MSS.close()`
3133

3234

3335
4.0.1 (2019-01-26)

docs/source/api.rst

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,6 @@ GNU/Linux
2727

2828
GNU/Linux initializations.
2929

30-
.. method:: close()
31-
32-
Disconnect from the X server.
33-
34-
.. versionadded:: 4.0.0
35-
3630
.. method:: get_error_details()
3731

3832
:rtype: Optional[dict[str, Any]]
@@ -83,20 +77,6 @@ GNU/Linux
8377
.. versionadded:: 3.3.0
8478

8579

86-
Windows
87-
-------
88-
89-
.. module:: mss.windows
90-
91-
.. class:: MSS
92-
93-
.. method:: close()
94-
95-
Close GDI handles and free DCs.
96-
97-
.. versionadded:: 4.0.0
98-
99-
10080
Methods
10181
=======
10282

@@ -108,10 +88,7 @@ Methods
10888

10989
.. method:: close()
11090

111-
Clean-up method. Does nothing by default. For more information, see specific implementations:
112-
113-
- :meth:`~mss.linux.MSS.close()` for GNU/Linux
114-
- :meth:`~mss.windows.MSS.close()` for Windows
91+
Clean-up method. Does nothing by default.
11592

11693
.. versionadded:: 4.0.0
11794

docs/source/examples/fps.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ def screen_record_efficient():
5656
cv2.destroyAllWindows()
5757
break
5858

59-
sct.close()
6059
return fps
6160

6261

docs/source/usage.rst

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ So MSS can be used as simply as::
99

1010
from mss import mss
1111

12-
1312
Or import the good one base on your operating system::
1413

1514
# MacOS X
@@ -30,26 +29,6 @@ So the module can be used as simply as::
3029
with mss() as sct:
3130
# ...
3231

33-
.. note::
34-
35-
On GNU/Linux and Windows, if you are using this kind of code::
36-
37-
sct = mss()
38-
sct.shot()
39-
sct.grab()
40-
# or any attribute/method of sct
41-
42-
Then you will have to **manually** call :meth:`~mss.base.MSSMixin.close()` to free resources.
43-
44-
.. warning::
45-
46-
This code is **highly** unadvised as there will be **resources leaks** without possibility to do something to clean them::
47-
48-
mss().shot()
49-
mss().grab()
50-
# or any mss().xxx or mss().xxx()
51-
52-
5332
Intensive Use
5433
=============
5534

mss/linux.py

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,11 @@ class MSS(MSSMixin):
181181
It uses intensively the Xlib and its Xrandr extension.
182182
"""
183183

184-
__slots__ = {"display", "drawable", "root", "xlib", "xrandr"}
184+
__slots__ = {"drawable", "root", "xlib", "xrandr"}
185+
186+
# Class attribute to store the display opened with XOpenDisplay().
187+
# Instancied one time to prevent resource leak.
188+
display = None
185189

186190
def __init__(self, display=None):
187191
# type: (Optional[Union[bytes, str]]) -> None
@@ -217,8 +221,9 @@ def __init__(self, display=None):
217221

218222
self._set_cfunctions()
219223

220-
self.display = self.xlib.XOpenDisplay(display)
221-
self.root = self.xlib.XDefaultRootWindow(self.display)
224+
if not MSS.display:
225+
MSS.display = self.xlib.XOpenDisplay(display)
226+
self.root = self.xlib.XDefaultRootWindow(MSS.display)
222227

223228
# Fix for XRRGetScreenResources and XGetImage:
224229
# expected LP_Display instance instead of LP_XWindowAttributes
@@ -285,7 +290,6 @@ def cfactory(attr=self.xlib, func=None, argtypes=None, restype=None):
285290
restype=pointer(XImage),
286291
)
287292
cfactory(func="XDestroyImage", argtypes=[pointer(XImage)], restype=void)
288-
cfactory(func="XCloseDisplay", argtypes=[pointer(Display)], restype=void)
289293

290294
# A simple benchmark calling 10 times those 2 functions:
291295
# XRRGetScreenResources(): 0.1755971429956844 s
@@ -326,20 +330,6 @@ def cfactory(attr=self.xlib, func=None, argtypes=None, restype=None):
326330
restype=void,
327331
)
328332

329-
def close(self):
330-
# type: () -> None
331-
"""
332-
Disconnect from the X server to prevent:
333-
Maximum number of clients reached. Segmentation fault (core dumped)
334-
"""
335-
336-
try:
337-
self.xlib.XCloseDisplay(self.display)
338-
# Delete the attribute to prevent interpreter crash if called twice
339-
del self.display
340-
except AttributeError:
341-
pass
342-
343333
def get_error_details(self):
344334
# type: () -> Optional[Dict[str, Any]]
345335
""" Get more information about the latest X server error. """
@@ -351,7 +341,7 @@ def get_error_details(self):
351341
ERROR.details = None
352342
xserver_error = ctypes.create_string_buffer(1024)
353343
self.xlib.XGetErrorText(
354-
self.display,
344+
MSS.display,
355345
details.get("xerror_details", {}).get("error_code", 0),
356346
xserver_error,
357347
len(xserver_error),
@@ -368,9 +358,11 @@ def monitors(self):
368358
""" Get positions of monitors (see parent class property). """
369359

370360
if not self._monitors:
361+
display = MSS.display
362+
371363
# All monitors
372364
gwa = XWindowAttributes()
373-
self.xlib.XGetWindowAttributes(self.display, self.root, ctypes.byref(gwa))
365+
self.xlib.XGetWindowAttributes(display, self.root, ctypes.byref(gwa))
374366
self._monitors.append(
375367
{
376368
"left": int(gwa.x),
@@ -381,11 +373,9 @@ def monitors(self):
381373
)
382374

383375
# Each monitors
384-
mon = self.xrandr.XRRGetScreenResourcesCurrent(self.display, self.drawable)
376+
mon = self.xrandr.XRRGetScreenResourcesCurrent(display, self.drawable)
385377
for idx in range(mon.contents.ncrtc):
386-
crtc = self.xrandr.XRRGetCrtcInfo(
387-
self.display, mon, mon.contents.crtcs[idx]
388-
)
378+
crtc = self.xrandr.XRRGetCrtcInfo(display, mon, mon.contents.crtcs[idx])
389379
if crtc.contents.noutput == 0:
390380
self.xrandr.XRRFreeCrtcInfo(crtc)
391381
continue
@@ -417,7 +407,7 @@ def grab(self, monitor):
417407
}
418408

419409
ximage = self.xlib.XGetImage(
420-
self.display,
410+
MSS.display,
421411
self.drawable,
422412
monitor["left"],
423413
monitor["top"],

mss/windows.py

Lines changed: 21 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -68,28 +68,19 @@ class BITMAPINFO(ctypes.Structure):
6868
class MSS(MSSMixin):
6969
""" Multiple ScreenShots implementation for Microsoft Windows. """
7070

71-
__slots__ = {
72-
"_bbox",
73-
"_bmi",
74-
"_bmp",
75-
"_data",
76-
"_memdc",
77-
"_srcdc",
78-
"gdi32",
79-
"monitorenumproc",
80-
"user32",
81-
}
71+
__slots__ = {"_bbox", "_bmi", "_data", "gdi32", "monitorenumproc", "user32"}
72+
73+
# Class attributes instancied one time to prevent resource leaks.
74+
bmp = None
75+
memdc = None
76+
srcdc = None
8277

8378
def __init__(self, **_):
8479
# type: (Any) -> None
8580
""" Windows initialisations. """
8681

8782
super().__init__()
8883

89-
self._bbox = {"height": 0, "width": 0}
90-
self._bmp = None
91-
self._data = ctypes.create_string_buffer(0) # type: ctypes.Array[ctypes.c_char]
92-
9384
self.monitorenumproc = ctypes.WINFUNCTYPE(
9485
INT, DWORD, DWORD, ctypes.POINTER(RECT), DOUBLE
9586
)
@@ -99,8 +90,12 @@ def __init__(self, **_):
9990
self._set_cfunctions()
10091
self._set_dpi_awareness()
10192

102-
self._srcdc = self.user32.GetWindowDC(0)
103-
self._memdc = self.gdi32.CreateCompatibleDC(self._srcdc)
93+
self._bbox = {"height": 0, "width": 0}
94+
self._data = ctypes.create_string_buffer(0) # type: ctypes.Array[ctypes.c_char]
95+
96+
if not MSS.srcdc or not MSS.memdc:
97+
MSS.srcdc = self.user32.GetWindowDC(0)
98+
MSS.memdc = self.gdi32.CreateCompatibleDC(MSS.srcdc)
10499

105100
bmi = BITMAPINFO()
106101
bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
@@ -129,17 +124,13 @@ def _set_cfunctions(self):
129124
self._cfactory(
130125
attr=self.user32, func="GetWindowDC", argtypes=[HWND], restype=HDC
131126
)
132-
self._cfactory(
133-
attr=self.user32, func="ReleaseDC", argtypes=[HWND, HGDIOBJ], restype=INT
134-
)
135127

136128
self._cfactory(
137129
attr=self.gdi32, func="GetDeviceCaps", argtypes=[HWND, INT], restype=INT
138130
)
139131
self._cfactory(
140132
attr=self.gdi32, func="CreateCompatibleDC", argtypes=[HDC], restype=HDC
141133
)
142-
self._cfactory(attr=self.gdi32, func="DeleteDC", argtypes=[HDC], restype=BOOL)
143134
self._cfactory(
144135
attr=self.gdi32,
145136
func="CreateCompatibleBitmap",
@@ -168,32 +159,10 @@ def _set_cfunctions(self):
168159
restype=BOOL,
169160
)
170161

171-
def close(self):
172-
# type: () -> None
173-
""" Close GDI handles and free DCs. """
174-
175-
try:
176-
self.gdi32.DeleteObject(self._bmp)
177-
del self._bmp
178-
except AttributeError:
179-
pass
180-
181-
try:
182-
self.gdi32.DeleteDC(self._memdc)
183-
del self._memdc
184-
except (OSError, AttributeError):
185-
pass
186-
187-
try:
188-
self.user32.ReleaseDC(0, self._srcdc)
189-
del self._srcdc
190-
except AttributeError:
191-
pass
192-
193162
def _set_dpi_awareness(self):
194163
""" Set DPI aware to capture full screen on Hi-DPI monitors. """
195164

196-
version = sys.getwindowsversion()[:2]
165+
version = sys.getwindowsversion()[:2] # pylint: disable=no-member
197166
if version >= (6, 3):
198167
# Windows 8.1+
199168
# Here 2 = PROCESS_PER_MONITOR_DPI_AWARE, which means:
@@ -288,29 +257,32 @@ def grab(self, monitor):
288257
"height": monitor[3] - monitor[1],
289258
}
290259

260+
srcdc, memdc = MSS.srcdc, MSS.memdc
291261
width, height = monitor["width"], monitor["height"]
292262

293263
if (self._bbox["height"], self._bbox["width"]) != (height, width):
294264
self._bbox = monitor
295265
self._bmi.bmiHeader.biWidth = width
296266
self._bmi.bmiHeader.biHeight = -height # Why minus? [1]
297267
self._data = ctypes.create_string_buffer(width * height * 4) # [2]
298-
self._bmp = self.gdi32.CreateCompatibleBitmap(self._srcdc, width, height)
299-
self.gdi32.SelectObject(self._memdc, self._bmp)
268+
if MSS.bmp:
269+
self.gdi32.DeleteObject(MSS.bmp)
270+
MSS.bmp = self.gdi32.CreateCompatibleBitmap(srcdc, width, height)
271+
self.gdi32.SelectObject(memdc, MSS.bmp)
300272

301273
self.gdi32.BitBlt(
302-
self._memdc,
274+
memdc,
303275
0,
304276
0,
305277
width,
306278
height,
307-
self._srcdc,
279+
srcdc,
308280
monitor["left"],
309281
monitor["top"],
310282
SRCCOPY | CAPTUREBLT,
311283
)
312284
bits = self.gdi32.GetDIBits(
313-
self._memdc, self._bmp, 0, height, self._data, self._bmi, DIB_RGB_COLORS
285+
memdc, MSS.bmp, 0, height, self._data, self._bmi, DIB_RGB_COLORS
314286
)
315287
if bits != height:
316288
raise ScreenShotError("gdi32.GetDIBits() failed.")

0 commit comments

Comments
 (0)