driverclient 0.2.29__tar.gz → 0.2.30__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 (25) hide show
  1. {driverclient-0.2.29 → driverclient-0.2.30}/PKG-INFO +1 -1
  2. {driverclient-0.2.29 → driverclient-0.2.30}/pyproject.toml +1 -1
  3. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/__init__.py +1 -1
  4. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/ops/capture.py +20 -39
  5. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/ops/download.py +19 -1
  6. {driverclient-0.2.29 → driverclient-0.2.30}/.gitignore +0 -0
  7. {driverclient-0.2.29 → driverclient-0.2.30}/README.md +0 -0
  8. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/__main__.py +0 -0
  9. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/config.json +0 -0
  10. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/config.py +0 -0
  11. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/core/__init__.py +0 -0
  12. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/core/hardware.py +0 -0
  13. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/core/http.py +0 -0
  14. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/core/hwid.py +0 -0
  15. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/core/proc.py +0 -0
  16. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/events.py +0 -0
  17. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/main.py +0 -0
  18. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/ops/__init__.py +0 -0
  19. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/ops/automate.py +0 -0
  20. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/ops/diagnose.py +0 -0
  21. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/ops/install.py +0 -0
  22. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/ops/offline.py +0 -0
  23. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/ops/resolve.py +0 -0
  24. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/ops/scan.py +0 -0
  25. {driverclient-0.2.29 → driverclient-0.2.30}/src/driverclient/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: driverclient
3
- Version: 0.2.29
3
+ Version: 0.2.30
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.29"
7
+ version = "0.2.30"
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"
@@ -26,7 +26,7 @@ from driverclient.main import run as _run
26
26
 
27
27
  __all__ = ["ClientEvent", "DriverClient"]
28
28
 
29
- __version__ = "0.2.29"
29
+ __version__ = "0.2.30"
30
30
 
31
31
 
32
32
  class DriverClient:
@@ -97,6 +97,9 @@ def _machine_meta() -> dict:
97
97
  "machine_id": machine_id,
98
98
  }
99
99
 
100
+
101
+ _WINDOWS_INF = Path("C:\\Windows\\INF")
102
+
100
103
  _PNPUTIL_KEYS: dict[str, str] = {
101
104
  "published name": "published_name",
102
105
  "original name": "original_name",
@@ -346,13 +349,12 @@ def wu_update(force: bool = False) -> CaptureResult:
346
349
  **skip_reasons,
347
350
  )
348
351
 
349
- baseline = _snapshot_published_names(cfg)
350
352
  trigger = _trigger_wu()
351
353
  if trigger is None:
352
354
  return CaptureResult(still_missing=len(wu_hwids))
353
355
 
354
356
  wu_start = datetime.now(timezone.utc)
355
- total = _poll_and_submit_incremental(wu_start, cfg, trigger, baseline)
357
+ total = _poll_and_submit_incremental(wu_start, cfg, trigger)
356
358
  total.wu_triggered = True
357
359
  total.still_missing = max(0, len(wu_hwids) - total.submitted)
358
360
  total.reboot_required = _wu_reboot_pending()
@@ -381,13 +383,12 @@ def wu_full() -> CaptureResult:
381
383
  "windows_update", "[wu-full] Triggering full Windows Update scan (no HWID filtering)…"
382
384
  )
383
385
 
384
- baseline = _snapshot_published_names(cfg)
385
386
  trigger = _trigger_wu()
386
387
  if trigger is None:
387
388
  return CaptureResult()
388
389
 
389
390
  wu_start = datetime.now(timezone.utc)
390
- total = _poll_and_submit_incremental(wu_start, cfg, trigger, baseline)
391
+ total = _poll_and_submit_incremental(wu_start, cfg, trigger)
391
392
  total.wu_triggered = True
392
393
  _post_wu_session(cfg, wu_start, [], total.submitted)
