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,335 @@
1
+ """SetupAPI queries used to detect and validate the virtual display device.
2
+
3
+ Pure ctypes (no ``pywin32`` dependency at import time) because this code also runs inside
4
+ the *elevated helper process*, where the import surface must stay as small as possible.
5
+
6
+ Note: property keys below are spelled out as raw ``(fmtid, pid)`` pairs on purpose —
7
+ ``DEVPKEY_Device_HardwareIds`` is ``{a45c254e-df1c-4efd-8020-67d146a850e0}, 3`` and
8
+ ``DEVPKEY_Device_DriverVersion`` is the same fmtid with property id ``13``.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import ctypes
14
+ from ctypes import wintypes
15
+ from dataclasses import dataclass, field
16
+ from typing import Iterable, List, Optional, Sequence
17
+
18
+ from ._log import get_logger
19
+ from ._winapi import IS_WINDOWS, format_hresult, require_windows
20
+
21
+ log = get_logger("devices")
22
+
23
+ __all__ = ["DeviceRecord", "enumerate_display_devices", "find_devices_by_hardware_id", "device_status_summary"]
24
+
25
+ if IS_WINDOWS: # pragma: no branch
26
+ setupapi = ctypes.WinDLL("setupapi", use_last_error=True)
27
+ else: # pragma: no cover - non-Windows import path
28
+ setupapi = None
29
+
30
+ DIGCF_PRESENT = 0x00000002
31
+ DIGCF_ALLCLASSES = 0x00000004
32
+ #: ``SPDRP_*`` property ids. ``SPDRP_HARDWAREID`` is **1**, not 0 — ``0`` is
33
+ #: ``SPDRP_DEVICEDESC``, and using it returns the human-readable description ("Virtual Display
34
+ #: Driver") instead of the hardware id ("Root\\MttVDD"), which silently defeats every
35
+ #: marker-based device check.
36
+ SPDRP_DEVICEDESC = 0x00000000
37
+ SPDRP_HARDWAREID = 0x00000001
38
+ MAX_DEVICE_ID_LEN = 200
39
+ MAX_PATH = 260
40
+
41
+ _DEVPKEY_Device_HardwareIds = ("{a45c254e-df1c-4efd-8020-67d146a850e0}", 3)
42
+ _DEVPKEY_Device_DriverVersion = ("{a45c254e-df1c-4efd-8020-67d146a850e0}", 13)
43
+ _DEVPKEY_Device_FriendlyName = ("{a45c254e-df1c-4efd-8020-67d146a850e0}", 14)
44
+ _DEVPKEY_Device_Class = ("{a45c254e-df1c-4efd-8020-67d146a850e0}", 10)
45
+
46
+
47
+ class _GUID(ctypes.Structure):
48
+ _fields_ = [
49
+ ("Data1", ctypes.c_ulong),
50
+ ("Data2", ctypes.c_ushort),
51
+ ("Data3", ctypes.c_ushort),
52
+ ("Data4", ctypes.c_ubyte * 8),
53
+ ]
54
+
55
+
56
+ class _SP_DEVINFO_DATA(ctypes.Structure):
57
+ _fields_ = [
58
+ ("cbSize", wintypes.DWORD),
59
+ ("ClassGuid", _GUID),
60
+ ("DevInst", wintypes.DWORD),
61
+ ("Reserved", ctypes.POINTER(ctypes.c_ulong)),
62
+ ]
63
+
64
+
65
+ class _SP_DEVICE_INTERFACE_DATA(ctypes.Structure):
66
+ _fields_ = [
67
+ ("cbSize", wintypes.DWORD),
68
+ ("InterfaceClassGuid", _GUID),
69
+ ("Flags", wintypes.DWORD),
70
+ ("Reserved", ctypes.POINTER(ctypes.c_ulong)),
71
+ ]
72
+
73
+
74
+ if IS_WINDOWS: # pragma: no branch
75
+ setupapi.SetupDiGetClassDevsW.argtypes = [
76
+ ctypes.POINTER(_GUID),
77
+ ctypes.c_wchar_p,
78
+ wintypes.HWND,
79
+ wintypes.DWORD,
80
+ ]
81
+ setupapi.SetupDiGetClassDevsW.restype = ctypes.c_void_p # HDEVINFO
82
+ setupapi.SetupDiEnumDeviceInfo.argtypes = [
83
+ ctypes.c_void_p,
84
+ wintypes.DWORD,
85
+ ctypes.POINTER(_SP_DEVINFO_DATA),
86
+ ]
87
+ setupapi.SetupDiEnumDeviceInfo.restype = wintypes.BOOL
88
+ setupapi.SetupDiGetDeviceInstanceIdW.argtypes = [
89
+ ctypes.c_void_p,
90
+ ctypes.POINTER(_SP_DEVINFO_DATA),
91
+ ctypes.c_wchar_p,
92
+ wintypes.DWORD,
93
+ ctypes.POINTER(wintypes.DWORD),
94
+ ]
95
+ setupapi.SetupDiGetDeviceInstanceIdW.restype = wintypes.BOOL
96
+ setupapi.SetupDiGetDeviceRegistryPropertyW.argtypes = [
97
+ ctypes.c_void_p,
98
+ ctypes.POINTER(_SP_DEVINFO_DATA),
99
+ wintypes.DWORD,
100
+ ctypes.POINTER(wintypes.DWORD),
101
+ ctypes.c_void_p,
102
+ wintypes.DWORD,
103
+ ctypes.POINTER(wintypes.DWORD),
104
+ ]
105
+ setupapi.SetupDiGetDeviceRegistryPropertyW.restype = wintypes.BOOL
106
+ setupapi.SetupDiDestroyDeviceInfoList.argtypes = [ctypes.c_void_p]
107
+ setupapi.SetupDiDestroyDeviceInfoList.restype = wintypes.BOOL
108
+ setupapi.SetupDiOpenDevRegKey.argtypes = [
109
+ ctypes.c_void_p,
110
+ ctypes.POINTER(_SP_DEVINFO_DATA),
111
+ wintypes.DWORD,
112
+ wintypes.DWORD,
113
+ wintypes.DWORD,
114
+ wintypes.DWORD,
115
+ ]
116
+ setupapi.SetupDiOpenDevRegKey.restype = wintypes.HKEY
117
+ setupapi.SetupDiGetDevicePropertyW.argtypes = [
118
+ ctypes.c_void_p,
119
+ ctypes.POINTER(_SP_DEVINFO_DATA),
120
+ ctypes.POINTER(_GUID),
121
+ wintypes.DWORD,
122
+ ctypes.POINTER(wintypes.DWORD),
123
+ ctypes.c_void_p,
124
+ wintypes.DWORD,
125
+ ctypes.POINTER(wintypes.DWORD),
126
+ wintypes.DWORD,
127
+ ]
128
+ setupapi.SetupDiGetDevicePropertyW.restype = wintypes.BOOL
129
+
130
+
131
+ def _guid_from_string(text: str) -> _GUID:
132
+ """Parse a GUID string (with or without braces) into a ``GUID`` struct."""
133
+ value = text.strip().strip("{}")
134
+ parts = value.split("-")
135
+ if len(parts) != 5:
136
+ raise ValueError(f"not a GUID: {text!r}")
137
+ guid = _GUID()
138
+ guid.Data1 = int(parts[0], 16)
139
+ guid.Data2 = int(parts[1], 16)
140
+ guid.Data3 = int(parts[2], 16)
141
+ tail = int(parts[3], 16) << 48 | int(parts[4], 16)
142
+ for index in range(8):
143
+ shift = 8 * (7 - index)
144
+ guid.Data4[index] = (tail >> shift) & 0xFF
145
+ return guid
146
+
147
+
148
+ @dataclass
149
+ class DeviceRecord:
150
+ instance_id: str = ""
151
+ description: str = ""
152
+ friendly_name: str = ""
153
+ hardware_ids: List[str] = field(default_factory=list)
154
+ driver_version: str = ""
155
+ class_name: str = ""
156
+
157
+ def matches(self, needle: str) -> bool:
158
+ haystack = " ".join([self.instance_id, self.description, *self.hardware_ids]).upper()
159
+ return needle.upper() in haystack
160
+
161
+ def to_dict(self) -> dict:
162
+ return {
163
+ "instance_id": self.instance_id,
164
+ "description": self.description,
165
+ "friendly_name": self.friendly_name,
166
+ "hardware_ids": list(self.hardware_ids),
167
+ "driver_version": self.driver_version,
168
+ "class_name": self.class_name,
169
+ }
170
+
171
+
172
+ def _read_description(hdevinfo, info: _SP_DEVINFO_DATA) -> str:
173
+ """``SPDRP_DEVICEDESC`` - the human-readable name ("Virtual Display Driver").
174
+
175
+ This complements the hardware id ("Root\\MttVDD"): a marker can legitimately match either,
176
+ and on some machines the description is the only place the vendor string appears.
177
+ """
178
+ required = wintypes.DWORD()
179
+ setupapi.SetupDiGetDeviceRegistryPropertyW(
180
+ hdevinfo, ctypes.byref(info), SPDRP_DEVICEDESC, None, None, 0, ctypes.byref(required)
181
+ )
182
+ if not required.value:
183
+ return ""
184
+ buffer = ctypes.create_unicode_buffer(required.value // ctypes.sizeof(ctypes.c_wchar) + 1)
185
+ if not setupapi.SetupDiGetDeviceRegistryPropertyW(
186
+ hdevinfo,
187
+ ctypes.byref(info),
188
+ SPDRP_DEVICEDESC,
189
+ None,
190
+ ctypes.cast(buffer, ctypes.c_void_p),
191
+ required.value,
192
+ ctypes.byref(required),
193
+ ):
194
+ return ""
195
+ return buffer.value
196
+
197
+
198
+ def _read_hardware_ids(hdevinfo, info: _SP_DEVINFO_DATA) -> List[str]:
199
+ required = wintypes.DWORD()
200
+ setupapi.SetupDiGetDeviceRegistryPropertyW(
201
+ hdevinfo, ctypes.byref(info), SPDRP_HARDWAREID, None, None, 0, ctypes.byref(required)
202
+ )
203
+ if not required.value:
204
+ return []
205
+ buffer = ctypes.create_unicode_buffer(required.value // ctypes.sizeof(ctypes.c_wchar))
206
+ if not setupapi.SetupDiGetDeviceRegistryPropertyW(
207
+ hdevinfo,
208
+ ctypes.byref(info),
209
+ SPDRP_HARDWAREID,
210
+ None,
211
+ ctypes.cast(buffer, ctypes.c_void_p),
212
+ required.value,
213
+ ctypes.byref(required),
214
+ ):
215
+ return []
216
+ raw = buffer[: required.value // ctypes.sizeof(ctypes.c_wchar)]
217
+ return [item for item in raw.split("\x00") if item]
218
+
219
+
220
+ def _read_string_property(hdevinfo, info: _SP_DEVINFO_DATA, fmtid_pid) -> str:
221
+ fmtid = _guid_from_string(fmtid_pid[0])
222
+ prop_type = wintypes.DWORD()
223
+ required = wintypes.DWORD()
224
+ setupapi.SetupDiGetDevicePropertyW(
225
+ hdevinfo,
226
+ ctypes.byref(info),
227
+ ctypes.byref(fmtid),
228
+ fmtid_pid[1],
229
+ ctypes.byref(prop_type),
230
+ None,
231
+ 0,
232
+ ctypes.byref(required),
233
+ 0,
234
+ )
235
+ if not required.value:
236
+ return ""
237
+ buffer = ctypes.create_unicode_buffer(required.value // ctypes.sizeof(ctypes.c_wchar) + 1)
238
+ if not setupapi.SetupDiGetDevicePropertyW(
239
+ hdevinfo,
240
+ ctypes.byref(info),
241
+ ctypes.byref(fmtid),
242
+ fmtid_pid[1],
243
+ ctypes.byref(prop_type),
244
+ ctypes.cast(buffer, ctypes.c_void_p),
245
+ required.value,
246
+ ctypes.byref(required),
247
+ 0,
248
+ ):
249
+ return ""
250
+ return buffer.value
251
+
252
+
253
+ def find_devices_by_hardware_id(
254
+ hardware_ids: Sequence[str], *, class_guid: Optional[str] = None, present_only: bool = False
255
+ ) -> List[DeviceRecord]:
256
+ """Enumerate devices whose hardware id *contains* any of ``hardware_ids``.
257
+
258
+ An **empty** ``hardware_ids`` means "every device in scope" (used by the diagnostic
259
+ report). A non-empty sequence always filters: returning everything and letting a caller
260
+ post-filter was a real bug — a caller that forgot to filter would see "device present"
261
+ for any machine with any device at all.
262
+ """
263
+ require_windows("find_devices_by_hardware_id")
264
+ needles = [h for h in hardware_ids if h]
265
+ flags = DIGCF_PRESENT if present_only else 0
266
+ if class_guid:
267
+ guid = _guid_from_string(class_guid)
268
+ handle = setupapi.SetupDiGetClassDevsW(ctypes.byref(guid), None, None, flags)
269
+ else:
270
+ handle = setupapi.SetupDiGetClassDevsW(None, None, None, DIGCF_ALLCLASSES | flags)
271
+ if not handle or handle == ctypes.c_void_p(-1).value:
272
+ log.debug("SetupDiGetClassDevs failed: %s", ctypes.get_last_error())
273
+ return []
274
+ try:
275
+ records: List[DeviceRecord] = []
276
+ index = 0
277
+ while True:
278
+ info = _SP_DEVINFO_DATA()
279
+ info.cbSize = ctypes.sizeof(_SP_DEVINFO_DATA)
280
+ if not setupapi.SetupDiEnumDeviceInfo(handle, index, ctypes.byref(info)):
281
+ break
282
+ index += 1
283
+ instance_buffer = ctypes.create_unicode_buffer(MAX_DEVICE_ID_LEN + 1)
284
+ instance_id = ""
285
+ if setupapi.SetupDiGetDeviceInstanceIdW(
286
+ handle, ctypes.byref(info), instance_buffer, MAX_DEVICE_ID_LEN + 1, None
287
+ ):
288
+ instance_id = instance_buffer.value
289
+ hardware = _read_hardware_ids(handle, info)
290
+ record = DeviceRecord(
291
+ instance_id=instance_id,
292
+ description=_read_description(handle, info),
293
+ hardware_ids=hardware,
294
+ driver_version=_read_string_property(handle, info, _DEVPKEY_Device_DriverVersion),
295
+ )
296
+ if _record_matches(record, needles):
297
+ records.append(record)
298
+ return records
299
+ finally:
300
+ setupapi.SetupDiDestroyDeviceInfoList(handle)
301
+
302
+
303
+ def _record_matches(record: DeviceRecord, needles: Sequence[str]) -> bool:
304
+ """Match needles against instance id, hardware ids *and* the friendly description.
305
+
306
+ The description is included on purpose: ``EnumDisplayDevices`` reports the adapter's
307
+ description ("Virtual Display Driver") where a display name is expected, so a marker list
308
+ that only covers hardware ids would miss it in monitors/adapters while still matching in
309
+ SetupAPI.
310
+ """
311
+ if not needles:
312
+ return True
313
+ haystack = " ".join(
314
+ [record.instance_id, record.description, record.friendly_name, *record.hardware_ids]
315
+ ).upper().replace("/", "\\")
316
+ return any(needle.upper().replace("/", "\\") in haystack for needle in needles)
317
+
318
+
319
+ def enumerate_display_devices() -> List[DeviceRecord]:
320
+ """All devices in the ``Display`` setup class (used by ``check`` for context)."""
321
+ return find_devices_by_hardware_id([], class_guid="4d36e968-e325-11ce-bfc1-08002be10318")
322
+
323
+
324
+ def device_status_summary(markers: Iterable[str]) -> dict:
325
+ """Report whether a virtual display device matching ``markers`` is installed."""
326
+ needles = [m for m in markers if m]
327
+ records: List[DeviceRecord] = []
328
+ if IS_WINDOWS:
329
+ records = find_devices_by_hardware_id(needles or [])
330
+ matched = [r for r in records if _record_matches(r, needles)]
331
+ return {
332
+ "present": bool(matched),
333
+ "count": len(matched),
334
+ "devices": [r.to_dict() for r in matched],
335
+ }