driverclient 0.2.21__tar.gz → 0.2.23__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.21 → driverclient-0.2.23}/PKG-INFO +1 -1
  2. {driverclient-0.2.21 → driverclient-0.2.23}/pyproject.toml +1 -1
  3. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/__init__.py +1 -1
  4. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/ops/install.py +80 -4
  5. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/ops/offline.py +4 -4
  6. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/ops/scan.py +1 -1
  7. {driverclient-0.2.21 → driverclient-0.2.23}/.gitignore +0 -0
  8. {driverclient-0.2.21 → driverclient-0.2.23}/README.md +0 -0
  9. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/__main__.py +0 -0
  10. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/config.json +0 -0
  11. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/config.py +0 -0
  12. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/core/__init__.py +0 -0
  13. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/core/hardware.py +0 -0
  14. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/core/http.py +0 -0
  15. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/core/hwid.py +0 -0
  16. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/core/proc.py +0 -0
  17. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/events.py +0 -0
  18. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/main.py +0 -0
  19. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/ops/__init__.py +0 -0
  20. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/ops/automate.py +0 -0
  21. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/ops/capture.py +0 -0
  22. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/ops/diagnose.py +0 -0
  23. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/ops/download.py +0 -0
  24. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/ops/resolve.py +0 -0
  25. {driverclient-0.2.21 → driverclient-0.2.23}/src/driverclient/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: driverclient
3
- Version: 0.2.21
3
+ Version: 0.2.23
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.21"
7
+ version = "0.2.23"
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"
@@ -25,7 +25,7 @@ from driverclient.main import run as _run
25
25
 
26
26
  __all__ = ["DriverClient", "ClientEvent"]
27
27
 
28
- __version__ = "0.2.21"
28
+ __version__ = "0.2.23"
29
29
 
30
30
 
31
31
  class DriverClient:
@@ -26,9 +26,11 @@ from pathlib import Path
26
26
  from driverclient import events
27
27
  from driverclient.config import get
28
28
  from driverclient.core import proc
29
+ from driverclient.core import hwid as hwid_mod
29
30
  from driverclient.core.http import http_post_retry
30
31
  from driverclient.ops.download import download_driver
31
32
  from driverclient.ops.resolve import ResolveResult, resolve
33
+ from driverclient.ops.scan import scan
32
34
 
33
35
  # Install order: lower number first (hardware dependency)
34
36
  _CATEGORY_ORDER: dict[str, int] = {
@@ -167,9 +169,11 @@ def resolve_and_install(force: bool = False) -> InstallResult:
167
169
  idx: dict = _installed_inf_index() if skip_installed else {}
168
170
 
169
171
  drivers = result.drivers
172
+ already_in_store: list[dict] = []
170
173
  if skip_installed:
174
+ already_in_store = [d for d in drivers if _already_in_store(d, idx)]
171
175
  kept = [d for d in drivers if not _already_in_store(d, idx)]
172
- skipped = len(drivers) - len(kept)
176
+ skipped = len(already_in_store)
173
177
  if skipped:
174
178
  events.progress(
175
179
  "install",
@@ -178,6 +182,15 @@ def resolve_and_install(force: bool = False) -> InstallResult:
178
182
  )
179
183
  drivers = kept
180
184
  if not drivers:
185
+ # A display driver can be sitting in the store (e.g. staged offline
186
+ # via DISM during imaging) without ever being BOUND to the device —
187
+ # the primary adapter doesn't reliably switch itself over. Refresh it
188
+ # now instead of leaving that gap for Windows Update to close later.
189
+ _refresh_bound_devices(
190
+ already_in_store,
191
+ {(d.get("binding_id") or d.get("package_id", "")) for d in already_in_store},
192
+ cfg,
193
+ )
181
194
  events.done("install",
182
195
  "[install] Nothing to install — all resolved drivers already current",
183
196
  total=0)
@@ -201,6 +214,16 @@ def resolve_and_install(force: bool = False) -> InstallResult:
201
214
  # Skipped drivers ARE satisfied on the machine — report them upstream as success.
202
215
  _confirm(result.trace_id, installed_ok + skipped_now, install_failed, cfg)
203
216
 
217
+ # Force-bind any display driver now confirmed in the store — freshly
218
+ # installed here, or already present from an earlier offline/DISM pass —
219
+ # so the device actually switches over instead of staying on whatever it
220
+ # booted with until Windows Update eventually forces a real reinstall.
221
+ ready_ids = (
222
+ {r.binding_id for r in installed_ok}
223
+ | {(d.get("binding_id") or d.get("package_id", "")) for d in already_in_store}
224
+ )
225
+ _refresh_bound_devices(result.drivers, ready_ids, cfg)
226
+
204
227
  reboot_required = any(r.reboot_required for r in installed_ok)
205
228
  reboot_tag = " — reboot required" if reboot_required else ""
206
229
  skip_tag = f" ({len(skipped_now)} already in store)" if skipped_now else ""
@@ -209,7 +232,7 @@ def resolve_and_install(force: bool = False) -> InstallResult:
209
232
  total=len(all_results), ok=len(installed_ok), failed=len(install_failed),
210
233
  skipped=len(skipped_now), reboot_required=reboot_required)
