driverclient 0.2.1__tar.gz → 0.2.2__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 (20) hide show
  1. {driverclient-0.2.1 → driverclient-0.2.2}/PKG-INFO +1 -1
  2. {driverclient-0.2.1 → driverclient-0.2.2}/pyproject.toml +1 -1
  3. {driverclient-0.2.1 → driverclient-0.2.2}/src/driverclient/__init__.py +1 -1
  4. {driverclient-0.2.1 → driverclient-0.2.2}/src/driverclient/ops/capture.py +27 -5
  5. {driverclient-0.2.1 → driverclient-0.2.2}/src/driverclient/ops/install.py +7 -1
  6. {driverclient-0.2.1 → driverclient-0.2.2}/src/driverclient/ops/scan.py +62 -0
  7. {driverclient-0.2.1 → driverclient-0.2.2}/.gitignore +0 -0
  8. {driverclient-0.2.1 → driverclient-0.2.2}/README.md +0 -0
  9. {driverclient-0.2.1 → driverclient-0.2.2}/src/driverclient/__main__.py +0 -0
  10. {driverclient-0.2.1 → driverclient-0.2.2}/src/driverclient/config.json +0 -0
  11. {driverclient-0.2.1 → driverclient-0.2.2}/src/driverclient/config.py +0 -0
  12. {driverclient-0.2.1 → driverclient-0.2.2}/src/driverclient/core/__init__.py +0 -0
  13. {driverclient-0.2.1 → driverclient-0.2.2}/src/driverclient/core/hardware.py +0 -0
  14. {driverclient-0.2.1 → driverclient-0.2.2}/src/driverclient/core/http.py +0 -0
  15. {driverclient-0.2.1 → driverclient-0.2.2}/src/driverclient/events.py +0 -0
  16. {driverclient-0.2.1 → driverclient-0.2.2}/src/driverclient/main.py +0 -0
  17. {driverclient-0.2.1 → driverclient-0.2.2}/src/driverclient/ops/__init__.py +0 -0
  18. {driverclient-0.2.1 → driverclient-0.2.2}/src/driverclient/ops/automate.py +0 -0
  19. {driverclient-0.2.1 → driverclient-0.2.2}/src/driverclient/ops/resolve.py +0 -0
  20. {driverclient-0.2.1 → driverclient-0.2.2}/src/driverclient/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: driverclient
3
- Version: 0.2.1
3
+ Version: 0.2.2
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.1"
7
+ version = "0.2.2"
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.1"
28
+ __version__ = "0.2.2"
29
29
 
30
30
 
31
31
  class DriverClient:
@@ -345,15 +345,36 @@ def _enumerate_packages_since(cutoff: datetime, cfg: dict) -> list[dict]:
345
345
  return result
346
346
 
347
347
 
348
+ def _norm_hwid(hwid: str) -> str:
349
+ """Normalize an HWID for comparison: uppercase and strip the &REV_* tail.
350
+
351
+ The repo's `not_found` list carries the stripped HWID variants
352
+ (e.g. PCI\\VEN_8086&DEV_9D15&SUBSYS_222C17AA) while the IDs reported by the
353
+ live machine — both `installed_versions` keys and pnputil /enum-devices —
354
+ carry a &REV_xx suffix. Comparing the two forms verbatim never matches, so
355
+ both sides are normalized to the same REV-less, uppercase form.
356
+ """
357
+ up = hwid.upper()
358
+ idx = up.find("&REV_")
359
+ return up[:idx] if idx != -1 else up
360
+
361
+
348
362
  def _find_installed_packages_for_hwids(
349
363
  not_found_hwids: set[str],
350
364
  scan_result: ScanResult,
351
365
  cfg: dict,
352
366
  ) -> list[dict]:
353
- locally_installed = {
354
- hwid for hwid in not_found_hwids
355
- if hwid in scan_result.installed_versions
356
- }
367
+ # A missing HWID is capturable when it belongs to a scanned device that
368
+ # already has a driver bound. Match on the device's own HWID list (which
369
+ # includes every variant) using normalized HWIDs, rather than exact
370
+ # membership in installed_versions whose keys are the full &REV_xx form.
371
+ not_found_norm = {_norm_hwid(h) for h in not_found_hwids}
372
+ locally_installed: set[str] = set()
373
+ for dev in scan_result.devices:
374
+ if not dev.has_driver:
375
+ continue
376
+ if any(_norm_hwid(h) in not_found_norm for h in dev.hwids):
377
+ locally_installed.update(h.upper() for h in dev.hwids)
357
378
  if not locally_installed:
358
379
  return []
359
380
 
@@ -395,8 +416,9 @@ def _map_hwids_to_published(hwids: set[str], cfg: dict) -> dict[str, str]:
395
416
  def _commit_hwid_block(state: dict, hwids: set[str], mapping: dict) -> None:
396
417
  """Flush accumulated HWIDs for the current device into mapping."""
397
418
  if state["published"]:
419
+ targets = {_norm_hwid(h) for h in hwids}
398
420
  for h in state["hwids"]:
399
- if h in hwids:
421
+ if _norm_hwid(h) in targets:
400
422
  mapping[h] = state["published"]
401
423
  state["published"] = ""
402
424
  state["hwids"] = []
