VirtualDesktop 1.6.5__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,576 @@
1
+ """Windows version detection and API-availability fallbacks.
2
+
3
+ Everything in this project has to behave identically on Windows 10 (1607 through 22H2) and
4
+ Windows 11 (21H2 through 24H2+). The APIs involved did not all arrive at once, and a few of
5
+ them changed *behaviour* rather than availability:
6
+
7
+ =========================== ========================== ==================================
8
+ API / behaviour First available Fallback used here
9
+ =========================== ========================== ==================================
10
+ ``SetProcessDpiAwareness`` Win 8.1 (shcore) ``SetProcessDPIAware`` (Vista+)
11
+ ``SetProcessDpiAwareness`` — *and* check the HRESULT: on Win 8.1
12
+ ``Context`` (pmv2) Win 10 1703 it fails with ``E_ACCESSDENIED``
13
+ once awareness is already set
14
+ ``GetDpiForWindow`` Win 10 1607 ``GetDpiForMonitor`` then the
15
+ monitor DC's ``LOGPIXELSX``
16
+ ``GetDpiForMonitor`` Win 8.1 (shcore) the monitor DC's ``LOGPIXELSX``
17
+ ``DwmGetWindowAttribute`` Vista (dwmapi) ``GetWindowRect`` fallback
18
+ ``PrintWindow`` + ``PW_RENDERFULLCONTENT`` plain ``PrintWindow``, ``WM_PRINT``,
19
+ ``PW_RENDERFULLCONTENT`` Win 8.1 ``BitBlt``
20
+ ``ShellExecuteExW`` runas Vista —
21
+ ``CreateDesktopW`` Win 2000 —
22
+ =========================== ========================== ==================================
23
+
24
+ The rule for callers: ask this module, never ``platform.version()`` directly, and never assume
25
+ a *newer* build implies a capability — Windows 10 and Windows 11 both ship builds in which an
26
+ optional API is present but returns a failure code.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import ctypes
32
+ import os
33
+ import sys
34
+ from ctypes import wintypes
35
+ from typing import Optional, Tuple
36
+
37
+ from ._log import get_logger
38
+
39
+ log = get_logger("compat")
40
+
41
+ __all__ = [
42
+ "WINDOWS_10_1607",
43
+ "WINDOWS_10_1703",
44
+ "WINDOWS_10_1809",
45
+ "WINDOWS_11_21H2",
46
+ "windows_build",
47
+ "windows_release",
48
+ "is_windows_11",
49
+ "at_least",
50
+ "describe_windows",
51
+ "supports_pmv2",
52
+ "set_process_dpi_awareness",
53
+ "get_process_dpi_awareness",
54
+ "dpi_awareness_name",
55
+ "monitor_dpi_legacy",
56
+ "window_dpi_compat",
57
+ "IDDCX_MIN_BUILD",
58
+ "iddcx_support",
59
+ "windows_compatibility_report",
60
+ ]
61
+
62
+ #: Build thresholds. Windows 10 and Windows 11 share the same major version (10.0), so the
63
+ #: build number is the only reliable discriminator — ``platform.release()`` is "10" on both.
64
+ WINDOWS_10_1607 = 14393
65
+ WINDOWS_10_1703 = 15063
66
+ WINDOWS_10_1809 = 17763
67
+ WINDOWS_11_21H2 = 22000
68
+
69
+ #: ``RtlGetVersion`` is used instead of ``GetVersionExW`` because the latter lies unless the
70
+ #: executable carries a compatibility manifest — a detail that differs between Python builds.
71
+ _RTL_OSVERSIONINFOW_SIZE = 6 * 4 + 128 * 2
72
+
73
+
74
+ class _OSVERSIONINFOW(ctypes.Structure):
75
+ _fields_ = [
76
+ ("dwOSVersionInfoSize", wintypes.DWORD),
77
+ ("dwMajorVersion", wintypes.DWORD),
78
+ ("dwMinorVersion", wintypes.DWORD),
79
+ ("dwBuildNumber", wintypes.DWORD),
80
+ ("dwPlatformId", wintypes.DWORD),
81
+ ("szCSDVersion", ctypes.c_wchar * 128),
82
+ ]
83
+
84
+
85
+ def _rtl_version() -> Optional[Tuple[int, int, int]]:
86
+ try:
87
+ ntdll = ctypes.WinDLL("ntdll", use_last_error=True)
88
+ info = _OSVERSIONINFOW()
89
+ info.dwOSVersionInfoSize = ctypes.sizeof(_OSVERSIONINFOW)
90
+ if ntdll.RtlGetVersion(ctypes.byref(info)) != 0:
91
+ return None
92
+ return int(info.dwMajorVersion), int(info.dwMinorVersion), int(info.dwBuildNumber)
93
+ except Exception: # pragma: no cover - non-Windows / blocked ntdll
94
+ return None
95
+
96
+
97
+ def windows_build() -> int:
98
+ """The OS build number (``22000``+ on Windows 11, ``10240``..``19045`` on Windows 10)."""
99
+ version = _rtl_version()
100
+ if version is not None:
101
+ return version[2]
102
+ # ``sys.getwindowsversion()`` is the documented Python accessor and needs no ctypes.
103
+ try:
104
+ return int(sys.getwindowsversion().build)
105
+ except Exception: # pragma: no cover - non-Windows
106
+ return 0
107
+
108
+
109
+ def windows_release() -> str:
110
+ """``"11"``, ``"10"``, ``"8.1"`` … — derived from the build, not from ``platform``."""
111
+ build = windows_build()
112
+ if build >= WINDOWS_11_21H2:
113
+ return "11"
114
+ if build >= 10240:
115
+ return "10"
116
+ version = _rtl_version()
117
+ if version is None:
118
+ return "unknown"
119
+ return f"{version[0]}.{version[1]}"
120
+
121
+
122
+ def is_windows_11() -> bool:
123
+ return windows_build() >= WINDOWS_11_21H2
124
+
125
+
126
+ def at_least(build: int) -> bool:
127
+ return windows_build() >= build
128
+
129
+
130
+ def describe_windows() -> str:
131
+ build = windows_build()
132
+ return f"Windows {windows_release()} (build {build or 'unknown'})"
133
+
134
+
135
+ # --------------------------------------------------------------------------------------
136
+ # DPI awareness
137
+ # --------------------------------------------------------------------------------------
138
+
139
+ #: ``DPI_AWARENESS_CONTEXT`` pseudo-handles. They are only understood by
140
+ #: ``SetProcessDpiAwarenessContext``, and passing them to the older APIs fails.
141
+ DPI_AWARENESS_CONTEXT_UNAWARE = -1
142
+ DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = -2
143
+ DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = -3
144
+ DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4
145
+ DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED = -5
146
+
147
+ _SHCORE_PROCESS_DPI_AWARENESS = {
148
+ -1: 0, # unaware
149
+ -2: 1, # system aware
150
+ -3: 2, # per monitor
151
+ -4: 2, # per monitor v2 is only expressible through the context API
152
+ }
153
+ _PROCESS_DPI_AWARENESS_NAMES = {0: "unaware", 1: "system-aware", 2: "per-monitor-aware"}
154
+ E_ACCESSDENIED = -2147024891 # 0x80070005, returned as a signed HRESULT by shcore
155
+
156
+ _user32 = None
157
+ _shcore = None
158
+
159
+ #: Whether ``SetProcessDpiAwarenessContext`` exists. ``ctypes`` does **not** expose a
160
+ #: missing export as ``AttributeError`` — it raises ``OSError`` on the first *access* — so
161
+ #: ``hasattr`` alone would answer "yes" on Windows 8.1 and then crash on the call. The probe
162
+ #: below resolves the symbol once, at import time, and records the truth.
163
+ _HAS_CONTEXT_API = False
164
+ _HAS_AWARENESS_API = False
165
+ if sys.platform == "win32": # pragma: no branch
166
+ try:
167
+ _user32 = ctypes.WinDLL("user32", use_last_error=True)
168
+ try:
169
+ _user32.SetProcessDpiAwarenessContext.argtypes = [ctypes.c_void_p]
170
+ _user32.SetProcessDpiAwarenessContext.restype = wintypes.BOOL
171
+ _HAS_CONTEXT_API = True
172
+ except OSError: # pragma: no cover - Windows 8.1 / 7
173
+ _HAS_CONTEXT_API = False
174
+ try:
175
+ _user32.GetThreadDpiAwarenessContext.argtypes = []
176
+ _user32.GetThreadDpiAwarenessContext.restype = ctypes.c_void_p
177
+ _HAS_CONTEXT_API = True
178
+ except OSError: # pragma: no cover - pre-1607
179
+ _HAS_CONTEXT_API = False
180
+ try:
181
+ _user32.GetAwarenessFromDpiAwarenessContext.argtypes = [ctypes.c_void_p]
182
+ _user32.GetAwarenessFromDpiAwarenessContext.restype = ctypes.c_int
183
+ _HAS_AWARENESS_API = True
184
+ except OSError: # pragma: no cover - pre-1607
185
+ _HAS_AWARENESS_API = False
186
+ _user32.SetProcessDPIAware.argtypes = []
187
+ _user32.SetProcessDPIAware.restype = wintypes.BOOL
188
+ except Exception: # pragma: no cover - very old user32
189
+ _user32 = None
190
+ try:
191
+ _shcore = ctypes.WinDLL("shcore", use_last_error=True)
192
+ _shcore.SetProcessDpiAwareness.argtypes = [ctypes.c_int]
193
+ _shcore.SetProcessDpiAwareness.restype = ctypes.c_long
194
+ _shcore.GetProcessDpiAwareness.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_int)]
195
+ _shcore.GetProcessDpiAwareness.restype = ctypes.c_long
196
+ except Exception: # pragma: no cover - Windows 8.0 / 7
197
+ _shcore = None
198
+
199
+
200
+ def supports_pmv2() -> bool:
201
+ """True when ``SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2)`` can be called."""
202
+ return _HAS_CONTEXT_API
203
+
204
+
205
+ def set_process_dpi_awareness(awareness: int = DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) -> dict:
206
+ """Become DPI aware, trying every API the running build offers, in order.
207
+
208
+ Returns ``{"applied": <context|None>, "api": <name|None>, "attempts": [...]}``. The
209
+ ``attempts`` list is kept so a caller can log *why* a fallback was taken — the interesting
210
+ case is Windows 8.1, where ``shcore`` exists but returns ``E_ACCESSDENIED`` once the
211
+ process awareness has already been set by an earlier call.
212
+ """
213
+ result: dict = {"applied": None, "api": None, "attempts": [], "build": windows_build()}
214
+ if sys.platform != "win32": # pragma: no cover - Windows-only library
215
+ return result
216
+
217
+ # (1) the modern context API (Win 10 1703+). Fails on older builds by absence.
218
+ if supports_pmv2():
219
+ try:
220
+ if _user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(awareness)):
221
+ result.update(applied=awareness, api="SetProcessDpiAwarenessContext")
222
+ result["attempts"].append({"api": "context", "ok": True})
223
+ return result
224
+ result["attempts"].append(
225
+ {"api": "context", "ok": False, "win32_error": ctypes.get_last_error()}
226
+ )
227
+ except Exception as exc: # noqa: BLE001 - never fatal, always fall through
228
+ result["attempts"].append({"api": "context", "ok": False, "error": str(exc)[:120]})
229
+ else:
230
+ result["attempts"].append({"api": "context", "ok": False, "reason": "not exported"})
231
+
232
+ # (2) per-monitor awareness through shcore (Win 8.1+).
233
+ mapped = _SHCORE_PROCESS_DPI_AWARENESS.get(awareness)
234
+ if _shcore is not None and mapped is not None:
235
+ try:
236
+ code = int(_shcore.SetProcessDpiAwareness(mapped))
237
+ if code == 0:
238
+ result.update(applied=awareness, api="SetProcessDpiAwareness")
239
+ result["attempts"].append({"api": "shcore", "ok": True})
240
+ return result
241
+ result["attempts"].append(
242
+ {
243
+ "api": "shcore",
244
+ "ok": False,
245
+ "hresult": hex(code & 0xFFFFFFFF),
246
+ "reason": "already-set" if code == E_ACCESSDENIED else "error",
247
+ }
248
+ )
249
+ except Exception as exc: # noqa: BLE001
250
+ result["attempts"].append({"api": "shcore", "ok": False, "error": str(exc)[:120]})
251
+ else:
252
+ result["attempts"].append({"api": "shcore", "ok": False, "reason": "unavailable"})
253
+
254
+ # (3) the Vista-era call, which is unconditional and always worth attempting.
255
+ try:
256
+ if _user32 is not None and _user32.SetProcessDPIAware():
257
+ result.update(applied=DPI_AWARENESS_CONTEXT_SYSTEM_AWARE, api="SetProcessDPIAware")
258
+ result["attempts"].append({"api": "legacy", "ok": True})
259
+ return result
260
+ result["attempts"].append(
261
+ {"api": "legacy", "ok": False, "win32_error": ctypes.get_last_error()}
262
+ )
263
+ except Exception as exc: # noqa: BLE001
264
+ result["attempts"].append({"api": "legacy", "ok": False, "error": str(exc)[:120]})
265
+ return result
266
+
267
+
268
+ def get_process_dpi_awareness() -> Optional[str]:
269
+ """``"per-monitor-v2"``, ``"per-monitor-aware"``, ``"system-aware"``, ``"unaware"``…
270
+
271
+ The subtlety that costs a crash if ignored: ``GetThreadDpiAwarenessContext`` does **not**
272
+ reliably return one of the documented negative pseudo-handles. Measured on Windows 11
273
+ 22631 (Python 3.11, unelevated): it returned ``0x6010`` — a real handle. Interpreting that
274
+ as a ``DPI_AWARENESS_CONTEXT_*`` constant and de-referencing it is an access violation, so
275
+ the value is only ever *compared*, never read, and the documented
276
+ ``GetAwarenessFromDpiAwarenessContext`` supplies the awareness level.
277
+
278
+ ``GetAwarenessFromDpiAwarenessContext`` cannot distinguish per-monitor-v1 from v2 (both
279
+ report ``DPI_AWARENESS_PER_MONITOR_AWARE``), so the v2 pseudo-handle is probed once by
280
+ *calling* with it and recording the resulting context — a comparison, never a dereference.
281
+ """
282
+ if sys.platform != "win32": # pragma: no cover
283
+ return None
284
+ if _user32 is not None and _HAS_CONTEXT_API and _HAS_AWARENESS_API:
285
+ try:
286
+ context = int(_user32.GetThreadDpiAwarenessContext() or 0)
287
+ if context:
288
+ awareness = int(
289
+ _user32.GetAwarenessFromDpiAwarenessContext(ctypes.c_void_p(context))
290
+ )
291
+ if awareness >= 0 and awareness != 3: # 3 = DPI_AWARENESS_INVALID
292
+ name = _PROCESS_DPI_AWARENESS_NAMES.get(awareness, f"unknown({awareness})")
293
+ if awareness == 2 and _per_monitor_v2_active(context):
294
+ return "per-monitor-v2"
295
+ return name
296
+ except Exception: # pragma: no cover - defensive
297
+ pass
298
+ if _shcore is not None:
299
+ try:
300
+ value = ctypes.c_int()
301
+ if _shcore.GetProcessDpiAwareness(None, ctypes.byref(value)) == 0:
302
+ return _PROCESS_DPI_AWARENESS_NAMES.get(
303
+ int(value.value), f"unknown({value.value})"
304
+ )
305
+ except Exception: # pragma: no cover - defensive
306
+ pass
307
+ return None
308
+
309
+
310
+ def _per_monitor_v2_active(context: int) -> bool:
311
+ """True when ``context`` is the per-monitor-v2 context.
312
+
313
+ Tested by *calling* ``SetThreadDpiAwarenessContext`` with the v2 pseudo-handle: it returns
314
+ the previous context, so ``previous == current-desired`` iff this thread already holds v2.
315
+ That is a comparison of two opaque handles — never a de-reference, which is what makes it
316
+ safe on the builds where the handle is real rather than a pseudo-handle.
317
+ """
318
+ if _user32 is None or not _HAS_CONTEXT_API: # pragma: no cover - pre-1703
319
+ return False
320
+ try:
321
+ _user32.SetThreadDpiAwarenessContext.argtypes = [ctypes.c_void_p]
322
+ _user32.SetThreadDpiAwarenessContext.restype = ctypes.c_void_p
323
+ previous = int(
324
+ _user32.SetThreadDpiAwarenessContext(
325
+ ctypes.c_void_p(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
326
+ )
327
+ or 0
328
+ )
329
+ if previous:
330
+ # Put the thread back exactly as it was.
331
+ _user32.SetThreadDpiAwarenessContext(ctypes.c_void_p(previous))
332
+ now = int(_user32.GetThreadDpiAwarenessContext() or 0)
333
+ return bool(now) and now == previous
334
+ except Exception: # pragma: no cover - defensive
335
+ return False
336
+
337
+
338
+ def dpi_awareness_name() -> str:
339
+ return get_process_dpi_awareness() or "unknown"
340
+
341
+
342
+ # --------------------------------------------------------------------------------------
343
+ # Per-monitor DPI, the pre-1607 way
344
+ # --------------------------------------------------------------------------------------
345
+
346
+
347
+ def window_dpi_compat(hwnd: int, *, fallback: int = 96) -> int:
348
+ """Effective DPI of ``hwnd``, using whichever accessor the running build provides.
349
+
350
+ 1. ``GetDpiForWindow`` — Win 10 1607+, and the only one that accounts for a per-monitor-v2
351
+ window that has been dragged between monitors;
352
+ 2. ``GetDpiForMonitor(MDT_EFFECTIVE_DPI)`` — Win 8.1+, resolved through
353
+ ``MonitorFromWindow(MONITOR_DEFAULTTONEAREST)``;
354
+ 3. the monitor DC's ``LOGPIXELSX`` — available on every supported build.
355
+ """
356
+ if sys.platform != "win32" or not hwnd: # pragma: no cover - Windows-only library
357
+ return fallback
358
+ get_dpi_for_window = None
359
+ monitor_from_window = None
360
+ if _user32 is not None:
361
+ try:
362
+ get_dpi_for_window = _user32.GetDpiForWindow
363
+ except (AttributeError, OSError): # pragma: no cover - pre-1607
364
+ get_dpi_for_window = None
365
+ if get_dpi_for_window is not None:
366
+ try: # a plain callable (a test double) legitimately has no argtypes
367
+ get_dpi_for_window.argtypes = [wintypes.HWND]
368
+ get_dpi_for_window.restype = wintypes.UINT
369
+ except (AttributeError, TypeError):
370
+ pass
371
+ try:
372
+ monitor_from_window = _user32.MonitorFromWindow
373
+ except (AttributeError, OSError): # pragma: no cover - pre-2000
374
+ monitor_from_window = None
375
+ if monitor_from_window is not None:
376
+ try:
377
+ monitor_from_window.argtypes = [wintypes.HWND, wintypes.DWORD]
378
+ monitor_from_window.restype = wintypes.HMONITOR
379
+ except (AttributeError, TypeError):
380
+ pass
381
+ if get_dpi_for_window is not None:
382
+ try:
383
+ value = int(get_dpi_for_window(wintypes.HWND(hwnd)))
384
+ if value:
385
+ return value
386
+ except Exception: # noqa: BLE001 - the call itself failed, try the next accessor
387
+ pass
388
+ monitor = 0
389
+ if monitor_from_window is not None:
390
+ try:
391
+ monitor = int(monitor_from_window(wintypes.HWND(hwnd), 2) or 0)
392
+ except Exception: # noqa: BLE001
393
+ monitor = 0
394
+ if monitor and _shcore is not None:
395
+ try:
396
+ get_dpi_for_monitor = _shcore.GetDpiForMonitor
397
+ get_dpi_for_monitor.argtypes = [
398
+ wintypes.HMONITOR,
399
+ ctypes.c_int,
400
+ ctypes.POINTER(wintypes.UINT),
401
+ ctypes.POINTER(wintypes.UINT),
402
+ ]
403
+ x, y = wintypes.UINT(), wintypes.UINT()
404
+ if get_dpi_for_monitor(
405
+ wintypes.HMONITOR(monitor), 0, ctypes.byref(x), ctypes.byref(y)
406
+ ) == 0 and int(x.value):
407
+ return int(x.value)
408
+ except Exception: # noqa: BLE001 - shcore missing (Windows 8.0) or call failed
409
+ pass
410
+ if monitor:
411
+ value = monitor_dpi_legacy(monitor, fallback=fallback)
412
+ if value:
413
+ return value
414
+ return fallback
415
+
416
+
417
+ def monitor_dpi_legacy(hmonitor: int, *, fallback: int = 96) -> Optional[int]:
418
+ """Effective DPI of ``hmonitor`` using only APIs present since Windows 2000.
419
+
420
+ ``GetDpiForWindow`` (1607+) and ``GetDpiForMonitor`` (8.1+) are the modern accessors, but
421
+ neither exists on every supported build. The device context of a monitor reports
422
+ ``LOGPIXELSX``, and Windows scales that value with the monitor's DPI setting, so it is a
423
+ usable last resort. The DC must be released or GDI leaks one handle per call.
424
+ """
425
+ if sys.platform != "win32" or not hmonitor: # pragma: no cover - Windows-only library
426
+ return None
427
+ LOGPIXELSX = 88
428
+ try:
429
+ gdi32 = ctypes.WinDLL("gdi32", use_last_error=True)
430
+ user32 = ctypes.WinDLL("user32", use_last_error=True)
431
+ user32.GetDC.argtypes = [wintypes.HWND]
432
+ user32.GetDC.restype = wintypes.HDC
433
+ user32.ReleaseDC.argtypes = [wintypes.HWND, wintypes.HDC]
434
+ user32.ReleaseDC.restype = ctypes.c_int
435
+ gdi32.GetDeviceCaps.argtypes = [wintypes.HDC, ctypes.c_int]
436
+ gdi32.GetDeviceCaps.restype = ctypes.c_int
437
+ # A monitor handle is a valid HDC source for GDI; ``GetDC`` accepts it directly.
438
+ dc = user32.GetDC(wintypes.HWND(hmonitor))
439
+ if not dc:
440
+ return None
441
+ try:
442
+ value = int(gdi32.GetDeviceCaps(dc, LOGPIXELSX))
443
+ finally:
444
+ user32.ReleaseDC(wintypes.HWND(hmonitor), dc)
445
+ return value or fallback
446
+ except Exception: # pragma: no cover - defensive
447
+ return None
448
+
449
+
450
+ # --------------------------------------------------------------------------------------
451
+ # Report
452
+ # --------------------------------------------------------------------------------------
453
+
454
+
455
+ def _file_version_dll(path: str) -> Optional[str]:
456
+ """The ``FileVersion`` resource of a system DLL, or ``None``.
457
+
458
+ Read with ``GetFileVersionInfoSizeW``/``GetFileVersionInfoW`` instead of loading the DLL:
459
+ probing by ``LoadLibrary`` would pull a display-driver framework into the process for no
460
+ reason, and on a locked-down build it can fail for unrelated policy reasons.
461
+ """
462
+ if sys.platform != "win32" or not os.path.isfile(path): # pragma: no cover - Windows only
463
+ return None
464
+ try:
465
+ version = ctypes.WinDLL("version", use_last_error=True)
466
+ version.GetFileVersionInfoSizeW.argtypes = [ctypes.c_wchar_p, ctypes.POINTER(wintypes.DWORD)]
467
+ version.GetFileVersionInfoSizeW.restype = wintypes.DWORD
468
+ version.GetFileVersionInfoW.argtypes = [
469
+ ctypes.c_wchar_p, wintypes.DWORD, wintypes.DWORD, ctypes.c_void_p
470
+ ]
471
+ version.GetFileVersionInfoW.restype = wintypes.BOOL
472
+ version.VerQueryValueW.argtypes = [
473
+ ctypes.c_void_p, ctypes.c_wchar_p, ctypes.POINTER(ctypes.c_void_p),
474
+ ctypes.POINTER(wintypes.UINT),
475
+ ]
476
+ version.VerQueryValueW.restype = wintypes.BOOL
477
+ handle = wintypes.DWORD()
478
+ size = version.GetFileVersionInfoSizeW(path, ctypes.byref(handle))
479
+ if not size:
480
+ return None
481
+ buffer = ctypes.create_string_buffer(size)
482
+ if not version.GetFileVersionInfoW(path, 0, size, buffer):
483
+ return None
484
+ pointer = ctypes.c_void_p()
485
+ length = wintypes.UINT()
486
+ if not version.VerQueryValueW(buffer, "\\", ctypes.byref(pointer), ctypes.byref(length)):
487
+ return None
488
+
489
+ class _FixedFileInfo(ctypes.Structure):
490
+ _fields_ = [
491
+ ("dwSignature", wintypes.DWORD),
492
+ ("dwStrucVersion", wintypes.DWORD),
493
+ ("dwFileVersionMS", wintypes.DWORD),
494
+ ("dwFileVersionLS", wintypes.DWORD),
495
+ ]
496
+
497
+ info = ctypes.cast(pointer, ctypes.POINTER(_FixedFileInfo)).contents
498
+ ms, ls = int(info.dwFileVersionMS), int(info.dwFileVersionLS)
499
+ return f"{ms >> 16}.{ms & 0xFFFF}.{ls >> 16}.{ls & 0xFFFF}"
500
+ except Exception: # pragma: no cover - defensive
501
+ return None
502
+
503
+
504
+ #: The virtual display driver is an IddCx (indirect display) driver. ``IddCx0102`` in its INF
505
+ #: means the UMDF extension interface version 1.2, which ships with Windows 10 1809 (build
506
+ #: 17763). Windows 10 1709 and earlier have no IddCx at all, so no indirect display driver can
507
+ #: load there — that is a hard platform floor, not a tunable.
508
+ IDDCX_MIN_BUILD = WINDOWS_10_1809
509
+
510
+
511
+ def iddcx_support() -> dict:
512
+ """Whether this build can host an IddCx indirect display driver, and why.
513
+
514
+ Two independent signals, because either can be missing on its own:
515
+
516
+ * the build number (``IddCx`` appeared in Windows 10 1703 and the 1.2 interface used by the
517
+ driver's INF in 1809);
518
+ * the ``IddCx.dll`` file version actually present in ``System32``.
519
+ """
520
+ system_root = os.environ.get("SystemRoot", r"C:\Windows")
521
+ candidates = [
522
+ os.path.join(system_root, "System32", "drivers", "UMDF", "IddCx.dll"),
523
+ os.path.join(system_root, "System32", "IddCx.dll"),
524
+ ]
525
+ found = next((path for path in candidates if os.path.isfile(path)), None)
526
+ version = _file_version_dll(found) if found else None
527
+ return {
528
+ "build": windows_build(),
529
+ "min_build": IDDCX_MIN_BUILD,
530
+ "build_ok": windows_build() >= IDDCX_MIN_BUILD,
531
+ "dll_present": bool(found),
532
+ "dll_path": found,
533
+ "dll_version": version,
534
+ "supported": windows_build() >= IDDCX_MIN_BUILD,
535
+ }
536
+
537
+
538
+ def windows_compatibility_report() -> dict:
539
+ """Everything a bug report needs: build, awareness and which optional APIs are present."""
540
+ report: dict = {
541
+ "release": windows_release(),
542
+ "build": windows_build(),
543
+ "is_windows_11": is_windows_11(),
544
+ "described": describe_windows(),
545
+ "python": sys.version.split()[0],
546
+ "dpi_awareness": get_process_dpi_awareness(),
547
+ "thresholds": {
548
+ "windows_10_1607": WINDOWS_10_1607,
549
+ "windows_10_1703": WINDOWS_10_1703,
550
+ "windows_10_1809": WINDOWS_10_1809,
551
+ "windows_11_21h2": WINDOWS_11_21H2,
552
+ },
553
+ "capabilities": {},
554
+ }
555
+ if sys.platform != "win32": # pragma: no cover
556
+ return report
557
+ checks = {
558
+ "SetProcessDpiAwarenessContext": ("user32", "SetProcessDpiAwarenessContext"),
559
+ "SetProcessDpiAwareness": ("shcore", "SetProcessDpiAwareness"),
560
+ "GetDpiForWindow": ("user32", "GetDpiForWindow"),
561
+ "GetDpiForMonitor": ("shcore", "GetDpiForMonitor"),
562
+ "DwmGetWindowAttribute": ("dwmapi", "DwmGetWindowAttribute"),
563
+ "PrintWindow": ("user32", "PrintWindow"),
564
+ "ShellExecuteExW": ("shell32", "ShellExecuteExW"),
565
+ "CreateDesktopW": ("user32", "CreateDesktopW"),
566
+ }
567
+ for label, (library, symbol) in checks.items():
568
+ try:
569
+ module = ctypes.WinDLL(library, use_last_error=True)
570
+ # Accessing a missing export raises OSError, not AttributeError, so probe by
571
+ # resolving the symbol rather than by ``hasattr``.
572
+ getattr(module, symbol)
573
+ report["capabilities"][label] = True
574
+ except Exception: # noqa: BLE001
575
+ report["capabilities"][label] = False
576
+ return report