211
234
  for r in install_failed:
212
- events.error("install", f" {r.binding_id}: {r.error}",
235
+ events.error("install", f" [x] {r.binding_id}: {r.error}",
213
236
  binding_id=r.binding_id, error=r.error)
214
237
 
215
238
  return InstallResult(
@@ -330,7 +353,7 @@ def _batch_install_infs(inf_drivers: list[tuple[dict, Path]], base: Path,
330
353
  tag = " (reboot required)" if reboot else ""
331
354
  (events.ok if ok else events.error)(
332
355
  "install",
333
- f" {'' if ok else ''} batch-installed {n} INF driver(s) ({dur}ms){tag}",
356
+ f" {'[ok]' if ok else '[x]'} batch-installed {n} INF driver(s) ({dur}ms){tag}",
334
357
  count=n, duration_ms=dur, reboot_required=reboot, error=err)
335
358
 
336
359
  return [
@@ -359,7 +382,7 @@ def _install_one_exe(driver: dict, driver_dir: Path, cfg: dict) -> DriverResult:
359
382
  ok, err, reboot = False, str(e), False
360
383
  dur = int((time.monotonic() - t0) * 1000)
361
384
  (events.ok if ok else events.error)(
362
- "install", f" {'' if ok else ''} [exe] {bid} ({dur}ms)",
385
+ "install", f" {'[ok]' if ok else '[x]'} [exe] {bid} ({dur}ms)",
363
386
  binding_id=bid, duration_ms=dur, error=err, reboot_required=reboot)
364
387
  return DriverResult(binding_id=bid, success=ok, error=err,
365
388
  duration_ms=dur, reboot_required=reboot)
@@ -369,6 +392,59 @@ def _install_one_exe(driver: dict, driver_dir: Path, cfg: dict) -> DriverResult:
369
392
  # the offline / pre-boot path.
370
393
 
371
394
 
395
+ # ── Device refresh (force-bind display drivers) ─────────────────────────────────
396
+
397
+ def _refresh_bound_devices(drivers: list[dict], ready_ids: set[str], cfg: dict) -> None:
398
+ """Force Windows to actually bind a display driver that's confirmed in the
399
+ driver store (just installed, or already there from an earlier offline/DISM
400
+ injection) but not necessarily applied to the device yet.
401
+
402
+ pnputil only stages a package into the store — the primary display adapter
403
+ is already rendering the current session with whatever driver it booted
404
+ with and doesn't reliably re-evaluate/switch itself over on its own. Left
405
+ alone, the device stays on the fallback driver until something forces a
406
+ real reinstall — which is what Windows' own automatic Windows Update
407
+ driver search eventually does, closing the gap our own pipeline left open.
408
+ Disabling + re-enabling the device here forces Windows to redo PnP install
409
+ for it immediately, picking up the driver that's already in the store.
410
+ """
411
+ display_hwids = {
412
+ d["hwid"] for d in drivers
413
+ if d.get("category") == "display" and d.get("hwid")
414
+ and (d.get("binding_id") or d.get("package_id", "")) in ready_ids
415
+ }
416
+ if not display_hwids:
417
+ return
418
+
419
+ canon_targets = {hwid_mod.canonicalize(h) for h in display_hwids}
420
+ scan_result = scan(force=False)
421
+ for dev in scan_result.devices:
422
+ if any(hwid_mod.canonicalize(h) in canon_targets for h in dev.hwids):
423
+ _refresh_device(dev.instance_id, cfg)
424
+
425
+
426
+ def _refresh_device(instance_id: str, cfg: dict) -> None:
427
+ """Disable then re-enable one device by instance ID — forces Windows to redo
428
+ PnP install for it, so a driver already in the store but not yet bound
429
+ takes effect immediately, without a reboot."""
430
+ events.progress("install",
431
+ f"[install] Refreshing device to bind new driver: {instance_id}",
432
+ instance_id=instance_id)
433
+ timeout = cfg.get("timeout_short", 30)
434
+ try:
435
+ proc.run(["pnputil", "/disable-device", instance_id],
436
+ capture_output=True, text=True, timeout=timeout)
437
+ r = proc.run(["pnputil", "/enable-device", instance_id],
438
+ capture_output=True, text=True, timeout=timeout)
439
+ ok = r.returncode == 0
440
+ (events.ok if ok else events.error)(
441
+ "install", f" {'[ok]' if ok else '[x]'} refreshed {instance_id}",
442
+ instance_id=instance_id, error="" if ok else r.stdout.strip()[:200])
443
+ except Exception as e:
444
+ events.error("install", f" [x] refresh failed for {instance_id}: {e}",
445
+ instance_id=instance_id, error=str(e))
446
+
447
+
372
448
  # ── Driver installation ─────────────────────────────────────────────────────────
373
449
 
374
450
  def _classify(returncode: int, stdout: str = "", stderr: str = "") -> tuple[bool, str, bool]:
@@ -70,7 +70,7 @@ def resolve_and_download() -> OfflineResult:
70
70
 
71
71
  drivers = result.drivers
72
72
  events.start("offline",
73
- f"[offline] {len(drivers)} driver(s) downloading to {target} ",
73
+ f"[offline] {len(drivers)} driver(s) -> downloading to {target} ...",
74
74
  total=len(drivers))
75
75
 
76
76
  downloaded = download_drivers(drivers, target, cfg)
@@ -82,12 +82,12 @@ def resolve_and_download() -> OfflineResult:
82
82
  (ok if success else failed).append(bid)
83
83
 
84
84
  for bid in failed:
85
- events.error("offline", f" {bid}: download_failed", binding_id=bid)
85
+ events.error("offline", f" [x] {bid}: download_failed", binding_id=bid)
86
86
 
87
87
  events.done(
88
88
  "offline",
89
- f"[offline] Done {len(ok)} downloaded {len(failed)} failed "
90
- f" {target} (DISM injects offline next)",
89
+ f"[offline] Done - {len(ok)} downloaded {len(failed)} failed "
90
+ f"-> {target} (DISM injects offline next)",
91
91
  total=len(downloaded), ok=len(ok), failed=len(failed),
92
92
  target_dir=str(target),
93
93
  )
@@ -369,7 +369,7 @@ def _print_summary(r: ScanResult) -> None:
369
369
  events.progress("scan", "[scan] Missing drivers:")
370
370
  for d in r.missing_driver:
371
371
  name = _device_label(d)
372
- events.progress("scan", f" {name}",
372
+ events.progress("scan", f" [x] {name}",
373
373
  device_name=name, class_name=d.class_name)
374
374
 
375
375
 
File without changes
File without changes