driverclient 0.2.19__tar.gz → 0.2.20__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.19 → driverclient-0.2.20}/PKG-INFO +1 -1
- {driverclient-0.2.19 → driverclient-0.2.20}/pyproject.toml +1 -1
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/__init__.py +1 -1
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/ops/install.py +124 -149
- {driverclient-0.2.19 → driverclient-0.2.20}/.gitignore +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/README.md +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/__main__.py +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/config.json +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/config.py +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/core/__init__.py +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/core/hardware.py +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/core/http.py +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/core/hwid.py +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/core/proc.py +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/events.py +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/main.py +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/ops/__init__.py +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/ops/automate.py +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/ops/capture.py +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/ops/diagnose.py +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/ops/resolve.py +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/ops/scan.py +0 -0
- {driverclient-0.2.19 → driverclient-0.2.20}/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.20
|
|
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.20"
|
|
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"
|
|
@@ -19,7 +19,7 @@ import subprocess
|
|
|
19
19
|
import time
|
|
20
20
|
import uuid
|
|
21
21
|
from collections import defaultdict
|
|
22
|
-
from concurrent.futures import
|
|
22
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
23
23
|
from dataclasses import dataclass, field
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
|
|
@@ -155,12 +155,13 @@ def resolve_and_install(force: bool = False) -> InstallResult:
|
|
|
155
155
|
# at the same-or-newer version — so a still-current big driver (graphics) is not
|
|
156
156
|
# re-downloaded, while missing/extension drivers still install. Toggle off with
|
|
157
157
|
# skip_installed_drivers=false to force-reinstall everything.
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
#
|
|
158
|
+
# Driver-store index (INF name -> highest installed version), built once to
|
|
159
|
+
# FILTER the download list: skip any package the store already has at the
|
|
160
|
+
# same-or-newer version. This is skip-if-present (idempotency), NOT dedup —
|
|
161
|
+
# no repo package is removed; a newer repo version still installs over an
|
|
162
|
+
# older store version (store_ver >= repo_ver skips). Duplicate versions that
|
|
163
|
+
# survive the filter are resolved at install time by a single batched pnputil
|
|
164
|
+
# pass, where Windows picks the best driver per device.
|
|
164
165
|
skip_installed = cfg.get("skip_installed_drivers", True)
|
|
165
166
|
idx: dict = _installed_inf_index() if skip_installed else {}
|
|
166
167
|
|
|
@@ -188,7 +189,7 @@ def resolve_and_install(force: bool = False) -> InstallResult:
|
|
|
188
189
|
events.start("install",
|
|
189
190
|
f"[install] {len(drivers)} driver(s) — starting parallel downloads…",
|
|
190
191
|
total=len(drivers))
|
|
191
|
-
all_results = _stream_install(drivers, work_dir, cfg
|
|
192
|
+
all_results = _stream_install(drivers, work_dir, cfg)
|
|
192
193
|
|
|
193
194
|
shutil.rmtree(work_dir, ignore_errors=True)
|
|
194
195
|
|
|
@@ -219,34 +220,59 @@ def resolve_and_install(force: bool = False) -> InstallResult:
|
|
|
219
220
|
|
|
220
221
|
# ── Streaming pipeline ──────────────────────────────────────────────────────────
|
|
221
222
|
|
|
222
|
-
def _stream_install(drivers: list[dict], base: Path, cfg: dict
|
|
223
|
-
idx: dict, skip_installed: bool) -> list[DriverResult]:
|
|
223
|
+
def _stream_install(drivers: list[dict], base: Path, cfg: dict) -> list[DriverResult]:
|
|
224
224
|
"""
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
225
|
+
Download every driver in parallel, then install in the fewest passes possible:
|
|
226
|
+
|
|
227
|
+
• all INF drivers -> ONE `pnputil /add-driver <base>\\*.inf /subdirs /install`
|
|
228
|
+
pass. Windows stages every package and installs the best/newest driver per
|
|
229
|
+
device in a single PnP reconciliation — far faster than one pnputil call
|
|
230
|
+
per driver, and it resolves duplicate versions itself (so "no dedup" is
|
|
231
|
+
handled at install time here, not by dropping packages).
|
|
232
|
+
• EXE-installer drivers -> run their silent installer individually (EXEs
|
|
233
|
+
cannot be batched).
|
|
234
|
+
|
|
235
|
+
Downloads stay fully parallel; only the install strategy changed from one
|
|
236
|
+
pnputil call per driver to a single batched call.
|
|
232
237
|
"""
|
|
233
238
|
repo_url = cfg["local_repo_url"]
|
|
234
239
|
parallel = cfg.get("parallel_downloads", 6)
|
|
235
240
|
|
|
236
|
-
by_level
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
241
|
+
by_level = _dedup_and_group(drivers)
|
|
242
|
+
ordered = [d for level in sorted(by_level) for d in by_level[level]]
|
|
243
|
+
|
|
244
|
+
events.progress("install",
|
|
245
|
+
f"[install] Downloading {len(ordered)} driver(s) in parallel…",
|
|
246
|
+
total=len(ordered))
|
|
247
|
+
|
|
248
|
+
results: list[DriverResult] = []
|
|
249
|
+
downloaded: list[tuple[dict, Path]] = []
|
|
250
|
+
with ThreadPoolExecutor(max_workers=parallel) as executor:
|
|
251
|
+
fut_map = {executor.submit(_download_driver, d, base, repo_url, cfg): d
|
|
252
|
+
for d in ordered}
|
|
253
|
+
for fut in as_completed(fut_map):
|
|
254
|
+
driver = fut_map[fut]
|
|
255
|
+
bid = driver.get("binding_id") or driver.get("package_id", "")
|
|
256
|
+
try:
|
|
257
|
+
dest, dl_ok = fut.result()
|
|
258
|
+
except Exception as e:
|
|
259
|
+
results.append(DriverResult(binding_id=bid, success=False, error=str(e)))
|
|
260
|
+
continue
|
|
261
|
+
if dl_ok:
|
|
262
|
+
downloaded.append((driver, dest))
|
|
263
|
+
else:
|
|
264
|
+
results.append(DriverResult(binding_id=bid, success=False,
|
|
265
|
+
error="download_failed"))
|
|
266
|
+
|
|
267
|
+
inf_drivers = [(d, p) for d, p in downloaded
|
|
268
|
+
if d.get("install_type", "inf_silent") != "exe_silent"]
|
|
269
|
+
exe_drivers = [(d, p) for d, p in downloaded
|
|
270
|
+
if d.get("install_type", "inf_silent") == "exe_silent"]
|
|
271
|
+
|
|
272
|
+
results += _batch_install_infs(inf_drivers, base, cfg)
|
|
273
|
+
for driver, dest in exe_drivers:
|
|
274
|
+
results.append(_install_one_exe(driver, dest, cfg))
|
|
275
|
+
return results
|
|
250
276
|
|
|
251
277
|
|
|
252
278
|
def _dedup_and_group(drivers: list[dict]) -> dict[int, list[dict]]:
|
|
@@ -262,81 +288,82 @@ def _dedup_and_group(drivers: list[dict]) -> dict[int, list[dict]]:
|
|
|
262
288
|
return by_level
|
|
263
289
|
|
|
264
290
|
|
|
265
|
-
def
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
fut: Future,
|
|
285
|
-
future_info: dict[Future, tuple[int, dict]],
|
|
286
|
-
cfg: dict,
|
|
287
|
-
idx: dict,
|
|
288
|
-
skip_installed: bool,
|
|
289
|
-
) -> DriverResult:
|
|
290
|
-
"""Handle one completed download Future: install if ok, return DriverResult."""
|
|
291
|
-
_, driver = future_info[fut]
|
|
292
|
-
bid = driver.get("binding_id", "")
|
|
293
|
-
cat = driver.get("category", "other")
|
|
294
|
-
|
|
295
|
-
# A driver installed earlier THIS pass may have added the same INF (another
|
|
296
|
-
# package of the same driver family), so a package that was missing at
|
|
297
|
-
# pass-start can now already be in the store at an equal-or-newer version.
|
|
298
|
-
# Re-check against the live index and skip the redundant re-install.
|
|
299
|
-
if skip_installed and _already_in_store(driver, idx):
|
|
300
|
-
events.ok("install", f" ⭑ [{cat}] {bid} already in driver store — skipped",
|
|
301
|
-
binding_id=bid, category=cat, skipped=True)
|
|
302
|
-
return DriverResult(binding_id=bid, success=True, skipped=True)
|
|
303
|
-
|
|
291
|
+
def _batch_install_infs(inf_drivers: list[tuple[dict, Path]], base: Path,
|
|
292
|
+
cfg: dict) -> list[DriverResult]:
|
|
293
|
+
"""Install every downloaded INF driver in ONE pnputil pass.
|
|
294
|
+
|
|
295
|
+
`pnputil /add-driver <base>\\*.inf /subdirs /install` recurses the staging tree
|
|
296
|
+
(each driver lives in its own subdir), adds every package to the driver store
|
|
297
|
+
and installs the best match onto each device in a single PnP reconciliation —
|
|
298
|
+
the fast path the old driverserver used (launcher.bat). pnputil does not report
|
|
299
|
+
a clean per-package status, so the batch's outcome is attributed to each driver
|
|
300
|
+
and pnputil's own output lines are surfaced so nothing is hidden by batching."""
|
|
301
|
+
if not inf_drivers:
|
|
302
|
+
return []
|
|
303
|
+
|
|
304
|
+
n = len(inf_drivers)
|
|
305
|
+
events.progress("install",
|
|
306
|
+
f"[install] Installing {n} INF driver(s) in one batched pnputil pass…",
|
|
307
|
+
total=n)
|
|
308
|
+
t0 = time.monotonic()
|
|
309
|
+
stdout = ""
|
|
304
310
|
try:
|
|
305
|
-
|
|
311
|
+
r = proc.run(
|
|
312
|
+
["pnputil", "/add-driver", str(base / "*.inf"), "/subdirs", "/install"],
|
|
313
|
+
capture_output=True, text=True,
|
|
314
|
+
timeout=cfg.get("timeout_install", 600),
|
|
315
|
+
)
|
|
316
|
+
stdout = r.stdout or ""
|
|
317
|
+
ok, err, reboot = _classify(r.returncode, r.stdout, r.stderr)
|
|
318
|
+
except subprocess.TimeoutExpired:
|
|
319
|
+
ok, err, reboot = False, "Batch install timed out", False
|
|
306
320
|
except Exception as e:
|
|
307
|
-
|
|
321
|
+
ok, err, reboot = False, str(e), False
|
|
322
|
+
dur = int((time.monotonic() - t0) * 1000)
|
|
308
323
|
|
|
309
|
-
|
|
310
|
-
|
|
324
|
+
for line in stdout.splitlines():
|
|
325
|
+
s = line.strip()
|
|
326
|
+
if s:
|
|
327
|
+
events.progress("install", f" {s}")
|
|
311
328
|
|
|
312
|
-
t0 = time.monotonic()
|
|
313
|
-
ok, err, reboot = _install_driver(driver, dest, cfg)
|
|
314
|
-
dur = int((time.monotonic() - t0) * 1000)
|
|
315
|
-
if ok:
|
|
316
|
-
_record_installed(driver, idx) # update live index so same-family dupes skip
|
|
317
329
|
tag = " (reboot required)" if reboot else ""
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
330
|
+
(events.ok if ok else events.error)(
|
|
331
|
+
"install",
|
|
332
|
+
f" {'✓' if ok else '✗'} batch-installed {n} INF driver(s) ({dur}ms){tag}",
|
|
333
|
+
count=n, duration_ms=dur, reboot_required=reboot, error=err)
|
|
334
|
+
|
|
335
|
+
return [
|
|
336
|
+
DriverResult(binding_id=(d.get("binding_id") or d.get("package_id", "")),
|
|
337
|
+
success=ok, error="" if ok else err,
|
|
338
|
+
duration_ms=dur, reboot_required=reboot)
|
|
339
|
+
for d, _ in inf_drivers
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _install_one_exe(driver: dict, driver_dir: Path, cfg: dict) -> DriverResult:
|
|
344
|
+
"""Run one EXE-installer driver's silent setup (EXEs cannot be batched)."""
|
|
345
|
+
bid = driver.get("binding_id") or driver.get("package_id", "")
|
|
346
|
+
t0 = time.monotonic()
|
|
347
|
+
try:
|
|
348
|
+
exe_files = list(driver_dir.glob("*.exe"))
|
|
349
|
+
if not exe_files:
|
|
350
|
+
return DriverResult(binding_id=bid, success=False, error="No EXE file found")
|
|
351
|
+
flags = driver.get("install_flags", "/S")
|
|
352
|
+
r = proc.run([str(exe_files[0])] + flags.split(),
|
|
353
|
+
capture_output=True, timeout=cfg["timeout_install"])
|
|
354
|
+
ok, err, reboot = _classify(r.returncode)
|
|
355
|
+
except subprocess.TimeoutExpired:
|
|
356
|
+
ok, err, reboot = False, "Install timed out", False
|
|
357
|
+
except Exception as e:
|
|
358
|
+
ok, err, reboot = False, str(e), False
|
|
359
|
+
dur = int((time.monotonic() - t0) * 1000)
|
|
360
|
+
(events.ok if ok else events.error)(
|
|
361
|
+
"install", f" {'✓' if ok else '✗'} [exe] {bid} ({dur}ms)",
|
|
362
|
+
binding_id=bid, duration_ms=dur, error=err, reboot_required=reboot)
|
|
322
363
|
return DriverResult(binding_id=bid, success=ok, error=err,
|
|
323
364
|
duration_ms=dur, reboot_required=reboot)
|
|
324
365
|
|
|
325
366
|
|
|
326
|
-
def _record_installed(driver: dict, idx: dict) -> None:
|
|
327
|
-
"""Fold a just-installed driver's INF(s)+version into the live store index, so a
|
|
328
|
-
later package of the same driver family at an equal-or-older version is skipped
|
|
329
|
-
this pass — mirrors what `pnputil /enum-drivers` will report on the next scan."""
|
|
330
|
-
ver = _parse_ver(driver.get("version_label", ""))
|
|
331
|
-
if not ver:
|
|
332
|
-
return
|
|
333
|
-
for f in driver.get("files", []):
|
|
334
|
-
name = f.get("filename", "").lower()
|
|
335
|
-
if f.get("file_role") == "inf" or name.endswith(".inf"):
|
|
336
|
-
if idx.get(name) is None or ver > idx[name]:
|
|
337
|
-
idx[name] = ver
|
|
338
|
-
|
|
339
|
-
|
|
340
367
|
def _download_driver(driver: dict, base: Path, repo_url: str,
|
|
341
368
|
cfg: dict) -> tuple[Path, bool]:
|
|
342
369
|
"""Download all files for one driver. Returns (dest_dir, success)."""
|
|
@@ -356,12 +383,6 @@ def _download_driver(driver: dict, base: Path, repo_url: str,
|
|
|
356
383
|
return dest, ok
|
|
357
384
|
|
|
358
385
|
|
|
359
|
-
def _level_label(level: int) -> str:
|
|
360
|
-
labels = {0: "chipset", 1: "firmware", 2: "storage",
|
|
361
|
-
3: "network", 5: "display", 6: "audio", 10: "other"}
|
|
362
|
-
return labels.get(level, f"level-{level}")
|
|
363
|
-
|
|
364
|
-
|
|
365
386
|
# ── Driver installation ─────────────────────────────────────────────────────────
|
|
366
387
|
|
|
367
388
|
def _classify(returncode: int, stdout: str = "", stderr: str = "") -> tuple[bool, str, bool]:
|
|
@@ -397,52 +418,6 @@ def _classify(returncode: int, stdout: str = "", stderr: str = "") -> tuple[bool
|
|
|
397
418
|
return False, (stderr.strip() or stdout.strip() or f"exit {returncode}")[:200], False
|
|
398
419
|
|
|
399
420
|
|
|
400
|
-
def _install_driver(driver: dict, driver_dir: Path, cfg: dict) -> tuple[bool, str, bool]:
|
|
401
|
-
install_type = driver.get("install_type", "inf_silent")
|
|
402
|
-
|
|
403
|
-
try:
|
|
404
|
-
if install_type == "inf_silent":
|
|
405
|
-
inf_files = list(driver_dir.glob("*.inf"))
|
|
406
|
-
if not inf_files:
|
|
407
|
-
return False, "No INF file found", False
|
|
408
|
-
r = proc.run(
|
|
409
|
-
["pnputil", "/add-driver", str(inf_files[0]), "/install", "/subdirs"],
|
|
410
|
-
capture_output=True, text=True,
|
|
411
|
-
timeout=cfg["timeout_long"],
|
|
412
|
-
)
|
|
413
|
-
return _classify(r.returncode, r.stdout, r.stderr)
|
|
414
|
-
|
|
415
|
-
if install_type == "exe_silent":
|
|
416
|
-
exe_files = list(driver_dir.glob("*.exe"))
|
|
417
|
-
if not exe_files:
|
|
418
|
-
return False, "No EXE file found", False
|
|
419
|
-
flags = driver.get("install_flags", "/S")
|
|
420
|
-
r = proc.run(
|
|
421
|
-
[str(exe_files[0])] + flags.split(),
|
|
422
|
-
capture_output=True,
|
|
423
|
-
timeout=cfg["timeout_install"],
|
|
424
|
-
)
|
|
425
|
-
return _classify(r.returncode)
|
|
426
|
-
|
|
427
|
-
if install_type == "firmware_flash":
|
|
428
|
-
inf_files = list(driver_dir.glob("*.inf"))
|
|
429
|
-
if not inf_files:
|
|
430
|
-
return False, "No firmware INF", False
|
|
431
|
-
r = proc.run(
|
|
432
|
-
["pnputil", "/add-driver", str(inf_files[0]), "/install"],
|
|
433
|
-
capture_output=True, text=True,
|
|
434
|
-
timeout=cfg["timeout_long"],
|
|
435
|
-
)
|
|
436
|
-
return _classify(r.returncode, r.stdout, r.stderr)
|
|
437
|
-
|
|
438
|
-
except subprocess.TimeoutExpired:
|
|
439
|
-
return False, "Install timed out", False
|
|
440
|
-
except Exception as e:
|
|
441
|
-
return False, str(e), False
|
|
442
|
-
|
|
443
|
-
return False, f"Unknown install_type: {install_type}", False
|
|
444
|
-
|
|
445
|
-
|
|
446
421
|
# ── Confirm ─────────────────────────────────────────────────────────────────────
|
|
447
422
|
|
|
448
423
|
def _confirm(trace_id: str, ok: list[DriverResult], fail: list[DriverResult],
|
|
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
|