driverclient 0.2.25__tar.gz → 0.2.27__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.25 → driverclient-0.2.27}/PKG-INFO +1 -1
- {driverclient-0.2.25 → driverclient-0.2.27}/pyproject.toml +1 -1
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/__init__.py +1 -1
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/ops/automate.py +1 -6
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/ops/capture.py +16 -3
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/ops/install.py +63 -18
- {driverclient-0.2.25 → driverclient-0.2.27}/.gitignore +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/README.md +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/__main__.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/config.json +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/config.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/core/__init__.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/core/hardware.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/core/http.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/core/hwid.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/core/proc.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/events.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/main.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/ops/__init__.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/ops/diagnose.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/ops/download.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/ops/offline.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/ops/resolve.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/src/driverclient/ops/scan.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.27}/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.27
|
|
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.27"
|
|
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"
|
|
@@ -18,7 +18,7 @@ from datetime import datetime, timezone
|
|
|
18
18
|
from driverclient import events
|
|
19
19
|
from driverclient.config import get
|
|
20
20
|
from driverclient.ops.capture import CaptureResult, classify_missing, wu_update
|
|
21
|
-
from driverclient.ops.install import DriverResult, InstallResult, resolve_and_install
|
|
21
|
+
from driverclient.ops.install import DriverResult, InstallResult, _bid, resolve_and_install
|
|
22
22
|
from driverclient.ops.resolve import ResolveResult, resolve
|
|
23
23
|
from driverclient.ops.scan import ScanResult, scan
|
|
24
24
|
|
|
@@ -113,11 +113,6 @@ def _step(n: int, label: str) -> None:
|
|
|
113
113
|
step=n, label=label)
|
|
114
114
|
|
|
115
115
|
|
|
116
|
-
def _bid(driver: dict) -> str:
|
|
117
|
-
"""Stable identity for a repo driver entry (matches install's dedup key)."""
|
|
118
|
-
return driver.get("binding_id") or driver.get("package_id", "")
|
|
119
|
-
|
|
120
|
-
|
|
121
116
|
def _merge_install(combined: InstallResult, inst: InstallResult) -> None:
|
|
122
117
|
"""Fold one pass's results into the running total, deduped by binding_id so a
|
|
123
118
|
same-boot re-attempt of an already-recorded driver is not double-counted; a
|
|
@@ -344,11 +344,22 @@ def wu_full() -> CaptureResult:
|
|
|
344
344
|
|
|
345
345
|
# ══ Missing-HWID classification ═══════════════════════════════════════════════
|
|
346
346
|
|
|
347
|
+
_INBOX_INADEQUATE_CLASSES = {"display adapters"}
|
|
348
|
+
|
|
349
|
+
|
|
347
350
|
def classify_missing(not_found: list[str], scan_result: ScanResult) -> dict[str, list[str]]:
|
|
348
351
|
"""Bucket repo-missing HWIDs by what the machine says about their device:
|
|
349
352
|
inbox (Windows ships the driver — not applicable), oem_local (OEM driver
|
|
350
353
|
installed locally — capturable), driverless (genuinely missing — WU
|
|
351
|
-
candidate), unknown (HWID not in scan).
|
|
354
|
+
candidate), unknown (HWID not in scan).
|
|
355
|
+
|
|
356
|
+
Display adapters are excluded from the inbox bucket: Microsoft Basic
|
|
357
|
+
Display Adapter is Windows' inbox fallback for a GPU whose real driver
|
|
358
|
+
was never bound, and it leaves the device non-accelerated — unlike other
|
|
359
|
+
inbox drivers (HID, mass storage, ...) it is not adequate coverage. Such
|
|
360
|
+
a device is treated as driverless so it still reaches WU / gets captured
|
|
361
|
+
once a real driver shows up, instead of being silently written off.
|
|
362
|
+
"""
|
|
352
363
|
hwid_to_device: dict[str, Device] = {}
|
|
353
364
|
for dev in scan_result.devices:
|
|
354
365
|
for h in dev.hwids:
|
|
@@ -363,8 +374,10 @@ def classify_missing(not_found: list[str], scan_result: ScanResult) -> dict[str,
|
|
|
363
374
|
buckets["unknown"].append(hwid)
|
|
364
375
|
elif not dev.has_driver:
|
|
365
376
|
buckets["driverless"].append(hwid)
|
|
366
|
-
elif dev.is_inbox:
|
|
377
|
+
elif dev.is_inbox and dev.class_name.lower() not in _INBOX_INADEQUATE_CLASSES:
|
|
367
378
|
buckets["inbox"].append(hwid)
|
|
379
|
+
elif dev.is_inbox:
|
|
380
|
+
buckets["driverless"].append(hwid)
|
|
368
381
|
else:
|
|
369
382
|
buckets["oem_local"].append(hwid)
|
|
370
383
|
return buckets
|
|
@@ -1070,4 +1083,4 @@ def _upload_worker(upload_q: queue.Queue, counts: dict,
|
|
|
1070
1083
|
# Extension selection was removed: capture now packs every file
|
|
1071
1084
|
# pnputil /export-driver produces (see _do_export), so the dump_extensions /
|
|
1072
1085
|
# dump_extensions_overrides config keys are no longer consulted — filtering by
|
|
1073
|
-
# extension is what caused incomplete packages (broken link D).
|
|
1086
|
+
# extension is what caused incomplete packages (broken link D).
|
|
@@ -64,6 +64,12 @@ _PNPUTIL_SUCCESS_MARKERS: tuple[str, ...] = (
|
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
def _bid(driver: dict) -> str:
|
|
68
|
+
"""Stable identity for a repo driver entry — binding_id, falling back to
|
|
69
|
+
package_id. Shared dedup key: also imported by ops/automate.py."""
|
|
70
|
+
return driver.get("binding_id") or driver.get("package_id", "")
|
|
71
|
+
|
|
72
|
+
|
|
67
73
|
@dataclass
|
|
68
74
|
class DriverResult:
|
|
69
75
|
binding_id: str
|
|
@@ -171,8 +177,9 @@ def resolve_and_install(force: bool = False) -> InstallResult:
|
|
|
171
177
|
drivers = result.drivers
|
|
172
178
|
already_in_store: list[dict] = []
|
|
173
179
|
if skip_installed:
|
|
174
|
-
|
|
175
|
-
|
|
180
|
+
kept = []
|
|
181
|
+
for d in drivers:
|
|
182
|
+
(already_in_store if _already_in_store(d, idx) else kept).append(d)
|
|
176
183
|
skipped = len(already_in_store)
|
|
177
184
|
if skipped:
|
|
178
185
|
events.progress(
|
|
@@ -188,7 +195,7 @@ def resolve_and_install(force: bool = False) -> InstallResult:
|
|
|
188
195
|
# now instead of leaving that gap for Windows Update to close later.
|
|
189
196
|
_refresh_bound_devices(
|
|
190
197
|
already_in_store,
|
|
191
|
-
{(d
|
|
198
|
+
{_bid(d) for d in already_in_store},
|
|
192
199
|
cfg,
|
|
193
200
|
)
|
|
194
201
|
events.done("install",
|
|
@@ -220,7 +227,7 @@ def resolve_and_install(force: bool = False) -> InstallResult:
|
|
|
220
227
|
# booted with until Windows Update eventually forces a real reinstall.
|
|
221
228
|
ready_ids = (
|
|
222
229
|
{r.binding_id for r in installed_ok}
|
|
223
|
-
| {(d
|
|
230
|
+
| {_bid(d) for d in already_in_store}
|
|
224
231
|
)
|
|
225
232
|
_refresh_bound_devices(result.drivers, ready_ids, cfg)
|
|
226
233
|
|
|
@@ -276,7 +283,7 @@ def _stream_install(drivers: list[dict], base: Path, cfg: dict) -> list[DriverRe
|
|
|
276
283
|
for d in ordered}
|
|
277
284
|
for fut in as_completed(fut_map):
|
|
278
285
|
driver = fut_map[fut]
|
|
279
|
-
bid =
|
|
286
|
+
bid = _bid(driver)
|
|
280
287
|
try:
|
|
281
288
|
dest, dl_ok = fut.result()
|
|
282
289
|
except Exception as e:
|
|
@@ -304,7 +311,7 @@ def _dedup_and_group(drivers: list[dict]) -> dict[int, list[dict]]:
|
|
|
304
311
|
seen: set[str] = set()
|
|
305
312
|
by_level: dict[int, list[dict]] = defaultdict(list)
|
|
306
313
|
for d in drivers:
|
|
307
|
-
bid =
|
|
314
|
+
bid = _bid(d)
|
|
308
315
|
if bid and bid not in seen:
|
|
309
316
|
seen.add(bid)
|
|
310
317
|
level = _CATEGORY_ORDER.get(d.get("category", "other"), 10)
|
|
@@ -357,7 +364,7 @@ def _batch_install_infs(inf_drivers: list[tuple[dict, Path]], base: Path,
|
|
|
357
364
|
count=n, duration_ms=dur, reboot_required=reboot, error=err)
|
|
358
365
|
|
|
359
366
|
return [
|
|
360
|
-
DriverResult(binding_id=(d
|
|
367
|
+
DriverResult(binding_id=_bid(d),
|
|
361
368
|
success=ok, error="" if ok else err,
|
|
362
369
|
duration_ms=dur, reboot_required=reboot)
|
|
363
370
|
for d, _ in inf_drivers
|
|
@@ -366,7 +373,7 @@ def _batch_install_infs(inf_drivers: list[tuple[dict, Path]], base: Path,
|
|
|
366
373
|
|
|
367
374
|
def _install_one_exe(driver: dict, driver_dir: Path, cfg: dict) -> DriverResult:
|
|
368
375
|
"""Run one EXE-installer driver's silent setup (EXEs cannot be batched)."""
|
|
369
|
-
bid =
|
|
376
|
+
bid = _bid(driver)
|
|
370
377
|
t0 = time.monotonic()
|
|
371
378
|
try:
|
|
372
379
|
exe_files = list(driver_dir.glob("*.exe"))
|
|
@@ -411,7 +418,7 @@ def _refresh_bound_devices(drivers: list[dict], ready_ids: set[str], cfg: dict)
|
|
|
411
418
|
display_hwids = {
|
|
412
419
|
d["hwid"] for d in drivers
|
|
413
420
|
if d.get("category") == "display" and d.get("hwid")
|
|
414
|
-
and (d
|
|
421
|
+
and _bid(d) in ready_ids
|
|
415
422
|
}
|
|
416
423
|
if not display_hwids:
|
|
417
424
|
return
|
|
@@ -426,23 +433,61 @@ def _refresh_bound_devices(drivers: list[dict], ready_ids: set[str], cfg: dict)
|
|
|
426
433
|
def _refresh_device(instance_id: str, cfg: dict) -> None:
|
|
427
434
|
"""Disable then re-enable one device by instance ID — forces Windows to redo
|
|
428
435
|
PnP install for it, so a driver already in the store but not yet bound
|
|
429
|
-
takes effect immediately, without a reboot.
|
|
436
|
+
takes effect immediately, without a reboot.
|
|
437
|
+
|
|
438
|
+
A disable that succeeds but is never followed by a working re-enable is the
|
|
439
|
+
worst outcome here: unlike a failed install, a `pnputil /disable-device`
|
|
440
|
+
persists across reboot, so the device (the primary display adapter, in the
|
|
441
|
+
case this exists for) does not self-heal — it silently drops to Microsoft
|
|
442
|
+
Basic Display until someone notices and re-enables it by hand. So the
|
|
443
|
+
re-enable is attempted in `finally` regardless of how disable went (an
|
|
444
|
+
enable-device call on an already-enabled device is a harmless no-op),
|
|
445
|
+
retried a few times, and a persistent failure is escalated loudly rather
|
|
446
|
+
than swallowed.
|
|
447
|
+
"""
|
|
430
448
|
events.progress("install",
|
|
431
449
|
f"[install] Refreshing device to bind new driver: {instance_id}",
|
|
432
450
|
instance_id=instance_id)
|
|
433
451
|
timeout = cfg.get("timeout_short", 30)
|
|
434
452
|
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],
|
|
453
|
+
r = proc.run(["pnputil", "/disable-device", instance_id],
|
|
438
454
|
capture_output=True, text=True, timeout=timeout)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
455
|
+
if r.returncode != 0:
|
|
456
|
+
events.warn("install",
|
|
457
|
+
f" [!] disable-device returned {r.returncode} for {instance_id}",
|
|
458
|
+
instance_id=instance_id, error=r.stdout.strip()[:200])
|
|
443
459
|
except Exception as e:
|
|
444
|
-
events.
|
|
460
|
+
events.warn("install", f" [!] disable-device raised for {instance_id}: {e}",
|
|
445
461
|
instance_id=instance_id, error=str(e))
|
|
462
|
+
finally:
|
|
463
|
+
ok, err = _enable_with_retries(instance_id, timeout)
|
|
464
|
+
if ok:
|
|
465
|
+
events.ok("install", f" [ok] refreshed {instance_id}", instance_id=instance_id)
|
|
466
|
+
else:
|
|
467
|
+
events.error(
|
|
468
|
+
"install",
|
|
469
|
+
f" [!!] CRITICAL: {instance_id} may be left DISABLED after refresh — "
|
|
470
|
+
f"run `pnputil /enable-device \"{instance_id}\"` manually to recover",
|
|
471
|
+
instance_id=instance_id, error=err, critical=True)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _enable_with_retries(instance_id: str, timeout: int, attempts: int = 3) -> tuple[bool, str]:
|
|
475
|
+
"""Run `pnputil /enable-device`, retrying a couple of times before giving up —
|
|
476
|
+
a transient PnP busy state can fail the first attempt even though the device
|
|
477
|
+
would enable fine a moment later. Returns (success, last_error)."""
|
|
478
|
+
last_err = ""
|
|
479
|
+
for attempt in range(1, attempts + 1):
|
|
480
|
+
try:
|
|
481
|
+
r = proc.run(["pnputil", "/enable-device", instance_id],
|
|
482
|
+
capture_output=True, text=True, timeout=timeout)
|
|
483
|
+
if r.returncode == 0:
|
|
484
|
+
return True, ""
|
|
485
|
+
last_err = r.stdout.strip()[:200]
|
|
486
|
+
except Exception as e:
|
|
487
|
+
last_err = str(e)
|
|
488
|
+
if attempt < attempts:
|
|
489
|
+
time.sleep(2)
|
|
490
|
+
return False, last_err
|
|
446
491
|
|
|
447
492
|
|
|
448
493
|
# ── Driver installation ─────────────────────────────────────────────────────────
|
|
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
|