11from __future__ import annotations
22
3- import errno
3+ import enum
44import os
55from mmap import PROT_READ , mmap # type: ignore[attr-defined]
6- from typing import TYPE_CHECKING , Any , Literal
6+ from typing import TYPE_CHECKING , Any
77
88from mss .exception import ScreenShotError
99from mss .linux import xcb
1616 from mss .screenshot import ScreenShot
1717
1818
19+ class ShmStatus (enum .Enum ):
20+ UNKNOWN = enum .auto ()
21+ AVAILABLE = enum .auto ()
22+ UNAVAILABLE = enum .auto ()
23+
24+
1925class MSS (MSSXCBBase ):
2026 """Multiple ScreenShots implementation for GNU/Linux.
2127
@@ -31,65 +37,78 @@ def __init__(self, /, **kwargs: Any) -> None:
3137 self ._buf : mmap | None = None
3238 self ._shmseg : xcb .ShmSeg | None = None
3339
34- # _shm_works is True if at least one screenshot has been taken, False if it's known to fail, and None until
35- # then.
36- self ._shm_works : bool | None = self ._setup_shm ()
40+ self .shm_status : ShmStatus = self ._setup_shm ()
41+ self .shm_failed_reason : str | None = None
3742
3843 def _shm_report_issue (self , msg : str , * args : Any ) -> None :
3944 """Debugging hook for troubleshooting MIT-SHM issues.
4045
4146 This will be called whenever MIT-SHM is disabled. The optional
4247 arguments are not well-defined; exceptions are common.
4348 """
44- print (msg , args )
49+ full_msg = msg
50+ if args :
51+ full_msg += " | " + ", " .join (str (arg ) for arg in args )
52+ self .shm_failed_reason = full_msg
4553
46- def _setup_shm (self ) -> Literal [ False ] | None :
54+ def _setup_shm (self ) -> ShmStatus : # noqa: PLR0911
4755 assert self .conn is not None # noqa: S101
4856
49- shm_ext_data = xcb .get_extension_data (self .conn , LIB .shm_id )
50- if not shm_ext_data .present :
51- self ._shm_report_issue ("MIT-SHM extension not present" )
52- return False
53-
54- # We use the FD-based version of ShmGetImage, so we require the extension to be at least 1.3.
55- shm_version_data = xcb .shm_query_version (self .conn )
56- shm_version = (shm_version_data .major_version , shm_version_data .minor_version )
57- if shm_version < (1 , 2 ):
58- self ._shm_report_issue ("MIT-SHM version too old" , shm_version )
59- return False
60-
61- # We allocate something large enough for the root, so we don't have to reallocate each time the window is
62- # resized.
63- # TODO(jholveck): Check in _grab_impl that we're not going to exceed this size. That can happen if the
64- # root is resized.
65- size = self .pref_screen .width_in_pixels * self .pref_screen .height_in_pixels * 4
66-
6757 try :
68- self ._memfd = os .memfd_create ("mss-shm-buf" , flags = os .MFD_CLOEXEC ) # type: ignore[attr-defined]
69- except OSError as e :
70- self ._shm_report_issue ("memfd_create failed" , e )
71- self ._shutdown_shm ()
72- return False
73- os .ftruncate (self ._memfd , size )
58+ shm_ext_data = xcb .get_extension_data (self .conn , LIB .shm_id )
59+ if not shm_ext_data .present :
60+ self ._shm_report_issue ("MIT-SHM extension not present" )
61+ return ShmStatus .UNAVAILABLE
62+
63+ # We use the FD-based version of ShmGetImage, so we require the extension to be at least 1.2.
64+ shm_version_data = xcb .shm_query_version (self .conn )
65+ shm_version = (shm_version_data .major_version , shm_version_data .minor_version )
66+ if shm_version < (1 , 2 ):
67+ self ._shm_report_issue ("MIT-SHM version too old" , shm_version )
68+ return ShmStatus .UNAVAILABLE
69+
70+ # We allocate something large enough for the root, so we don't have to reallocate each time the window is
71+ # resized.
72+ self ._bufsize = self .pref_screen .width_in_pixels * self .pref_screen .height_in_pixels * 4
73+
74+ if not hasattr (os , "memfd_create" ):
75+ self ._shm_report_issue ("os.memfd_create not available" )
76+ return ShmStatus .UNAVAILABLE
77+ try :
78+ self ._memfd = os .memfd_create ("mss-shm-buf" , flags = os .MFD_CLOEXEC ) # type: ignore[attr-defined]
79+ except OSError as e :
80+ self ._shm_report_issue ("memfd_create failed" , e )
81+ self ._shutdown_shm ()
82+ return ShmStatus .UNAVAILABLE
83+ os .ftruncate (self ._memfd , self ._bufsize )
7484
75- try :
76- self ._buf = mmap (self ._memfd , size , prot = PROT_READ ) # type: ignore[call-arg]
77- except OSError as e :
78- self ._shm_report_issue ("mmap failed" , e )
79- self ._shutdown_shm ()
80- return False
85+ try :
86+ self ._buf = mmap (self ._memfd , self . _bufsize , prot = PROT_READ ) # type: ignore[call-arg]
87+ except OSError as e :
88+ self ._shm_report_issue ("mmap failed" , e )
89+ self ._shutdown_shm ()
90+ return ShmStatus . UNAVAILABLE
8191
82- self ._shmseg = xcb .ShmSeg (xcb .generate_id (self .conn ).value )
83- try :
84- # This will normally be what raises an exception if you're on a remote connection. I previously thought
85- # the server deferred that until the GetImage call, but I had not been properly checking the status here.
86- xcb .shm_attach_fd (self .conn , self ._shmseg , self ._memfd , read_only = False )
87- except xcb .XError as e :
88- self ._shm_report_issue ("Cannot attach MIT-SHM segment" , e )
92+ self ._shmseg = xcb .ShmSeg (xcb .generate_id (self .conn ).value )
93+ try :
94+ # This will normally be what raises an exception if you're on a remote connection. (I previously
95+ # thought the server deferred that until the GetImage call, but I had not been properly checking the
96+ # status.)
97+ # This will close _memfd, on success or on failure.
98+ try :
99+ xcb .shm_attach_fd (self .conn , self ._shmseg , self ._memfd , read_only = False )
100+ finally :
101+ self ._memfd = None
102+ except xcb .XError as e :
103+ self ._shm_report_issue ("Cannot attach MIT-SHM segment" , e )
104+ self ._shutdown_shm ()
105+ return ShmStatus .UNAVAILABLE
106+
107+ except Exception :
89108 self ._shutdown_shm ()
90- return False
109+ raise
91110
92- return None
111+ return ShmStatus . UNKNOWN
93112
94113 def close (self ) -> None :
95114 self ._shutdown_shm ()
@@ -103,15 +122,7 @@ def _shutdown_shm(self) -> None:
103122 self ._buf .close ()
104123 self ._buf = None
105124 if self ._memfd is not None :
106- # TODO(jholveck): For some reason, at this point, self._memfd is no longer valid. If I try to close it,
107- # I get EBADF, even if I try to close it before closing the mmap. The theories I have about this involve
108- # the mmap object taking control, but it doesn't make sense that I could still use shm_attach_fd in that
109- # case. I need to investigate before releasing.
110- try :
111- os .close (self ._memfd )
112- except OSError as e :
113- if e .errno != errno .EBADF :
114- raise
125+ os .close (self ._memfd )
115126 self ._memfd = None
116127
117128 def _grab_impl_xshmgetimage (self , monitor : Monitor ) -> ScreenShot :
@@ -121,6 +132,16 @@ def _grab_impl_xshmgetimage(self, monitor: Monitor) -> ScreenShot:
121132 assert self ._buf is not None # noqa: S101
122133 assert self ._shmseg is not None # noqa: S101
123134
135+ required_size = monitor ["width" ] * monitor ["height" ] * 4
136+ if required_size > self ._bufsize :
137+ # This is temporary. The permanent fix will depend on how
138+ # issue https://github.com/BoboTiG/python-mss/issues/432 is resolved.
139+ msg = (
140+ "Requested capture size exceeds the allocated buffer. If you have resized the screen, "
141+ "please recreate your MSS object."
142+ )
143+ raise ScreenShotError (msg )
144+
124145 img_reply = xcb .shm_get_image (
125146 self .conn ,
126147 self .drawable .value ,
@@ -154,19 +175,19 @@ def _grab_impl_xshmgetimage(self, monitor: Monitor) -> ScreenShot:
154175
155176 def _grab_impl (self , monitor : Monitor ) -> ScreenShot :
156177 """Retrieve all pixels from a monitor. Pixels have to be RGBX."""
157- if self ._shm_works == False : # noqa: E712
178+ if self .shm_status == ShmStatus . UNAVAILABLE :
158179 return super ()._grab_impl_xgetimage (monitor )
159180
160181 try :
161182 rv = self ._grab_impl_xshmgetimage (monitor )
162183 except XProtoError as e :
163- if self ._shm_works is not None :
184+ if self .shm_status != ShmStatus . UNKNOWN :
164185 raise
165186 self ._shm_report_issue ("MIT-SHM GetImage failed" , e )
166- self ._shm_works = False
187+ self .shm_status = ShmStatus . UNAVAILABLE
167188 self ._shutdown_shm ()
168189 rv = super ()._grab_impl_xgetimage (monitor )
169190 else :
170- self ._shm_works = True
191+ self .shm_status = ShmStatus . AVAILABLE
171192
172193 return rv
0 commit comments