VirtualDesktop 1.0.1__py3-none-any.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,831 @@
1
+ """Driver bootstrap: detect, verify, install and remove the virtual display driver.
2
+
3
+ The flow is deliberately *two-phase*, because installing a display driver is a
4
+ machine-wide privileged operation:
5
+
6
+ 1. **Unprivileged phase** (this module, first call): work out whether the device already
7
+ exists. If it does, nothing else happens — the common case costs a single SetupAPI
8
+ enumeration.
9
+ 2. **Privileged phase** (``--internal-elevated`` child): download + digest-verify the
10
+ package, verify its Authenticode signature, then create the ROOT device node and bind
11
+ the driver with ``nefconw``.
12
+
13
+ If the process is already elevated, phase 2 runs in-process; otherwise it is executed
14
+ through UAC via :func:`~VirtualDesktop._process.elevate_and_run_helper`, and the caller
15
+ gets the child's structured result back over a private named pipe.
16
+
17
+ Nothing here shells out with ``shell=True``, nothing is installed without a SHA-256 match
18
+ against the manifest, and a driver that fails signature verification is reported instead
19
+ of being installed silently (``signature_policy="warn"``) or refused
20
+ (``signature_policy="strict"``).
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import os
26
+ import time
27
+ from dataclasses import dataclass, field
28
+ from pathlib import Path
29
+ from typing import Any, Dict, List, Optional, Sequence
30
+
31
+ from ._bundle import BundleManager, driver_spec_for_arch, machine_arch
32
+ from ._devices import device_status_summary, find_devices_by_hardware_id
33
+ from ._download import cache_status, default_cache_dir
34
+ from ._log import get_logger
35
+ from ._process import (
36
+ CommandResult,
37
+ default_log_dir,
38
+ elevate_and_run_helper,
39
+ run_command,
40
+ tail_file,
41
+ write_response,
42
+ )
43
+ from ._sigcheck import assert_driver_trusted
44
+ from ._winapi import IS_WINDOWS, enumerate_monitors, is_admin, is_system_session, require_windows
45
+ from .errors import (
46
+ ConfigError,
47
+ DriverInstallError,
48
+ DriverNotFoundError,
49
+ ElevationDenied,
50
+ ElevationRequired,
51
+ OffscreenMountError,
52
+ )
53
+ from .manifest import Manifest, load_manifest
54
+
55
+ log = get_logger("driver")
56
+
57
+ __all__ = [
58
+ "STRATEGIES",
59
+ "VIRTUAL_DISPLAY_MARKERS",
60
+ "InstallReport",
61
+ "DriverInstaller",
62
+ "virtual_display_present",
63
+ "wait_for_virtual_display",
64
+ ]
65
+
66
+ #: Marker substrings that identify our virtual display among all display devices.
67
+ VIRTUAL_DISPLAY_MARKERS = ("MttVDD", "VID_1B0F", "IddSampleDriver", "ROOT\\IDD")
68
+ STRATEGIES = ("auto", "nefcon", "nefcon-manual", "pnputil", "scoop")
69
+ _REBOOT_REQUIRED = 3010
70
+
71
+
72
+ def virtual_display_present(
73
+ markers: Sequence[str] = VIRTUAL_DISPLAY_MARKERS, *, extra_markers: Sequence[str] = ()
74
+ ) -> bool:
75
+ """True when a virtual display device is registered (installed), present or not.
76
+
77
+ This is a *device* check (SetupAPI), which is what matters: a driver can be installed
78
+ and enabled while its monitor is temporarily absent, and vice versa.
79
+ """
80
+ if not IS_WINDOWS:
81
+ return False
82
+ summary = device_status_summary([*markers, *extra_markers])
83
+ return bool(summary["present"])
84
+
85
+
86
+ def wait_for_virtual_display(
87
+ timeout: float = 20.0,
88
+ *,
89
+ interval: float = 1.0,
90
+ markers: Sequence[str] = VIRTUAL_DISPLAY_MARKERS,
91
+ extra_markers: Sequence[str] = (),
92
+ ) -> bool:
93
+ """Poll for the monitor to show up after installation (drivers come up asynchronously)."""
94
+ deadline = time.monotonic() + timeout
95
+ while True:
96
+ if IS_WINDOWS:
97
+ monitors = enumerate_monitors()
98
+ if any(
99
+ any(
100
+ m.upper() in f"{mon.device_string} {mon.pnp_device_id}".upper()
101
+ for m in [*markers, *extra_markers]
102
+ )
103
+ for mon in monitors
104
+ ):
105
+ return True
106
+ if virtual_display_present(markers, extra_markers=extra_markers):
107
+ # Device is registered; the monitor itself may need a few more seconds.
108
+ if IS_WINDOWS and time.monotonic() < deadline:
109
+ monitors = enumerate_monitors()
110
+ if len(monitors) > 1:
111
+ return True
112
+ else:
113
+ return True
114
+ if time.monotonic() >= deadline:
115
+ return virtual_display_present(markers, extra_markers=extra_markers)
116
+ time.sleep(interval)
117
+
118
+
119
+ @dataclass
120
+ class InstallReport:
121
+ """Structured outcome of a bootstrap attempt (JSON-serialisable)."""
122
+
123
+ action: str = "detect"
124
+ ok: bool = False
125
+ device_present: bool = False
126
+ driver: str = ""
127
+ version: str = ""
128
+ strategy_used: str = ""
129
+ elevated: bool = False
130
+ requires_reboot: bool = False
131
+ skipped_reason: str = ""
132
+ signature_findings: List[str] = field(default_factory=list)
133
+ commands: List[dict] = field(default_factory=list)
134
+ cache: Dict[str, Any] = field(default_factory=dict)
135
+ message: str = ""
136
+
137
+ def to_dict(self) -> dict:
138
+ return {
139
+ "action": self.action,
140
+ "ok": self.ok,
141
+ "device_present": self.device_present,
142
+ "driver": self.driver,
143
+ "version": self.version,
144
+ "strategy_used": self.strategy_used,
145
+ "elevated": self.elevated,
146
+ "requires_reboot": self.requires_reboot,
147
+ "skipped_reason": self.skipped_reason,
148
+ "signature_findings": list(self.signature_findings),
149
+ "commands": list(self.commands),
150
+ "cache": dict(self.cache),
151
+ "message": self.message,
152
+ }
153
+
154
+
155
+ class DriverInstaller:
156
+ """Owns every decision about the virtual display driver."""
157
+
158
+ def __init__(
159
+ self,
160
+ *,
161
+ manifest: Optional[Manifest] = None,
162
+ cache_dir: Optional[os.PathLike] = None,
163
+ driver: Optional[str] = None,
164
+ strategy: str = "auto",
165
+ signature_policy: str = "warn",
166
+ allow_scoop: bool = False,
167
+ install_timeout: float = 600.0,
168
+ download_timeout: float = 300.0,
169
+ monitor_tags: Sequence[str] = (),
170
+ ) -> None:
171
+ if strategy not in STRATEGIES:
172
+ raise ConfigError(
173
+ f"unknown strategy {strategy!r}; expected one of {', '.join(STRATEGIES)}",
174
+ code="bad-strategy",
175
+ )
176
+ if signature_policy not in ("warn", "strict", "off"):
177
+ raise ConfigError(
178
+ f"unknown signature_policy {signature_policy!r}", code="bad-signature-policy"
179
+ )
180
+ self.manifest = manifest or load_manifest()
181
+ self.cache_dir = Path(cache_dir) if cache_dir else default_cache_dir()
182
+ self.spec = driver_spec_for_arch(self.manifest, driver)
183
+ self.strategy = strategy
184
+ self.signature_policy = signature_policy
185
+ self.allow_scoop = allow_scoop
186
+ self.install_timeout = install_timeout
187
+ self.download_timeout = download_timeout
188
+ self.monitor_tags = tuple(monitor_tags)
189
+ self.bundles = BundleManager(self.cache_dir)
190
+ self.log_dir = default_log_dir(self.cache_dir)
191
+
192
+ # -- detection -------------------------------------------------------------------
193
+ @property
194
+ def markers(self) -> tuple:
195
+ return tuple(VIRTUAL_DISPLAY_MARKERS) + tuple(self.spec.device_match) + self.monitor_tags
196
+
197
+ def is_present(self) -> bool:
198
+ return virtual_display_present(extra_markers=self.markers)
199
+
200
+ def devices(self) -> List[dict]:
201
+ return device_status_summary(self.markers)["devices"]
202
+
203
+ def detect(self) -> InstallReport:
204
+ present = self.is_present()
205
+ return InstallReport(
206
+ action="detect",
207
+ ok=present,
208
+ device_present=present,
209
+ driver=self.spec.name,
210
+ version=self.spec.version,
211
+ skipped_reason="" if present else "device-absent",
212
+ cache=self.cache_report(),
213
+ message=(
214
+ "virtual display device is registered"
215
+ if present
216
+ else "virtual display device is not installed yet"
217
+ ),
218
+ )
219
+
220
+ def cache_report(self) -> Dict[str, Any]:
221
+ artifact = self.spec.artifact
222
+ status = cache_status(artifact, self.cache_dir)
223
+ return {
224
+ "cache_dir": str(self.cache_dir),
225
+ "artifact": artifact.filename,
226
+ "artifact_url": artifact.url,
227
+ "artifact_cached": status.ok,
228
+ "artifact_reason": status.reason,
229
+ "driver_tree": bool(self.bundles.cached_driver(self.spec)),
230
+ "tools": self.bundles.tools_integrity(self.manifest),
231
+ "log_dir": str(self.log_dir),
232
+ }
233
+
234
+ # -- installation ----------------------------------------------------------------
235
+ def ensure(self, *, allow_install: bool = True, timeout: Optional[float] = None) -> InstallReport:
236
+ """Install the driver when it is missing, going through UAC when needed."""
237
+ present = self.is_present()
238
+ if present:
239
+ report = self.detect()
240
+ report.action = "ensure"
241
+ report.skipped_reason = "already-installed"
242
+ report.message = "virtual display device already present; nothing to install"
243
+ log.info(report.message)
244
+ return report
245
+ if not allow_install:
246
+ raise DriverNotFoundError(
247
+ "no virtual display device is installed and driver installation is disabled "
248
+ "(driver_policy='never')",
249
+ details={"driver": self.spec.name, "markers": list(self.markers)},
250
+ )
251
+ if is_admin():
252
+ log.info("process is elevated; installing the driver in-process")
253
+ report = self.install(timeout=timeout)
254
+ report.elevated = True
255
+ return report
256
+ if is_system_session():
257
+ raise ElevationRequired(
258
+ "driver installation is required but the process runs in a non-interactive "
259
+ "session, so a UAC prompt cannot be shown",
260
+ hint="Pre-install the driver once from an elevated terminal: "
261
+ "`VirtualDesktop install-driver`.",
262
+ )
263
+ return self._install_elevated(timeout=timeout)
264
+
265
+ def _announce(self, action: str) -> None:
266
+ """Tell the operator that a UAC prompt is imminent.
267
+
268
+ The elevated call blocks on the helper's response pipe, so a silent console looks like a
269
+ hang while the consent dialog waits for a click. Printing first makes the interaction
270
+ obvious (and is the difference between "it hung" and "I missed the prompt").
271
+ """
272
+ print(
273
+ f"[VirtualDesktop] {action} requires administrator rights - "
274
+ "approve the UAC prompt (press Enter on it) to continue ...",
275
+ flush=True,
276
+ )
277
+
278
+ @staticmethod
279
+ def _helper_env() -> Dict[str, str]:
280
+ """Extra environment the elevated helper needs, and nothing more.
281
+
282
+ The helper builds its own minimal environment; these are the values it legitimately
283
+ needs from us: which manifest to trust, and whether the operator asked for debug logs.
284
+ """
285
+ passthrough = ("VIRTUALDESKTOP_MANIFEST", "VIRTUALDESKTOP_DEBUG", "VIRTUALDESKTOP_HELPER_TRACE")
286
+ return {name: os.environ[name] for name in passthrough if os.environ.get(name)}
287
+
288
+ def _install_elevated(self, *, timeout: Optional[float] = None) -> InstallReport:
289
+ """Run the privileged phase through UAC and translate the child's report."""
290
+ log.info("driver is missing; requesting elevation to install %s", self.spec.name)
291
+ self._announce(f"installing driver {self.spec.name}")
292
+ log_dir = str(self.log_dir)
293
+ try:
294
+ result = elevate_and_run_helper(
295
+ [
296
+ "--cache-dir",
297
+ str(self.cache_dir),
298
+ "--strategy",
299
+ self.strategy,
300
+ "--signature-policy",
301
+ self.signature_policy,
302
+ "--driver",
303
+ self.spec.name,
304
+ "--install-timeout",
305
+ str(self.install_timeout),
306
+ "--log-dir",
307
+ log_dir,
308
+ ],
309
+ timeout=timeout or (self.install_timeout + 300.0),
310
+ log_dir=log_dir,
311
+ extra_env=self._helper_env(),
312
+ )
313
+ except ElevationDenied:
314
+ raise
315
+ if result.timed_out:
316
+ raise DriverInstallError(
317
+ "the elevated installer did not report a result before the timeout",
318
+ hint=f"Check {self.log_dir} for the elevated log.",
319
+ code="driver-install-timeout",
320
+ )
321
+ response = result.response or {}
322
+ report = InstallReport(
323
+ action="install",
324
+ ok=bool(response.get("ok")),
325
+ device_present=bool(response.get("device_present")),
326
+ driver=str(response.get("driver", self.spec.name)),
327
+ version=str(response.get("version", self.spec.version)),
328
+ strategy_used=str(response.get("strategy_used", "")),
329
+ elevated=True,
330
+ requires_reboot=bool(response.get("requires_reboot")),
331
+ signature_findings=list(response.get("signature_findings") or []),
332
+ commands=list(response.get("commands") or []),
333
+ cache=self.cache_report(),
334
+ message=str(response.get("message", "")),
335
+ )
336
+ if not report.ok:
337
+ detail = response.get("error") or report.message or "unknown failure"
338
+ raise DriverInstallError(
339
+ f"the elevated driver installation failed: {detail}",
340
+ attempts=[c.get("argv", []) for c in report.commands],
341
+ hint=f"Full log: {self.log_dir}",
342
+ details={"response": response},
343
+ )
344
+ log.info("elevated installation succeeded (%s)", report.strategy_used)
345
+ return report
346
+
347
+ def install(self, *, timeout: Optional[float] = None) -> InstallReport:
348
+ """Privileged phase: download, verify, extract and install. Must run elevated."""
349
+ require_windows("driver installation")
350
+ report = self.detect()
351
+ report.action = "install"
352
+ report.elevated = is_admin()
353
+ if report.device_present:
354
+ report.ok = True
355
+ report.skipped_reason = "already-installed"
356
+ report.message = "virtual display device already present"
357
+ return report
358
+
359
+ bundle = self.bundles.prepare_driver(self.spec, progress=self._progress_logger())
360
+ report.driver = self.spec.name
361
+ report.version = self.spec.version
362
+ report.signature_findings = assert_driver_trusted(
363
+ bundle.driver_dir,
364
+ self.spec.signer_thumbprints,
365
+ policy=self.signature_policy,
366
+ )
367
+ report.cache = self.cache_report()
368
+
369
+ attempts: List[str] = []
370
+ for name, runner in self._strategy_order():
371
+ log.info("trying driver install strategy %s", name)
372
+ outcome = runner(bundle, timeout=timeout)
373
+ report.commands.extend(outcome.setdefault("commands", []))
374
+ if outcome.get("ok"):
375
+ report.ok = True
376
+ report.strategy_used = name
377
+ report.requires_reboot = bool(outcome.get("requires_reboot"))
378
+ report.message = outcome.get("message", f"installed via {name}")
379
+ break
380
+ attempts.append(f"{name}: {outcome.get('message', 'failed')}")
381
+ if outcome.get("fatal"):
382
+ break
383
+ else:
384
+ report.ok = False
385
+
386
+ if not report.ok:
387
+ raise DriverInstallError(
388
+ "all driver installation strategies failed",
389
+ attempts=attempts,
390
+ code="driver-install-failed",
391
+ hint=f"Inspect {self.log_dir} (nefcon output is captured there).",
392
+ details={"driver": self.spec.name, "attempts": attempts},
393
+ )
394
+
395
+ report.device_present = wait_for_virtual_display(
396
+ timeout=20.0, extra_markers=self.markers
397
+ )
398
+ if not report.device_present:
399
+ report.message += " (device registered but its monitor has not appeared yet)"
400
+ log.warning("driver installed but no virtual monitor is visible yet")
401
+ if report.requires_reboot:
402
+ report.message += " - a reboot is required to finish the installation"
403
+ log.warning("driver install reported ERROR_SUCCESS_REBOOT_REQUIRED (3010)")
404
+ return report
405
+
406
+ def _progress_logger(self):
407
+ state = {"next": 0.0}
408
+
409
+ def _progress(done: int, total: Optional[int]) -> None:
410
+ now = time.monotonic()
411
+ if now < state["next"] and (total is None or done < total):
412
+ return
413
+ state["next"] = now + 2.0
414
+ if total:
415
+ log.info("downloading driver package: %.1f%% (%d/%d bytes)", 100.0 * done / total, done, total)
416
+ else:
417
+ log.info("downloading driver package: %d bytes", done)
418
+
419
+ return _progress
420
+
421
+ def _strategy_order(self):
422
+ """Yield ``(name, callable)`` pairs in the order dictated by ``strategy``."""
423
+ available = {
424
+ "nefcon": self._strategy_nefcon,
425
+ "nefcon-manual": self._strategy_nefcon_manual,
426
+ "pnputil": self._strategy_pnputil,
427
+ "scoop": self._strategy_scoop,
428
+ }
429
+ if self.strategy == "auto":
430
+ order = ["nefcon", "nefcon-manual", "pnputil"]
431
+ else:
432
+ order = [self.strategy]
433
+ for name in order:
434
+ runner = available.get(name)
435
+ if runner is None:
436
+ continue
437
+ if name == "scoop" and not self.allow_scoop:
438
+ log.debug("skipping the scoop strategy (allow_scoop=False)")
439
+ continue
440
+ yield name, runner
441
+
442
+ # -- individual strategies -------------------------------------------------------
443
+ def _nefcon(self, *, console: bool = False) -> Path:
444
+ tool = "nefcon_console" if console else "nefcon"
445
+ return self.bundles.tool(self.manifest, tool, progress=self._progress_logger())
446
+
447
+ def _log_file(self, name: str) -> Path:
448
+ return self.log_dir / f"{name}.log"
449
+
450
+ def _strategy_nefcon(self, bundle, *, timeout: Optional[float] = None) -> dict:
451
+ """``nefconw install <inf> <hwid> --no-duplicates``: device node + driver in one step."""
452
+ try:
453
+ exe = self._nefcon()
454
+ except OffscreenMountError as exc:
455
+ return {"ok": False, "message": f"nefcon unavailable: {exc}", "commands": []}
456
+ commands: List[dict] = []
457
+ last_message = ""
458
+ for hardware_id in self.spec.all_hardware_ids:
459
+ argv = [str(exe), "install", str(bundle.inf_path), hardware_id, "--no-duplicates"]
460
+ result = run_command(
461
+ argv,
462
+ timeout=timeout or self.install_timeout,
463
+ log_path=self._log_file("nefcon-install"),
464
+ )
465
+ commands.append(result.to_dict())
466
+ if result.ok or result.requires_reboot:
467
+ return {
468
+ "ok": True,
469
+ "requires_reboot": result.requires_reboot,
470
+ "message": f"nefcon install succeeded for {hardware_id}",
471
+ "commands": commands,
472
+ "result": result,
473
+ }
474
+ last_message = (result.stderr or result.stdout or "").strip()[-400:]
475
+ log.warning("nefcon install failed for %s (exit %s): %s", hardware_id, result.returncode, last_message)
476
+ return {
477
+ "ok": False,
478
+ "message": f"nefcon install failed: {last_message or 'unknown error'}",
479
+ "commands": commands,
480
+ }
481
+
482
+ def _strategy_nefcon_manual(self, bundle, *, timeout: Optional[float] = None) -> dict:
483
+ """Explicit ``--create-device-node`` + ``--install-driver`` (matches the docs)."""
484
+ try:
485
+ exe = self._nefcon()
486
+ except OffscreenMountError as exc:
487
+ return {"ok": False, "message": f"nefcon unavailable: {exc}", "commands": []}
488
+ commands: List[dict] = []
489
+ create = run_command(
490
+ [
491
+ str(exe),
492
+ "--create-device-node",
493
+ "--hardware-id",
494
+ self.spec.hardware_id,
495
+ "--class-name",
496
+ self.spec.class_name,
497
+ "--class-guid",
498
+ self.spec.class_guid,
499
+ "--no-duplicates",
500
+ ],
501
+ timeout=timeout or self.install_timeout,
502
+ log_path=self._log_file("nefcon-create-node"),
503
+ )
504
+ commands.append(create.to_dict())
505
+ # Already-existing node is not an error for us; the install step is what matters.
506
+ if not create.ok and not _looks_like_already_exists(create):
507
+ return {
508
+ "ok": False,
509
+ "message": f"device node creation failed: {_tail(create)}",
510
+ "commands": commands,
511
+ }
512
+ install = run_command(
513
+ [str(exe), "--install-driver", "--inf-path", str(bundle.inf_path)],
514
+ timeout=timeout or self.install_timeout,
515
+ log_path=self._log_file("nefcon-install-driver"),
516
+ )
517
+ commands.append(install.to_dict())
518
+ if install.ok or install.requires_reboot:
519
+ return {
520
+ "ok": True,
521
+ "requires_reboot": install.requires_reboot,
522
+ "message": "device node created and driver installed",
523
+ "commands": commands,
524
+ "result": install,
525
+ }
526
+ return {
527
+ "ok": False,
528
+ "message": f"--install-driver failed: {_tail(install)}",
529
+ "commands": commands,
530
+ }
531
+
532
+ def _strategy_pnputil(self, bundle, *, timeout: Optional[float] = None) -> dict:
533
+ """Fallback: add the package to the driver store and let PnP bind the ROOT node."""
534
+ pnputil = os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "System32", "pnputil.exe")
535
+ if not os.path.isfile(pnputil):
536
+ return {"ok": False, "message": "pnputil.exe not found", "commands": []}
537
+ commands: List[dict] = []
538
+ add = run_command(
539
+ [pnputil, "/add-driver", str(bundle.inf_path), "/install"],
540
+ timeout=timeout or self.install_timeout,
541
+ log_path=self._log_file("pnputil-add-driver"),
542
+ )
543
+ commands.append(add.to_dict())
544
+ if add.ok or _REBOOT_REQUIRED == add.returncode:
545
+ # pnputil cannot create a ROOT-enumerated node; nefcon (or the installer the
546
+ # user ran) must have created one already for the driver to bind.
547
+ if self.is_present():
548
+ return {
549
+ "ok": True,
550
+ "requires_reboot": add.returncode == _REBOOT_REQUIRED,
551
+ "message": "driver store updated and the device is present",
552
+ "commands": commands,
553
+ "result": add,
554
+ }
555
+ return {
556
+ "ok": False,
557
+ "message": (
558
+ "pnputil added the package but no ROOT\\MttVDD device node exists, so the "
559
+ "driver cannot bind (this is the documented pnputil limitation)"
560
+ ),
561
+ "commands": commands,
562
+ }
563
+ return {
564
+ "ok": False,
565
+ "message": f"pnputil failed: {_tail(add)}",
566
+ "commands": commands,
567
+ }
568
+
569
+ def _strategy_scoop(self, bundle, *, timeout: Optional[float] = None) -> dict:
570
+ """Opt-in convenience path for machines that already use Scoop."""
571
+ import shutil
572
+
573
+ if not self.allow_scoop:
574
+ return {"ok": False, "message": "scoop strategy disabled (allow_scoop=False)", "commands": []}
575
+ scoop = shutil.which("scoop")
576
+ if not scoop:
577
+ return {"ok": False, "message": "scoop is not on PATH", "commands": []}
578
+ commands: List[dict] = []
579
+ for argv in (
580
+ [scoop, "bucket", "add", "extras"],
581
+ [scoop, "bucket", "add", "nonportable"],
582
+ [scoop, "install", "iddsampledriver-ge9-np", "-g"],
583
+ ):
584
+ result = run_command(
585
+ argv, timeout=timeout or self.install_timeout, log_path=self._log_file("scoop")
586
+ )
587
+ commands.append(result.to_dict())
588
+ if not result.ok and "already" not in (result.stderr or "").lower():
589
+ return {
590
+ "ok": False,
591
+ "message": f"scoop step failed: {_tail(result)}",
592
+ "commands": commands,
593
+ }
594
+ return {
595
+ "ok": self.is_present(),
596
+ "message": "scoop finished" if self.is_present() else "scoop ran but no device appeared",
597
+ "commands": commands,
598
+ }
599
+
600
+ # -- removal ---------------------------------------------------------------------
601
+ def uninstall(
602
+ self,
603
+ *,
604
+ purge_files: bool = False,
605
+ purge_driver_store: bool = False,
606
+ timeout: Optional[float] = None,
607
+ elevate: bool = True,
608
+ ) -> InstallReport:
609
+ """Remove the device node, optionally purging the store package and cached files.
610
+
611
+ Removal is a privileged operation (measured: nefcon refuses with "This command requires
612
+ elevated privileges"), so an unelevated caller is routed through the same UAC helper that
613
+ installation uses. ``elevate=False`` keeps the in-process behaviour for callers that are
614
+ already elevated (the helper passes it that way).
615
+ """
616
+ require_windows("driver uninstall")
617
+ if elevate and not is_admin():
618
+ return self._uninstall_elevated(
619
+ purge_files=purge_files, purge_driver_store=purge_driver_store, timeout=timeout
620
+ )
621
+ return self._uninstall_in_process(
622
+ purge_files=purge_files, purge_driver_store=purge_driver_store, timeout=timeout
623
+ )
624
+
625
+ def _uninstall_in_process(
626
+ self,
627
+ *,
628
+ purge_files: bool = False,
629
+ purge_driver_store: bool = False,
630
+ timeout: Optional[float] = None,
631
+ ) -> InstallReport:
632
+ report = self.detect()
633
+ report.action = "uninstall"
634
+ if not report.device_present:
635
+ report.ok = True
636
+ report.message = "no virtual display device was registered"
637
+ else:
638
+ try:
639
+ exe = self._nefcon()
640
+ except OffscreenMountError as exc:
641
+ raise DriverInstallError(
642
+ f"nefcon is required to remove the device node: {exc}",
643
+ hint="Run `VirtualDesktop fetch-driver` first (needs network access).",
644
+ ) from exc
645
+ remove = run_command(
646
+ [
647
+ str(exe),
648
+ "--remove-device-node",
649
+ "--hardware-id",
650
+ self.spec.hardware_id,
651
+ "--class-guid",
652
+ self.spec.class_guid,
653
+ ],
654
+ timeout=timeout or self.install_timeout,
655
+ log_path=self._log_file("nefcon-remove-device-node"),
656
+ )
657
+ report.commands.append(remove.to_dict())
658
+ report.ok = remove.ok or remove.requires_reboot
659
+ report.requires_reboot = remove.requires_reboot
660
+ report.message = "device node removed" if report.ok else f"removal failed: {_tail(remove)}"
661
+ if not report.ok:
662
+ raise DriverInstallError(
663
+ f"could not remove the virtual display device: {_tail(remove)}",
664
+ attempts=[remove.to_dict()],
665
+ hint=f"See {self._log_file('nefcon-remove-device-node')}",
666
+ )
667
+ if purge_driver_store:
668
+ purge = run_command(
669
+ [
670
+ str(exe),
671
+ "--remove-driver-store-package",
672
+ "--inf-name",
673
+ Path(self.spec.inf_member).name,
674
+ "--class-guid",
675
+ self.spec.class_guid,
676
+ ],
677
+ timeout=timeout or self.install_timeout,
678
+ log_path=self._log_file("nefcon-purge-package"),
679
+ )
680
+ report.commands.append(purge.to_dict())
681
+ if not purge.ok:
682
+ log.warning("driver-store purge failed: %s", _tail(purge))
683
+ if purge_files:
684
+ report.skipped_reason = (
685
+ "cached driver tree removed"
686
+ if self.bundles.remove_driver_tree(self.spec)
687
+ else "no cached driver tree to remove"
688
+ )
689
+ return report
690
+
691
+ def _uninstall_elevated(
692
+ self,
693
+ *,
694
+ purge_files: bool,
695
+ purge_driver_store: bool,
696
+ timeout: Optional[float] = None,
697
+ ) -> InstallReport:
698
+ """Run the removal through UAC, exactly like installation does."""
699
+ log.info("removing the virtual display; requesting elevation")
700
+ self._announce("removing the virtual display driver")
701
+ log_dir = str(self.log_dir)
702
+ try:
703
+ result = elevate_and_run_helper(
704
+ [
705
+ "--uninstall",
706
+ "--cache-dir",
707
+ str(self.cache_dir),
708
+ "--driver",
709
+ self.spec.name,
710
+ "--install-timeout",
711
+ str(self.install_timeout),
712
+ "--log-dir",
713
+ log_dir,
714
+ *(["--purge-files"] if purge_files else []),
715
+ *(["--purge-driver-store"] if purge_driver_store else []),
716
+ ],
717
+ timeout=timeout or (self.install_timeout + 300.0),
718
+ log_dir=log_dir,
719
+ extra_env=self._helper_env(),
720
+ )
721
+ except ElevationDenied:
722
+ raise
723
+ if result.timed_out:
724
+ raise DriverInstallError(
725
+ "the elevated uninstaller did not report a result before the timeout",
726
+ hint=f"Check {self.log_dir} for the elevated log.",
727
+ code="driver-uninstall-timeout",
728
+ )
729
+ response = result.response or {}
730
+ report = InstallReport(
731
+ action="uninstall",
732
+ ok=bool(response.get("ok")),
733
+ device_present=bool(response.get("device_present")),
734
+ driver=str(response.get("driver", self.spec.name)),
735
+ elevated=True,
736
+ requires_reboot=bool(response.get("requires_reboot")),
737
+ commands=list(response.get("commands") or []),
738
+ cache=self.cache_report(),
739
+ message=str(response.get("message", "")),
740
+ )
741
+ if not report.ok:
742
+ detail = response.get("error") or report.message or "unknown failure"
743
+ raise DriverInstallError(
744
+ f"the elevated removal failed: {detail}",
745
+ attempts=[c.get("argv", []) for c in report.commands],
746
+ hint=f"Full log: {self.log_dir}",
747
+ details={"response": response},
748
+ )
749
+ log.info("elevated removal succeeded")
750
+ return report
751
+
752
+
753
+ def _tail(result: CommandResult, limit: int = 400) -> str:
754
+ text = (result.stderr or "").strip() or (result.stdout or "").strip()
755
+ return text[-limit:] if text else f"exit code {result.returncode}"
756
+
757
+
758
+ def _looks_like_already_exists(result: CommandResult) -> bool:
759
+ haystack = f"{result.stdout} {result.stderr}".lower()
760
+ return "already exists" in haystack or "0x800700b7" in haystack or "183" in haystack
761
+
762
+
763
+ def install_from_helper(
764
+ *,
765
+ cache_dir: Optional[str] = None,
766
+ strategy: str = "auto",
767
+ signature_policy: str = "warn",
768
+ driver: Optional[str] = None,
769
+ install_timeout: float = 600.0,
770
+ log_dir: Optional[str] = None,
771
+ response_pipe: Optional[str] = None,
772
+ uninstall: bool = False,
773
+ purge_files: bool = False,
774
+ purge_driver_store: bool = False,
775
+ elevate: bool = True,
776
+ ) -> int:
777
+ """Entry point for the elevated child process (``_elevated-install``).
778
+
779
+ Handles both directions: installing (default) and **removing** the device
780
+ (``uninstall=True``). Removal needs the same privilege as installation — measured: nefcon
781
+ answers ``This command requires elevated privileges`` otherwise, which is why
782
+ ``VirtualDesktop uninstall-driver`` goes through this helper exactly like installation does.
783
+
784
+ ``elevate`` must be ``False`` when *this* call already runs inside the elevated child. It is
785
+ the caller's decision, not a token probe: the token probe (``is_admin()``) reports "not
786
+ elevated" inside the elevated child on some configurations, and ``uninstall()`` would then
787
+ ask UAC for elevation a second time — spawning a nested helper that nobody is left to
788
+ approve, so the outer helper waited on its pipe until the parent gave up
789
+ (measured: ``driver-uninstall-timeout`` with no ``consent.exe`` and no helper process).
790
+ """
791
+ payload: Dict[str, Any] = {"ok": False, "action": "uninstall" if uninstall else "install"}
792
+ exit_code = 1
793
+ installer: Optional[DriverInstaller] = None
794
+ try:
795
+ installer = DriverInstaller(
796
+ cache_dir=cache_dir,
797
+ driver=driver,
798
+ strategy=strategy,
799
+ signature_policy=signature_policy,
800
+ install_timeout=install_timeout,
801
+ )
802
+ if uninstall:
803
+ report = installer.uninstall(
804
+ purge_files=purge_files,
805
+ purge_driver_store=purge_driver_store,
806
+ timeout=install_timeout,
807
+ elevate=elevate,
808
+ )
809
+ payload = report.to_dict()
810
+ payload["ok"] = bool(report.ok) and not installer.is_present()
811
+ payload["device_present"] = installer.is_present()
812
+ exit_code = 0 if payload["ok"] else 1
813
+ else:
814
+ report = installer.install()
815
+ payload = report.to_dict()
816
+ payload["ok"] = True
817
+ payload["device_present"] = report.device_present
818
+ exit_code = 0
819
+ except Exception as exc: # noqa: BLE001 - the child must never die silently
820
+ log.exception("elevated driver operation failed")
821
+ payload["ok"] = False
822
+ payload["error"] = str(exc)
823
+ payload["error_code"] = getattr(exc, "code", type(exc).__name__)
824
+ payload["hint"] = getattr(exc, "hint", None)
825
+ if isinstance(exc, DriverInstallError):
826
+ payload["commands"] = list(exc.details.get("attempts") or [])
827
+ payload["cache"] = installer.cache_report() if installer else {}
828
+ finally:
829
+ if response_pipe:
830
+ write_response(response_pipe, payload)
831
+ return exit_code