driverclient 0.2.31__tar.gz → 0.2.32__tar.gz

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.
Files changed (25) hide show
  1. {driverclient-0.2.31 → driverclient-0.2.32}/PKG-INFO +1 -1
  2. {driverclient-0.2.31 → driverclient-0.2.32}/pyproject.toml +1 -1
  3. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/__init__.py +1 -1
  4. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/capture.py +14 -2
  5. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/install.py +67 -13
  6. {driverclient-0.2.31 → driverclient-0.2.32}/.gitignore +0 -0
  7. {driverclient-0.2.31 → driverclient-0.2.32}/README.md +0 -0
  8. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/__main__.py +0 -0
  9. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/config.json +0 -0
  10. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/config.py +0 -0
  11. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/core/__init__.py +0 -0
  12. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/core/hardware.py +0 -0
  13. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/core/http.py +0 -0
  14. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/core/hwid.py +0 -0
  15. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/core/proc.py +0 -0
  16. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/events.py +0 -0
  17. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/main.py +0 -0
  18. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/__init__.py +0 -0
  19. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/automate.py +0 -0
  20. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/diagnose.py +0 -0
  21. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/download.py +0 -0
  22. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/offline.py +0 -0
  23. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/resolve.py +0 -0
  24. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/scan.py +0 -0
  25. {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: driverclient
3
- Version: 0.2.31
3
+ Version: 0.2.32
4
4
  Summary: Driver Server client with an embeddable connector for config injection at runtime.
5
5
  Project-URL: Homepage, https://example.com/driverclient
6
6
  Author-email: Raja Sanaullah <sanaullah@99technologies.com>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "driverclient"
7
- version = "0.2.31"
7
+ version = "0.2.32"
8
8
  description = "Driver Server client with an embeddable connector for config injection at runtime."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -26,7 +26,7 @@ from driverclient.main import run as _run
26
26
 
27
27
  __all__ = ["ClientEvent", "DriverClient"]
28
28
 
29
- __version__ = "0.2.31"
29
+ __version__ = "0.2.32"
30
30
 
31
31
 
32
32
  class DriverClient:
@@ -404,12 +404,22 @@ def wu_full() -> CaptureResult:
404
404
 
405
405
  # ══ Missing-HWID classification ═══════════════════════════════════════════════
406
406
 
407
+ _INBOX_INADEQUATE_CLASSES = {"display adapters"}
408
+
407
409
 
408
410
  def classify_missing(not_found: list[str], scan_result: ScanResult) -> dict[str, list[str]]:
409
411
  """Bucket repo-missing HWIDs by what the machine says about their device:
410
412
  inbox (Windows ships the driver — not applicable), oem_local (OEM driver
411
413
  installed locally — capturable), driverless (genuinely missing — WU
412
- candidate), unknown (HWID not in scan)."""
414
+ candidate), unknown (HWID not in scan).
415
+
416
+ Display adapters are excluded from the inbox bucket: Microsoft Basic
417
+ Display Adapter is Windows' inbox fallback for a GPU whose real driver
418
+ was never bound, and it leaves the device non-accelerated — unlike other
419
+ inbox drivers (HID, mass storage, ...) it is not adequate coverage. Such
420
+ a device is treated as driverless so it still reaches WU / gets captured
421
+ once a real driver shows up, instead of being silently written off.
422
+ """
413
423
  hwid_to_device: dict[str, Device] = {}
414
424
  for scanned in scan_result.devices:
415
425
  for h in scanned.hwids:
@@ -427,8 +437,10 @@ def classify_missing(not_found: list[str], scan_result: ScanResult) -> dict[str,
427
437
  buckets["unknown"].append(hwid)
428
438
  elif not dev.has_driver:
429
439
  buckets["driverless"].append(hwid)
430
- elif dev.is_inbox:
440
+ elif dev.is_inbox and dev.class_name.lower() not in _INBOX_INADEQUATE_CLASSES:
431
441
  buckets["inbox"].append(hwid)
442
+ elif dev.is_inbox:
443
+ buckets["driverless"].append(hwid)
432
444
  else:
433
445
  buckets["oem_local"].append(hwid)
434
446
  return buckets
@@ -90,6 +90,13 @@ class InstallResult:
90
90
  installed_ok: list[DriverResult] = field(default_factory=list)
91
91
  install_failed: list[DriverResult] = field(default_factory=list)
92
92
  trace_id: str = ""
93
+ # A display driver is confirmed in the driver store but the device is STILL
94
+ # bound to an inbox/Basic Display driver after the disable/enable refresh —
95
+ # display drivers often need a reboot to actually bind. Set by
96
+ # _refresh_bound_devices so the orchestrator reboots (once) to let it bind,
97
+ # instead of silently leaving the device on Basic Display and falling back
98
+ # to Windows Update.
99
+ rebind_reboot: bool = False
93
100
 
94
101
  @property
95
102
  def ok_count(self) -> int:
@@ -101,8 +108,9 @@ class InstallResult:
101
108
 
102
109
  @property
103
110
  def reboot_required(self) -> bool:
104
- """True if any successfully-installed driver reported reboot-required."""
105
- return any(r.reboot_required for r in self.installed_ok)
111
+ """True if a successfully-installed driver reported reboot-required, or a
112
+ display device is still on Basic Display and needs a reboot to bind."""
113
+ return self.rebind_reboot or any(r.reboot_required for r in self.installed_ok)
106
114
 
107
115
 
108
116
  def _parse_ver(s: str) -> tuple | None:
@@ -203,17 +211,19 @@ def resolve_and_install(force: bool = False) -> InstallResult:
203
211
  # via DISM during imaging) without ever being BOUND to the device —
204
212
  # the primary adapter doesn't reliably switch itself over. Refresh it
205
213
  # now instead of leaving that gap for Windows Update to close later.
206
- _refresh_bound_devices(
214
+ rebind_reboot = _refresh_bound_devices(
207
215
  already_in_store,
208
216
  {_bid(d) for d in already_in_store},
209
217
  cfg,
210
218
  )
211
219
  events.done(
212
220
  "install",
213
- "[install] Nothing to install — all resolved drivers already current",
221
+ "[install] Nothing to install — all resolved drivers already current"
222
+ + (" — reboot required to bind display" if rebind_reboot else ""),
214
223
  total=0,
224
+ reboot_required=rebind_reboot,
215
225
  )
216
- return InstallResult(trace_id=result.trace_id)
226
+ return InstallResult(trace_id=result.trace_id, rebind_reboot=rebind_reboot)
217
227
 
218
228
  session_id = str(uuid.uuid4())
219
229
  work_dir = Path(cfg.get("work_dir", "C:\\DriverServer\\client\\staging")) / session_id
@@ -240,9 +250,9 @@ def resolve_and_install(force: bool = False) -> InstallResult:
240
250
  # so the device actually switches over instead of staying on whatever it
241
251
  # booted with until Windows Update eventually forces a real reinstall.
242
252
  ready_ids = {r.binding_id for r in installed_ok} | {_bid(d) for d in already_in_store}
243
- _refresh_bound_devices(result.drivers, ready_ids, cfg)
253
+ rebind_reboot = _refresh_bound_devices(result.drivers, ready_ids, cfg)
244
254
 
245
- reboot_required = any(r.reboot_required for r in installed_ok)
255
+ reboot_required = rebind_reboot or any(r.reboot_required for r in installed_ok)
246
256
  reboot_tag = " — reboot required" if reboot_required else ""
247
257
  skip_tag = f" ({len(skipped_now)} already in store)" if skipped_now else ""
248
258
  events.done(
@@ -263,6 +273,7 @@ def resolve_and_install(force: bool = False) -> InstallResult:
263
273
  installed_ok=installed_ok,
264
274
  install_failed=install_failed,
265
275
  trace_id=result.trace_id,
276
+ rebind_reboot=rebind_reboot,
266
277
  )
267
278
 
268
279
 
@@ -439,7 +450,7 @@ def _install_one_exe(driver: dict, driver_dir: Path, cfg: dict) -> DriverResult:
439
450
  # ── Device refresh (force-bind display drivers) ─────────────────────────────────
440
451
 
441
452
 
442
- def _refresh_bound_devices(drivers: list[dict], ready_ids: set[str], cfg: dict) -> None:
453
+ def _refresh_bound_devices(drivers: list[dict], ready_ids: set[str], cfg: dict) -> bool:
443
454
  """Force Windows to actually bind a display driver that's confirmed in the
