Skip to content

Commit 6ba662b

Browse files
jholveckBoboTiG
andauthored
linux: Check the server for Xrandr support version (#417)
* Check the server for Xrandr support version We currently only check the client libXrandr to see if it supports the XRRGetScreenResourcesCurrent function. To make sure the function will work, we also need to check the server's Xrandr version. * Fix the test for missing XRRGetScreenResourcesCurrent Previously, the test would try to monkey-patch the libXrandr module by deleting the XRRGetScreenResourcesCurrent function. However, this actually doesn't work as expected. It deletes all the configuration you've assigned to that C function (argument and return types). But ctypes.CDLL resolves symbols lazily in __getattr__ via dlsym. Deleting the attribute only discards the previously created _FuncPtr object; the next getattr/hasattr on the CDLL will just call dlsym again and recreate it. The existing test's code path seemed to be working, because the implementation was just testing for AttributeError. The assumption had been that if XRRGetScreenResourcesCurrent was deleted, then trying to access it would raise an AttributeError. However, what was actually being raised was when the .contents attribute of the return value was being accessed: since the restype had been erased, then the return value was now an int, not a pointer. XRRGetScreenResourcesCurrent was still getting called, but trying to read its .contents raised the AttributeError. The new test now uses a proxy object to genuinely hide XRRGetScreenResourcesCurrent. Additionally, the test was split into three. The tests now will ensure that the right functions are being called based on whether XRRGetScreenResourcesCurrent is supported, missing on the client library, or missing on the server. --------- Co-authored-by: Mickaël Schoentgen <[email protected]>
1 parent b83cc58 commit 6ba662b

File tree

2 files changed

+72
-5
lines changed

2 files changed

+72
-5
lines changed

src/mss/linux.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ def _validate(retval: int, func: Any, args: tuple[Any, Any], /) -> tuple[Any, An
250250
"XGetWindowAttributes": ("xlib", [POINTER(Display), POINTER(XWindowAttributes), POINTER(XWindowAttributes)], c_int),
251251
"XOpenDisplay": ("xlib", [c_char_p], POINTER(Display)),
252252
"XQueryExtension": ("xlib", [POINTER(Display), c_char_p, POINTER(c_int), POINTER(c_int), POINTER(c_int)], c_uint),
253+
"XRRQueryVersion": ("xrandr", [POINTER(Display), POINTER(c_int), POINTER(c_int)], c_int),
253254
"XRRFreeCrtcInfo": ("xrandr", [POINTER(XRRCrtcInfo)], c_void_p),
254255
"XRRFreeScreenResources": ("xrandr", [POINTER(XRRScreenResources)], c_void_p),
255256
"XRRGetCrtcInfo": ("xrandr", [POINTER(Display), POINTER(XRRScreenResources), c_long], POINTER(XRRCrtcInfo)),
@@ -388,6 +389,10 @@ def _monitors_impl(self) -> None:
388389
int_ = int
389390
xrandr = self.xrandr
390391

392+
xrandr_major = c_int(0)
393+
xrandr_minor = c_int(0)
394+
xrandr.XRRQueryVersion(display, xrandr_major, xrandr_minor)
395+
391396
# All monitors
392397
gwa = XWindowAttributes()
393398
self.xlib.XGetWindowAttributes(display, self._handles.root, byref(gwa))
@@ -400,9 +405,11 @@ def _monitors_impl(self) -> None:
400405
# XRRGetScreenResources(): 0.1755971429956844 s
401406
# XRRGetScreenResourcesCurrent(): 0.0039125580078689 s
402407
# The second is faster by a factor of 44! So try to use it first.
403-
try:
408+
# It doesn't query the monitors for updated information, but it does require the server to support
409+
# RANDR 1.3. We also make sure the client supports 1.3, by checking for the presence of the function.
410+
if hasattr(xrandr, "XRRGetScreenResourcesCurrent") and (xrandr_major.value, xrandr_minor.value) >= (1, 3):
404411
mon = xrandr.XRRGetScreenResourcesCurrent(display, self._handles.drawable).contents
405-
except AttributeError:
412+
else:
406413
mon = xrandr.XRRGetScreenResources(display, self._handles.drawable).contents
407414

408415
crtcs = mon.crtcs

src/tests/test_gnu_linux.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
import platform
66
from collections.abc import Generator
7-
from unittest.mock import Mock, patch
7+
from ctypes import CFUNCTYPE, POINTER, _Pointer, c_int
8+
from typing import Any
9+
from unittest.mock import Mock, NonCallableMock, patch
810

911
import pytest
1012

@@ -22,6 +24,14 @@
2224
DEPTH = 24
2325

2426

27+
def spy_and_patch(monkeypatch: pytest.MonkeyPatch, obj: Any, name: str) -> Mock:
28+
"""Replace obj.name with a call-through mock and return the mock."""
29+
real = getattr(obj, name)
30+
spy = Mock(wraps=real)
31+
monkeypatch.setattr(obj, name, spy, raising=False)
32+
return spy
33+
34+
2535
@pytest.fixture
2636
def display() -> Generator:
2737
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:
133143
assert not sct._is_extension_enabled("NOEXT")
134144

135145

136-
def test_missing_fast_function_for_monitor_details_retrieval(display: str) -> None:
146+
def test_fast_function_for_monitor_details_retrieval(display: str, monkeypatch: pytest.MonkeyPatch) -> None:
137147
with mss.mss(display=display) as sct:
138148
assert isinstance(sct, mss.linux.MSS) # For Mypy
139149
assert hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent")
150+
fast_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResourcesCurrent")
151+
slow_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResources")
140152
screenshot_with_fast_fn = sct.grab(sct.monitors[1])
141153

154+
fast_spy.assert_called()
155+
slow_spy.assert_not_called()
156+
142157
assert set(screenshot_with_fast_fn.rgb) == {0}
143158

159+
160+
def test_client_missing_fast_function_for_monitor_details_retrieval(
161+
display: str, monkeypatch: pytest.MonkeyPatch
162+
) -> None:
144163
with mss.mss(display=display) as sct:
145164
assert isinstance(sct, mss.linux.MSS) # For Mypy
146165
assert hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent")
147-
del sct.xrandr.XRRGetScreenResourcesCurrent
166+
# Even though we're going to delete it, we'll still create a fast spy, to make sure that it isn't somehow
167+
# getting accessed through a path we hadn't considered.
168+
fast_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResourcesCurrent")
169+
slow_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResources")
170+
# If we just delete sct.xrandr.XRRGetScreenResourcesCurrent, it will get recreated automatically by ctypes
171+
# the next time it's accessed. A Mock will remember that the attribute was explicitly deleted and hide it.
172+
mock_xrandr = NonCallableMock(wraps=sct.xrandr)
173+
del mock_xrandr.XRRGetScreenResourcesCurrent
174+
monkeypatch.setattr(sct, "xrandr", mock_xrandr)
175+
assert not hasattr(sct.xrandr, "XRRGetScreenResourcesCurrent")
148176
screenshot_with_slow_fn = sct.grab(sct.monitors[1])
149177

178+
fast_spy.assert_not_called()
179+
slow_spy.assert_called()
180+
181+
assert set(screenshot_with_slow_fn.rgb) == {0}
182+
183+
184+
def test_server_missing_fast_function_for_monitor_details_retrieval(
185+
display: str, monkeypatch: pytest.MonkeyPatch
186+
) -> None:
187+
fake_xrrqueryversion_type = CFUNCTYPE(
188+
c_int, # Status
189+
POINTER(mss.linux.Display), # Display*
190+
POINTER(c_int), # int* major
191+
POINTER(c_int), # int* minor
192+
)
193+
194+
@fake_xrrqueryversion_type
195+
def fake_xrrqueryversion(_dpy: _Pointer, major_p: _Pointer, minor_p: _Pointer) -> int:
196+
major_p[0] = 1
197+
minor_p[0] = 2
198+
return 1
199+
200+
with mss.mss(display=display) as sct:
201+
assert isinstance(sct, mss.linux.MSS) # For Mypy
202+
monkeypatch.setattr(sct.xrandr, "XRRQueryVersion", fake_xrrqueryversion)
203+
fast_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResourcesCurrent")
204+
slow_spy = spy_and_patch(monkeypatch, sct.xrandr, "XRRGetScreenResources")
205+
screenshot_with_slow_fn = sct.grab(sct.monitors[1])
206+
207+
fast_spy.assert_not_called()
208+
slow_spy.assert_called()
209+
150210
assert set(screenshot_with_slow_fn.rgb) == {0}
151211

152212

0 commit comments

Comments
 (0)