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.
- VirtualDesktop/__init__.py +152 -0
- VirtualDesktop/__main__.py +10 -0
- VirtualDesktop/_bundle.py +369 -0
- VirtualDesktop/_compat.py +576 -0
- VirtualDesktop/_desktop_runtime.py +465 -0
- VirtualDesktop/_devices.py +335 -0
- VirtualDesktop/_download.py +462 -0
- VirtualDesktop/_enumerate.py +97 -0
- VirtualDesktop/_helper.py +31 -0
- VirtualDesktop/_log.py +85 -0
- VirtualDesktop/_process.py +912 -0
- VirtualDesktop/_sigcheck.ps1 +32 -0
- VirtualDesktop/_sigcheck.py +321 -0
- VirtualDesktop/_vendor/FFMPEG-LICENSE.txt +23 -0
- VirtualDesktop/_vendor/ffmpeg.exe +4 -0
- VirtualDesktop/_winapi.py +739 -0
- VirtualDesktop/agent.py +403 -0
- VirtualDesktop/capture.py +676 -0
- VirtualDesktop/cli.py +673 -0
- VirtualDesktop/config.py +255 -0
- VirtualDesktop/desktop.py +231 -0
- VirtualDesktop/desktop_capture.py +159 -0
- VirtualDesktop/desktop_session.py +848 -0
- VirtualDesktop/display_setup.py +790 -0
- VirtualDesktop/driver.py +831 -0
- VirtualDesktop/driver_manifest.json +118 -0
- VirtualDesktop/errors.py +211 -0
- VirtualDesktop/manifest.py +310 -0
- VirtualDesktop/record.py +701 -0
- VirtualDesktop/session.py +514 -0
- VirtualDesktop/window.py +1240 -0
- VirtualDesktop/workspace.py +1274 -0
- virtualdesktop-1.6.5.dist-info/METADATA +846 -0
- virtualdesktop-1.6.5.dist-info/RECORD +38 -0
- virtualdesktop-1.6.5.dist-info/WHEEL +5 -0
- virtualdesktop-1.6.5.dist-info/entry_points.txt +2 -0
- virtualdesktop-1.6.5.dist-info/licenses/LICENSE +21 -0
- virtualdesktop-1.6.5.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
}
|