444
455
  driver store (just installed, or already there from an earlier offline/DISM
445
456
  injection) but not necessarily applied to the device yet.
@@ -452,6 +463,18 @@ def _refresh_bound_devices(drivers: list[dict], ready_ids: set[str], cfg: dict)
452
463
  driver search eventually does, closing the gap our own pipeline left open.
453
464
  Disabling + re-enabling the device here forces Windows to redo PnP install
454
465
  for it immediately, picking up the driver that's already in the store.
466
+
467
+ After refreshing, a FRESH scan verifies each target device actually left the
468
+ inbox/Basic Display driver and is now on its OEM (oemNN.inf) driver. A
469
+ display driver frequently needs a reboot to bind — disable/enable alone
470
+ doesn't always switch the primary adapter over — so any device still on an
471
+ inbox driver is reported loudly and this returns True, telling the caller a
472
+ reboot is needed. That is far better than the old silent success, which left
473
+ the device on Basic Display and pushed the machine into the slow Windows
474
+ Update path on every deployment.
475
+
476
+ Returns True when at least one target display device is still bound to an
477
+ inbox driver after the refresh (→ reboot required to bind); False otherwise.
455
478
  """
456
479
  display_hwids = {
457
480
  d["hwid"]
@@ -459,13 +482,44 @@ def _refresh_bound_devices(drivers: list[dict], ready_ids: set[str], cfg: dict)
459
482
  if d.get("category") == "display" and d.get("hwid") and _bid(d) in ready_ids
460
483
  }
461
484
  if not display_hwids:
462
- return
485
+ return False
463
486
 
464
487
  canon_targets = {hwid_mod.canonicalize(h) for h in display_hwids}
465
- scan_result = scan(force=False)
466
- for dev in scan_result.devices:
467
- if any(hwid_mod.canonicalize(h) in canon_targets for h in dev.hwids):
468
- _refresh_device(dev.instance_id, cfg)
488
+ # force=True: we just installed/staged drivers, so a cached scan would be stale.
489
+ scan_result = scan(force=True)
490
+ targets = [
491
+ dev
492
+ for dev in scan_result.devices
493
+ if any(hwid_mod.canonicalize(h) in canon_targets for h in dev.hwids)
494
+ ]
495
+ if not targets:
496
+ return False
497
+
498
+ for dev in targets:
499
+ _refresh_device(dev.instance_id, cfg)
500
+
501
+ # Verify the refresh actually switched each device off its inbox/Basic
502
+ # Display fallback onto the OEM driver now in the store.
503
+ verify = scan(force=True)
504
+ by_instance = {d.instance_id: d for d in verify.devices}
505
+ still_inbox = []
506
+ for dev in targets:
507
+ cur = by_instance.get(dev.instance_id)
508
+ # No longer present (rare) is treated as "resolved"; still on an inbox
509
+ # driver (or no driver at all) means the OEM driver did not take.
510
+ if cur is not None and (not cur.has_driver or cur.is_inbox):
511
+ still_inbox.append(cur)
512
+
513
+ for dev in still_inbox:
514
+ label = dev.device_name or dev.instance_id
515
+ events.warn(
516
+ "install",
517
+ f"[install] Display device still on inbox/Basic Display after refresh: "
518
+ f"{label} (now: {dev.driver_name or 'no driver'}) — will reboot to bind",
519
+ instance_id=dev.instance_id,
520
+ driver_name=dev.driver_name,
521
+ )
522
+ return bool(still_inbox)
469
523
 
470
524
 
471
525
  def _refresh_device(instance_id: str, cfg: dict) -> None:
File without changes
File without changes