driverclient 0.2.18__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.18 → driverclient-0.2.20}/PKG-INFO +1 -1
- {driverclient-0.2.18 → driverclient-0.2.20}/pyproject.toml +1 -1
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/__init__.py +1 -1
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/ops/install.py +134 -114
- {driverclient-0.2.18 → driverclient-0.2.20}/.gitignore +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/README.md +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/__main__.py +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/config.json +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/config.py +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/core/__init__.py +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/core/hardware.py +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/core/http.py +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/core/hwid.py +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/core/proc.py +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/events.py +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/main.py +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/ops/__init__.py +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/ops/automate.py +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/ops/capture.py +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/ops/diagnose.py +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/ops/resolve.py +0 -0
- {driverclient-0.2.18 → driverclient-0.2.20}/src/driverclient/ops/scan.py +0 -0
- {driverclient-0.2.18 → 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
|
|
|
@@ -68,6 +68,7 @@ class DriverResult:
|
|
|
68
68
|
error: str = ""
|
|
69
69
|
duration_ms: int = 0
|
|
70
70
|
reboot_required: bool = False # installed OK but needs a reboot (pnputil 3010)
|
|
71
|
+
skipped: bool = False # already present in the driver store — not re-fetched/installed
|
|
71
72
|
|
|
72
73
|
|
|
73
74
|
@dataclass
|
|
@@ -154,9 +155,18 @@ def resolve_and_install(force: bool = False) -> InstallResult:
|
|
|
154
155
|
# at the same-or-newer version — so a still-current big driver (graphics) is not
|
|
155
156
|
# re-downloaded, while missing/extension drivers still install. Toggle off with
|
|
156
157
|
# skip_installed_drivers=false to force-reinstall everything.
|
|
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.
|
|
165
|
+
skip_installed = cfg.get("skip_installed_drivers", True)
|
|
166
|
+
idx: dict = _installed_inf_index() if skip_installed else {}
|
|
167
|
+
|
|
157
168
|
drivers = result.drivers
|
|
158
|
-
if
|
|
159
|
-
idx = _installed_inf_index()
|
|
169
|
+
if skip_installed:
|
|
160
170
|
kept = [d for d in drivers if not _already_in_store(d, idx)]
|
|
161
171
|
skipped = len(drivers) - len(kept)
|
|
162
172
|
if skipped:
|
|
@@ -183,17 +193,20 @@ def resolve_and_install(force: bool = False) -> InstallResult:
|
|
|
183
193
|
|
|
184
194
|
shutil.rmtree(work_dir, ignore_errors=True)
|
|
185
195
|
|
|
186
|
-
installed_ok = [r for r in all_results if r.success]
|
|
196
|
+
installed_ok = [r for r in all_results if r.success and not r.skipped]
|
|
197
|
+
skipped_now = [r for r in all_results if r.skipped]
|
|
187
198
|
install_failed = [r for r in all_results if not r.success]
|
|
188
199
|
|
|
189
|
-
|
|
200
|
+
# Skipped drivers ARE satisfied on the machine — report them upstream as success.
|
|
201
|
+
_confirm(result.trace_id, installed_ok + skipped_now, install_failed, cfg)
|
|
190
202
|
|
|
191
203
|
reboot_required = any(r.reboot_required for r in installed_ok)
|
|
192
204
|
reboot_tag = " — reboot required" if reboot_required else ""
|
|
205
|
+
skip_tag = f" ({len(skipped_now)} already in store)" if skipped_now else ""
|
|
193
206
|
events.done("install",
|
|
194
|
-
f"[install] Done — {len(installed_ok)} ok {len(install_failed)} failed{reboot_tag}",
|
|
207
|
+
f"[install] Done — {len(installed_ok)} ok {len(install_failed)} failed{skip_tag}{reboot_tag}",
|
|
195
208
|
total=len(all_results), ok=len(installed_ok), failed=len(install_failed),
|
|
196
|
-
reboot_required=reboot_required)
|
|
209
|
+
skipped=len(skipped_now), reboot_required=reboot_required)
|
|
197
210
|
for r in install_failed:
|
|
198
211
|
events.error("install", f" ✗ {r.binding_id}: {r.error}",
|
|
199
212
|
binding_id=r.binding_id, error=r.error)
|
|
@@ -209,26 +222,57 @@ def resolve_and_install(force: bool = False) -> InstallResult:
|
|
|
209
222
|
|
|
210
223
|
def _stream_install(drivers: list[dict], base: Path, cfg: dict) -> list[DriverResult]:
|
|
211
224
|
"""
|
|
212
|
-
|
|
213
|
-
|
|
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.
|
|
214
237
|
"""
|
|
215
238
|
repo_url = cfg["local_repo_url"]
|
|
216
239
|
parallel = cfg.get("parallel_downloads", 6)
|
|
217
240
|
|
|
218
|
-
by_level
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
232
276
|
|
|
233
277
|
|
|
234
278
|
def _dedup_and_group(drivers: list[dict]) -> dict[int, list[dict]]:
|
|
@@ -244,50 +288,78 @@ def _dedup_and_group(drivers: list[dict]) -> dict[int, list[dict]]:
|
|
|
244
288
|
return by_level
|
|
245
289
|
|
|
246
290
|
|
|
247
|
-
def
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
fut: Future,
|
|
267
|
-
future_info: dict[Future, tuple[int, dict]],
|
|
268
|
-
cfg: dict,
|
|
269
|
-
) -> DriverResult:
|
|
270
|
-
"""Handle one completed download Future: install if ok, return DriverResult."""
|
|
271
|
-
_, driver = future_info[fut]
|
|
272
|
-
bid = driver.get("binding_id", "")
|
|
273
|
-
cat = driver.get("category", "other")
|
|
274
|
-
|
|
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 = ""
|
|
275
310
|
try:
|
|
276
|
-
|
|
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
|
|
277
320
|
except Exception as e:
|
|
278
|
-
|
|
321
|
+
ok, err, reboot = False, str(e), False
|
|
322
|
+
dur = int((time.monotonic() - t0) * 1000)
|
|
279
323
|
|
|
280
|
-
|
|
281
|
-
|
|
324
|
+
for line in stdout.splitlines():
|
|
325
|
+
s = line.strip()
|
|
326
|
+
if s:
|
|
327
|
+
events.progress("install", f" {s}")
|
|
282
328
|
|
|
283
|
-
t0 = time.monotonic()
|
|
284
|
-
ok, err, reboot = _install_driver(driver, dest, cfg)
|
|
285
|
-
dur = int((time.monotonic() - t0) * 1000)
|
|
286
329
|
tag = " (reboot required)" if reboot else ""
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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)
|
|
291
363
|
return DriverResult(binding_id=bid, success=ok, error=err,
|
|
292
364
|
duration_ms=dur, reboot_required=reboot)
|
|
293
365
|
|
|
@@ -311,12 +383,6 @@ def _download_driver(driver: dict, base: Path, repo_url: str,
|
|
|
311
383
|
return dest, ok
|
|
312
384
|
|
|
313
385
|
|
|
314
|
-
def _level_label(level: int) -> str:
|
|
315
|
-
labels = {0: "chipset", 1: "firmware", 2: "storage",
|
|
316
|
-
3: "network", 5: "display", 6: "audio", 10: "other"}
|
|
317
|
-
return labels.get(level, f"level-{level}")
|
|
318
|
-
|
|
319
|
-
|
|
320
386
|
# ── Driver installation ─────────────────────────────────────────────────────────
|
|
321
387
|
|
|
322
388
|
def _classify(returncode: int, stdout: str = "", stderr: str = "") -> tuple[bool, str, bool]:
|
|
@@ -352,52 +418,6 @@ def _classify(returncode: int, stdout: str = "", stderr: str = "") -> tuple[bool
|
|
|
352
418
|
return False, (stderr.strip() or stdout.strip() or f"exit {returncode}")[:200], False
|
|
353
419
|
|
|
354
420
|
|
|
355
|
-
def _install_driver(driver: dict, driver_dir: Path, cfg: dict) -> tuple[bool, str, bool]:
|
|
356
|
-
install_type = driver.get("install_type", "inf_silent")
|
|
357
|
-
|
|
358
|
-
try:
|
|
359
|
-
if install_type == "inf_silent":
|
|
360
|
-
inf_files = list(driver_dir.glob("*.inf"))
|
|
361
|
-
if not inf_files:
|
|
362
|
-
return False, "No INF file found", False
|
|
363
|
-
r = proc.run(
|
|
364
|
-
["pnputil", "/add-driver", str(inf_files[0]), "/install", "/subdirs"],
|
|
365
|
-
capture_output=True, text=True,
|
|
366
|
-
timeout=cfg["timeout_long"],
|
|
367
|
-
)
|
|
368
|
-
return _classify(r.returncode, r.stdout, r.stderr)
|
|
369
|
-
|
|
370
|
-
if install_type == "exe_silent":
|
|
371
|
-
exe_files = list(driver_dir.glob("*.exe"))
|
|
372
|
-
if not exe_files:
|
|
373
|
-
return False, "No EXE file found", False
|
|
374
|
-
flags = driver.get("install_flags", "/S")
|
|
375
|
-
r = proc.run(
|
|
376
|
-
[str(exe_files[0])] + flags.split(),
|
|
377
|
-
capture_output=True,
|
|
378
|
-
timeout=cfg["timeout_install"],
|
|
379
|
-
)
|
|
380
|
-
return _classify(r.returncode)
|
|
381
|
-
|
|
382
|
-
if install_type == "firmware_flash":
|
|
383
|
-
inf_files = list(driver_dir.glob("*.inf"))
|
|
384
|
-
if not inf_files:
|
|
385
|
-
return False, "No firmware INF", False
|
|
386
|
-
r = proc.run(
|
|
387
|
-
["pnputil", "/add-driver", str(inf_files[0]), "/install"],
|
|
388
|
-
capture_output=True, text=True,
|
|
389
|
-
timeout=cfg["timeout_long"],
|
|
390
|
-
)
|
|
391
|
-
return _classify(r.returncode, r.stdout, r.stderr)
|
|
392
|
-
|
|
393
|
-
except subprocess.TimeoutExpired:
|
|
394
|
-
return False, "Install timed out", False
|
|
395
|
-
except Exception as e:
|
|
396
|
-
return False, str(e), False
|
|
397
|
-
|
|
398
|
-
return False, f"Unknown install_type: {install_type}", False
|
|
399
|
-
|
|
400
|
-
|
|
401
421
|
# ── Confirm ─────────────────────────────────────────────────────────────────────
|
|
402
422
|
|
|
403
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
|