393
394
  events.done(
@@ -403,22 +404,12 @@ def wu_full() -> CaptureResult:
403
404
 
404
405
  # ══ Missing-HWID classification ═══════════════════════════════════════════════
405
406
 
406
- _INBOX_INADEQUATE_CLASSES = {"display adapters"}
407
-
408
407
 
409
408
  def classify_missing(not_found: list[str], scan_result: ScanResult) -> dict[str, list[str]]:
410
409
  """Bucket repo-missing HWIDs by what the machine says about their device:
411
410
  inbox (Windows ships the driver — not applicable), oem_local (OEM driver
412
411
  installed locally — capturable), driverless (genuinely missing — WU
413
- candidate), unknown (HWID not in scan).
414
-
415
- Display adapters are excluded from the inbox bucket: Microsoft Basic
416
- Display Adapter is Windows' inbox fallback for a GPU whose real driver
417
- was never bound, and it leaves the device non-accelerated — unlike other
418
- inbox drivers (HID, mass storage, ...) it is not adequate coverage. Such
419
- a device is treated as driverless so it still reaches WU / gets captured
420
- once a real driver shows up, instead of being silently written off.
421
- """
412
+ candidate), unknown (HWID not in scan)."""
422
413
  hwid_to_device: dict[str, Device] = {}
423
414
  for scanned in scan_result.devices:
424
415
  for h in scanned.hwids:
@@ -436,10 +427,8 @@ def classify_missing(not_found: list[str], scan_result: ScanResult) -> dict[str,
436
427
  buckets["unknown"].append(hwid)
437
428
  elif not dev.has_driver:
438
429
  buckets["driverless"].append(hwid)
439
- elif dev.is_inbox and dev.class_name.lower() not in _INBOX_INADEQUATE_CLASSES:
440
- buckets["inbox"].append(hwid)
441
430
  elif dev.is_inbox:
442
- buckets["driverless"].append(hwid)
431
+ buckets["inbox"].append(hwid)
443
432
  else:
444
433
  buckets["oem_local"].append(hwid)
445
434
  return buckets
@@ -527,25 +516,17 @@ def _parse_enum_drivers(output: str) -> list[dict]:
527
516
  return packages
528
517
 
529
518
 
530
- def _snapshot_published_names(cfg: dict) -> set[str]:
531
- """Published names (oemNN.inf) already in the driver store — the baseline a
532
- WU run's new-package detection diffs against. Must be taken BEFORE
533
- triggering WU so nothing it installs is mistaken for pre-existing."""
534
- return {p["published_name"] for p in _enumerate_all_packages(cfg) if p.get("published_name")}
535
-
536
-
537
- def _enumerate_new_packages(baseline: set[str], cfg: dict) -> list[dict]:
538
- """Return packages present now that weren't in the pre-WU baseline.
539
-
540
- Published names are assigned once by Windows and never reused while a
541
- package stays in the store, so "not in baseline" is a robust new-package
542
- signal. The previous approach compared each INF's mtime under
543
- C:\\Windows\\INF against the WU start time — but that mtime can predate the
544
- run (e.g. Windows already staged the file) or otherwise not bump the way
545
- expected, silently dropping a genuinely new package (e.g. a discrete AMD
546
- display driver landing via WU alongside an already-captured Intel one)
547
- from ever being detected as new, so it was never submitted."""
548
- return [p for p in _enumerate_all_packages(cfg) if p.get("published_name") not in baseline]
519
+ def _enumerate_packages_since(cutoff: datetime, cfg: dict) -> list[dict]:
520
+ """Return packages whose C:\\Windows\\INF mtime >= cutoff."""
521
+ result = []
522
+ for pkg in _enumerate_all_packages(cfg):
523
+ inf_path = _WINDOWS_INF / pkg["published_name"]
524
+ if not inf_path.exists():
525
+ continue
526
+ mtime = datetime.fromtimestamp(inf_path.stat().st_mtime, tz=timezone.utc)
527
+ if mtime >= cutoff:
528
+ result.append(pkg)
529
+ return result
549
530
 
550
531
 
551
532
  def _published_names_for_hwids(
@@ -885,7 +866,7 @@ def _drain_wu_log(trigger: "_WuTrigger | None", pos: int) -> int:
885
866
 
886
867
 
887
868
  def _poll_and_submit_incremental(
888
- wu_start: datetime, cfg: dict, trigger: "_WuTrigger | None", baseline: set[str]
869
+ wu_start: datetime, cfg: dict, trigger: "_WuTrigger | None"
889
870
  ) -> CaptureResult:
890
871
  """
891
872
  Wait on Windows Update, submitting each new DriverStore package as it lands.
@@ -920,7 +901,7 @@ def _poll_and_submit_incremental(
920
901
 
921
902
  while datetime.now(timezone.utc) < deadline:
922
903
  log_pos = _drain_wu_log(trigger, log_pos) # surface WU's live phase
923
- all_new = _enumerate_new_packages(baseline, cfg)
904
+ all_new = _enumerate_packages_since(wu_start, cfg)
924
905
  new_batch = [p for p in all_new if p.get("published_name", "") not in seen]
925
906
 
926
907
  if new_batch:
@@ -12,6 +12,7 @@ Factored out here so the two modes share one download implementation:
12
12
  • download_drivers() — fan the above out across a driver list in parallel
13
13
  """
14
14
 
15
+ import shutil
15
16
  from concurrent.futures import ThreadPoolExecutor, as_completed
16
17
  from pathlib import Path
17
18
 
@@ -20,7 +21,21 @@ from driverclient.core.http import http_download_retry
20
21
 
21
22
  def download_driver(driver: dict, base: Path, repo_url: str, cfg: dict) -> tuple[Path, bool]:
22
23
  """Download all files for one driver into base/<binding_id>/.
23
- Returns (dest_dir, success). success is False if ANY file failed."""
24
+ Returns (dest_dir, success). success is False if ANY file failed.
25
+
26
+ On failure, dest is removed before returning. The live install path stages
27
+ every driver under one shared `base` and installs it with a single batched
28
+ `pnputil /add-driver base\\*.inf /subdirs` — that command globs the
29
+ filesystem directly, so a driver this function reports as failed (and the
30
+ caller correctly excludes from the install list) would otherwise still
31
+ leave its already-downloaded files (commonly just the small .inf, if a
32
+ large companion .sys/.dll timed out) sitting under base/<binding_id>/ for
33
+ pnputil to find anyway. pnputil then tries to add that incomplete package —
34
+ "cannot find the file specified" for its missing payload — which large
35
+ graphics drivers hit most often (biggest files, likeliest to time out
36
+ partway), and can poison the whole batched pass for the OTHER, fully
37
+ downloaded drivers riding in the same pnputil call.
38
+ """
24
39
  bid = driver.get("binding_id") or driver.get("package_id", "")
25
40
  pkg_id = driver["package_id"]
26
41
  dest = base / bid
@@ -36,6 +51,9 @@ def download_driver(driver: dict, base: Path, repo_url: str, cfg: dict) -> tuple
36
51
  if not http_download_retry(url, path, f["sha256"], timeout=cfg["timeout_long"]):
37
52
  ok = False
38
53
 
54
+ if not ok:
55
+ shutil.rmtree(dest, ignore_errors=True)
56
+
39
57
  return dest, ok
40
58
 
41
59
 
File without changes
File without changes