diff --git a/src/mss/linux.py b/src/mss/linux.py index 009b4234..66d16434 100644 --- a/src/mss/linux.py +++ b/src/mss/linux.py @@ -250,6 +250,7 @@ def _validate(retval: int, func: Any, args: tuple[Any, Any], /) -> tuple[Any, An "XGetWindowAttributes": ("xlib", [POINTER(Display), POINTER(XWindowAttributes), POINTER(XWindowAttributes)], c_int), "XOpenDisplay": ("xlib", [c_char_p], POINTER(Display)), "XQueryExtension": ("xlib", [POINTER(Display), c_char_p, POINTER(c_int), POINTER(c_int), POINTER(c_int)], c_uint), + "XRRQueryVersion": ("xrandr", [POINTER(Display), POINTER(c_int), POINTER(c_int)], c_int), "XRRFreeCrtcInfo": ("xrandr", [POINTER(XRRCrtcInfo)], c_void_p), "XRRFreeScreenResources": ("xrandr", [POINTER(XRRScreenResources)], c_void_p), "XRRGetCrtcInfo": ("xrandr", [POINTER(Display), POINTER(XRRScreenResources), c_long], POINTER(XRRCrtcInfo)), @@ -388,6 +389,10 @@ def _monitors_impl(self) -> None: int_ = int xrandr = self.xrandr + xrandr_major = c_int(0) + xrandr_minor = c_int(0) + xrandr.XRRQueryVersion(display, xrandr_major, xrandr_minor) + # All monitors gwa = XWindowAttributes() self.xlib.XGetWindowAttributes(display, self._handles.root, byref(gwa)) @@ -400,9 +405,11 @@ def _monitors_impl(self) -> None: # XRRGetScreenResources(): 0.1755971429956844 s # XRRGetScreenResourcesCurrent(): 0.0039125580078689 s # The second is faster by a factor of 44! So try to use it first. - try: + # It doesn't query the monitors for updated information, but it does require the server to support + # RANDR 1.3. We also make sure the client supports 1.3, by checking for the presence of the function. + if hasattr(xrandr, "XRRGetScreenResourcesCurrent") and (xrandr_major.value, xrandr_minor.value) >= (1, 3): mon = xrandr.XRRGetScreenResourcesCurrent(display, self._handles.drawable).contents - except AttributeError: + else: mon = xrandr.XRRGetScreenResources(display, self._handles.drawable).contents crtcs = mon.crtcs diff --git a/src/tests/test_gnu_linux.py b/src/tests/test_gnu_linux.py index 87d53eea..2ed417fa 100644 --- a/src/tests/test_gnu_linux.py +++ b/src/tests/test_gnu_linux.py @@ -4,7 +4,9 @@ import platform from collections.abc import Generator -from unittest.mock import Mock, patch +from ctypes import CFUNCTYPE, POINTER, _Pointer, c_int +from typing import Any +from unittest.mock import Mock, NonCallableMock, patch import pytest @@ -22,6 +24,14 @@ DEPTH = 24 +def spy_and_patch(monkeypatch: pytest.MonkeyPatch, obj: Any, name: str) -> Mock: + """Replace obj.name with a call-through mock and return the mock.""" + real = getattr(obj, name) + spy = Mock(wraps=real) + monkeypatch.setattr(obj, name, spy, raising=False) + return spy + + @pytest.fixture def display() -> Generator: with pyvirtualdisplay.Display(size=(WIDTH, HEIGHT), color_depth=DEPTH) as vdisplay: @@ -133,20 +143,70 @@ def test__is_extension_enabled_unknown_name(display: str) -> None: assert not sct._is_extension_enabled("NOEXT") -def test_missing_fast_function_for_monitor_details_retrieval(display: str) -> None: +def test_fast_function_for_monitor_details_retrieval(display: str, monkeypatch: pytest.MonkeyPatch) -> None: with mss.mss(display=display) as sct: assert isinstance(sct, mss.linux.MSS) # For Mypy assert hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent") + fast_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResourcesCurrent") + slow_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResources") screenshot_with_fast_fn = sct.grab(sct.monitors[1]) + fast_spy.assert_called() + slow_spy.assert_not_called() + assert set(screenshot_with_fast_fn.rgb) == {0} + +def test_client_missing_fast_function_for_monitor_details_retrieval( + display: str, monkeypatch: pytest.MonkeyPatch +) -> None: with mss.mss(display=display) as sct: assert isinstance(sct, mss.linux.MSS) # For Mypy assert hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent") - del sct.xrandr.XRRGetScreenResourcesCurrent + # Even though we're going to delete it, we'll still create a fast spy, to make sure that it isn't somehow + # getting accessed through a path we hadn't considered. + fast_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResourcesCurrent") + slow_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResources") + # If we just delete sct.xrandr.XRRGetScreenResourcesCurrent, it will get recreated automatically by ctypes + # the next time it's accessed. A Mock will remember that the attribute was explicitly deleted and hide it. + mock_xrandr = NonCallableMock(wraps=sct.xrandr) + del mock_xrandr.XRRGetScreenResourcesCurrent + monkeypatch.setattr(sct, "xrandr", mock_xrandr) + assert not hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent") screenshot_with_slow_fn = sct.grab(sct.monitors[1]) + fast_spy.assert_not_called() + slow_spy.assert_called() + + assert set(screenshot_with_slow_fn.rgb) == {0} + + +def test_server_missing_fast_function_for_monitor_details_retrieval( + display: str, monkeypatch: pytest.MonkeyPatch +) -> None: + fake_xrrqueryversion_type = CFUNCTYPE( + c_int, # Status + POINTER(mss.linux.Display), # Display* + POINTER(c_int), # int* major + POINTER(c_int), # int* minor + ) + + @fake_xrrqueryversion_type + def fake_xrrqueryversion(_dpy: _Pointer, major_p: _Pointer, minor_p: _Pointer) -> int: + major_p[0] = 1 + minor_p[0] = 2 + return 1 + + with mss.mss(display=display) as sct: + assert isinstance(sct, mss.linux.MSS) # For Mypy + monkeypatch.setattr(sct.xrandr, "XRRQueryVersion", fake_xrrqueryversion) + fast_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResourcesCurrent") + slow_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResources") + screenshot_with_slow_fn = sct.grab(sct.monitors[1]) + + fast_spy.assert_not_called() + slow_spy.assert_called() + assert set(screenshot_with_slow_fn.rgb) == {0}