@@ -38,6 +38,11 @@ _CATEGORY_ORDER: dict[str, int] = {
38
38
  # 1641 = ERROR_SUCCESS_REBOOT_INITIATED
39
39
  _REBOOT_CODES: frozenset[int] = frozenset({3010, 1641})
40
40
 
41
+ # Installer exit codes that mean "succeeded, nothing to do" (NOT a failure).
42
+ # 259 = ERROR_NO_MORE_ITEMS — pnputil added the package but no device needed
43
+ # updating (driver already present / "Already exists in the system").
44
+ _SUCCESS_NO_CHANGE_CODES: frozenset[int] = frozenset({259})
45
+
41
46
 
42
47
  @dataclass
43
48
  class DriverResult:
@@ -230,10 +235,11 @@ def _classify(returncode: int, err_text: str = "") -> tuple[bool, str, bool]:
230
235
  Map an installer exit code to (success, error, reboot_required).
231
236
 
232
237
  0 -> success, no reboot
238
+ 259 -> success, no reboot (package already present / no change)
233
239
  3010 / 1641 -> success, reboot required (NOT a failure)
234
240
  anything else -> failure, carry err_text
235
241
  """
236
- if returncode == 0:
242
+ if returncode == 0 or returncode in _SUCCESS_NO_CHANGE_CODES:
237
243
  return True, "", False
238
244
  if returncode in _REBOOT_CODES:
239
245
  return True, "", True
@@ -9,6 +9,7 @@ Public API:
9
9
  scan(force=False) -> ScanResult
10
10
  """
11
11
  import json
12
+ import re
12
13
  import subprocess
13
14
  from concurrent.futures import ThreadPoolExecutor
14
15
  from dataclasses import asdict, dataclass, field
@@ -62,6 +63,54 @@ class ScanResult:
62
63
  return [d for d in self.devices if d.has_driver]
63
64
 
64
65
 
66
+ _ACPI_VENDEV = re.compile(r"^ACPI\\VEN_([^&]+)&DEV_(.+)$")
67
+
68
+
69
+ def _canon_hwid(hwid: str) -> str:
70
+ """Fold the two HWID forms Windows reports for one device into one key.
71
+
72
+ `installed_versions` comes from Win32_PnPSignedDriver.HardWareID — the
73
+ canonical VEN_/DEV_ form with a &REV_xx tail, and ACPI IDs written as
74
+ ACPI\\VEN_INT&DEV_3394. The HWIDs sent to /resolve come from pnputil
75
+ /enum-devices — raw form, no &REV, ACPI as ACPI\\INT3394. Both collapse to
76
+ one comparable key: uppercase, &REV_* dropped, ACPI VEN_/DEV_ concatenated.
77
+ """
78
+ up = hwid.upper()
79
+ idx = up.find("&REV_")
80
+ if idx != -1:
81
+ up = up[:idx]
82
+ m = _ACPI_VENDEV.match(up)
83
+ if m:
84
+ up = f"ACPI\\{m.group(1)}{m.group(2)}"
85
+ return up
86
+
87
+
88
+ def _installed_prefix_index(installed_versions: dict[str, str]) -> dict[str, str]:
89
+ """Expand each canonical installed HWID into its &-boundary prefixes.
90
+
91
+ The installed key is the most-specific device ID
92
+ (PCI\\VEN_8086&DEV_9D15&SUBSYS_222C17AA&REV_F1) while /resolve frequently
93
+ matches on a shorter variant (PCI\\VEN_8086&DEV_9D15). Emitting every
94
+ prefix that still contains &DEV_ lets the shorter form find the version.
95
+ The bare bus/vendor root (PCI\\VEN_8086) is skipped so an over-generic ID
96
+ can't be mislabelled as already-current.
97
+ """
98
+ idx: dict[str, str] = {}
99
+ for key, ver in installed_versions.items():
100
+ canon = _canon_hwid(key)
101
+ parts = canon.split("&")
102
+ acc = parts[0]
103
+ prefixes = [acc]
104
+ for p in parts[1:]:
105
+ acc = f"{acc}&{p}"
106
+ prefixes.append(acc)
107
+ for pref in prefixes:
108
+ if "&" in canon and "&DEV_" not in pref:
109
+ continue # skip bare bus/vendor root
110
+ idx.setdefault(pref, ver)
111
+ return idx
112
+
113
+
65
114
  def scan(force: bool = False) -> ScanResult:
66
115
  """
67
116
  Run a full hardware scan. Loads from ds_scan.json cache if fresh
@@ -91,6 +140,19 @@ def scan(force: bool = False) -> ScanResult:
91
140
  total=len(all_hwids))
92
141
  installed_versions = get_installed_versions(all_hwids)
93
142
 
143
+ # Re-key installed_versions onto the raw HWID strings the client sends to
144
+ # /resolve. Win32_PnPSignedDriver reports the canonical VEN_/DEV_+REV form,
145
+ # which never matches the raw pnputil HWIDs verbatim — so without this the
146
+ # server's already-current check (and Device.installed_version below) miss
147
+ # every time and every repo-covered driver shows up as "to install".
148
+ if installed_versions:
149
+ prefix_idx = _installed_prefix_index(installed_versions)
150
+ for h in all_hwids:
151
+ if h not in installed_versions:
152
+ ver = prefix_idx.get(_canon_hwid(h))
153
+ if ver:
154
+ installed_versions[h] = ver
155
+
94
156
  for dev in devices:
95
157
  ver = next(
96
158
  (installed_versions[h] for h in dev.hwids if h in installed_versions),
File without changes
File without changes