driverclient 0.2.25__tar.gz → 0.2.26__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.26}/PKG-INFO +1 -1
- {driverclient-0.2.25 → driverclient-0.2.26}/pyproject.toml +1 -1
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/__init__.py +1 -1
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/ops/automate.py +1 -6
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/ops/install.py +63 -18
- {driverclient-0.2.25 → driverclient-0.2.26}/.gitignore +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/README.md +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/__main__.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/config.json +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/config.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/core/__init__.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/core/hardware.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/core/http.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/core/hwid.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/core/proc.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/events.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/main.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/ops/__init__.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/ops/capture.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/ops/diagnose.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/ops/download.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/ops/offline.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/ops/resolve.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/src/driverclient/ops/scan.py +0 -0
- {driverclient-0.2.25 → driverclient-0.2.26}/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.26
|
|
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.26"
|
|
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
|
|
@@ -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
|
|
File without changes
|