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.
- {driverclient-0.2.31 → driverclient-0.2.32}/PKG-INFO +1 -1
- {driverclient-0.2.31 → driverclient-0.2.32}/pyproject.toml +1 -1
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/__init__.py +1 -1
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/capture.py +14 -2
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/install.py +67 -13
- {driverclient-0.2.31 → driverclient-0.2.32}/.gitignore +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/README.md +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/__main__.py +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/config.json +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/config.py +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/core/__init__.py +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/core/hardware.py +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/core/http.py +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/core/hwid.py +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/core/proc.py +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/events.py +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/main.py +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/__init__.py +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/automate.py +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/diagnose.py +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/download.py +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/offline.py +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/resolve.py +0 -0
- {driverclient-0.2.31 → driverclient-0.2.32}/src/driverclient/ops/scan.py +0 -0
- {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.
|
|
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.
|
|
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"
|
|
@@ -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
|
|
105
|
-
|
|
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) ->
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|