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.
Files changed (23) hide show
  1. {driverclient-0.2.19 → driverclient-0.2.20}/PKG-INFO +1 -1
  2. {driverclient-0.2.19 → driverclient-0.2.20}/pyproject.toml +1 -1
  3. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/__init__.py +1 -1
  4. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/ops/install.py +124 -149
  5. {driverclient-0.2.19 → driverclient-0.2.20}/.gitignore +0 -0
  6. {driverclient-0.2.19 → driverclient-0.2.20}/README.md +0 -0
  7. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/__main__.py +0 -0
  8. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/config.json +0 -0
  9. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/config.py +0 -0
  10. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/core/__init__.py +0 -0
  11. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/core/hardware.py +0 -0
  12. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/core/http.py +0 -0
  13. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/core/hwid.py +0 -0
  14. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/core/proc.py +0 -0
  15. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/events.py +0 -0
  16. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/main.py +0 -0
  17. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/ops/__init__.py +0 -0
  18. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/ops/automate.py +0 -0
  19. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/ops/capture.py +0 -0
  20. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/ops/diagnose.py +0 -0
  21. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/ops/resolve.py +0 -0
  22. {driverclient-0.2.19 → driverclient-0.2.20}/src/driverclient/ops/scan.py +0 -0
  23. {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.19
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.19"
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"
@@ -25,7 +25,7 @@ from driverclient.main import run as _run
25
25
 
26
26
  __all__ = ["DriverClient", "ClientEvent"]
27
27
 
28
- __version__ = "0.2.19"
28
+ __version__ = "0.2.20"
29
29
 
30
30
 
31
31
  class DriverClient:
@@ -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 Future, ThreadPoolExecutor, as_completed
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
- # Live driver-store index: INF name -> highest installed version. Built once,
159
- # then UPDATED as each driver installs this pass (see _record_installed) so a
160
- # later package of the same driver family (same INF) at an equal-or-older
161
- # version is skipped the driver store already has it. This is skip-if-present
162
- # (idempotency), NOT deduplication: no repo package is removed; a newer repo
163
- # version still installs over an older store version (store_ver >= repo_ver skips).
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, idx, skip_installed)
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
- Submit ALL downloads (all levels) simultaneously, then install level-by-level.
226
- Each driver installs the moment its own download Future completes.
227
-
228
- `idx` is the live driver-store index (INF -> installed version); it is read to
229
- skip a driver the store already has and updated after each successful install
230
- so same-family duplicates later in the pass are skipped too. Installs run one
231
- at a time here (main thread), so mutating `idx` needs no lock.
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 = _dedup_and_group(drivers)
237
- executor = ThreadPoolExecutor(max_workers=parallel)
238
- future_info, level_futs = _submit_all_downloads(by_level, base, repo_url, cfg, executor)
239
-
240
- all_results: list[DriverResult] = []
241
- for level in sorted(by_level):
242
- events.progress("install",
243
- f"[install] {_level_label(level)} ({len(level_futs[level])} driver(s))…",
244
- category=_level_label(level), count=len(level_futs[level]))
245
- for fut in as_completed(level_futs[level]):
246
- all_results.append(_process_completed(fut, future_info, cfg, idx, skip_installed))
247
-
248
- executor.shutdown(wait=False)
249
- return all_results
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 _submit_all_downloads(
266
- by_level: dict[int, list[dict]],
267
- base: Path,
268
- repo_url: str,
269
- cfg: dict,
270
- executor: ThreadPoolExecutor,
271
- ) -> tuple[dict[Future, tuple[int, dict]], dict[int, list[Future]]]:
272
- """Submit every driver download to the pool at once. Returns lookup maps."""
273
- future_info: dict[Future, tuple[int, dict]] = {}
274
- level_futs: dict[int, list[Future]] = defaultdict(list)
275
- for level in sorted(by_level):
276
- for driver in by_level[level]:
277
- fut = executor.submit(_download_driver, driver, base, repo_url, cfg)
278
- future_info[fut] = (level, driver)
279
- level_futs[level].append(fut)
280
- return future_info, level_futs
281
-
282
-
283
- def _process_completed(
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
- dest, dl_ok = fut.result()
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
- return DriverResult(binding_id=bid, success=False, error=str(e))
321
+ ok, err, reboot = False, str(e), False
322
+ dur = int((time.monotonic() - t0) * 1000)
308
323
 
309
- if not dl_ok:
310
- return DriverResult(binding_id=bid, success=False, error="download_failed")
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
- line = f" {'✓' if ok else '✗'} [{cat}] {bid} ({dur}ms){tag}"
319
- emit = events.ok if ok else events.error
320
- emit("install", line, binding_id=bid, category=cat, duration_ms=dur,
321
- error=err, reboot_required=reboot)
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