driverclient 0.2.2__tar.gz → 0.2.3__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.2 → driverclient-0.2.3}/PKG-INFO +1 -1
- {driverclient-0.2.2 → driverclient-0.2.3}/pyproject.toml +1 -1
- {driverclient-0.2.2 → driverclient-0.2.3}/src/driverclient/__init__.py +1 -1
- {driverclient-0.2.2 → driverclient-0.2.3}/src/driverclient/ops/automate.py +23 -6
- {driverclient-0.2.2 → driverclient-0.2.3}/src/driverclient/ops/capture.py +84 -47
- {driverclient-0.2.2 → driverclient-0.2.3}/src/driverclient/ops/resolve.py +6 -2
- {driverclient-0.2.2 → driverclient-0.2.3}/src/driverclient/ops/scan.py +33 -4
- {driverclient-0.2.2 → driverclient-0.2.3}/.gitignore +0 -0
- {driverclient-0.2.2 → driverclient-0.2.3}/README.md +0 -0
- {driverclient-0.2.2 → driverclient-0.2.3}/src/driverclient/__main__.py +0 -0
- {driverclient-0.2.2 → driverclient-0.2.3}/src/driverclient/config.json +0 -0
- {driverclient-0.2.2 → driverclient-0.2.3}/src/driverclient/config.py +0 -0
- {driverclient-0.2.2 → driverclient-0.2.3}/src/driverclient/core/__init__.py +0 -0
- {driverclient-0.2.2 → driverclient-0.2.3}/src/driverclient/core/hardware.py +0 -0
- {driverclient-0.2.2 → driverclient-0.2.3}/src/driverclient/core/http.py +0 -0
- {driverclient-0.2.2 → driverclient-0.2.3}/src/driverclient/events.py +0 -0
- {driverclient-0.2.2 → driverclient-0.2.3}/src/driverclient/main.py +0 -0
- {driverclient-0.2.2 → driverclient-0.2.3}/src/driverclient/ops/__init__.py +0 -0
- {driverclient-0.2.2 → driverclient-0.2.3}/src/driverclient/ops/install.py +0 -0
- {driverclient-0.2.2 → driverclient-0.2.3}/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.3
|
|
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>
|
|
@@ -17,7 +17,7 @@ from datetime import datetime, timezone
|
|
|
17
17
|
|
|
18
18
|
from driverclient import events
|
|
19
19
|
from driverclient.config import get
|
|
20
|
-
from driverclient.ops.capture import CaptureResult, wu_update
|
|
20
|
+
from driverclient.ops.capture import CaptureResult, classify_missing, wu_update
|
|
21
21
|
from driverclient.ops.install import InstallResult, resolve_and_install
|
|
22
22
|
from driverclient.ops.resolve import ResolveResult, resolve
|
|
23
23
|
from driverclient.ops.scan import ScanResult, scan
|
|
@@ -116,22 +116,39 @@ def _summary(r: AutomateResult) -> None:
|
|
|
116
116
|
inst = r.install or InstallResult()
|
|
117
117
|
wu = r.wu or CaptureResult()
|
|
118
118
|
|
|
119
|
+
# Bucket the repo-missing HWIDs so "not found" reads truthfully: inbox
|
|
120
|
+
# devices are covered by Windows itself, not missing from the repo.
|
|
121
|
+
buckets = None
|
|
122
|
+
if r.scan and r.resolve and r.resolve.not_found:
|
|
123
|
+
buckets = classify_missing(r.resolve.not_found, r.scan)
|
|
124
|
+
|
|
119
125
|
events.done("done", "")
|
|
120
126
|
events.done("done", "=" * 60)
|
|
121
127
|
events.done("done", " Summary")
|
|
122
128
|
events.done("done", f" Duration : {r.duration_s}s", duration_s=r.duration_s)
|
|
129
|
+
if r.resolve:
|
|
130
|
+
events.done("done",
|
|
131
|
+
f" Current : {r.resolve.already_current} already current "
|
|
132
|
+
f"(of {r.resolve.total_hwids} HWIDs)",
|
|
133
|
+
already_current=r.resolve.already_current,
|
|
134
|
+
total_hwids=r.resolve.total_hwids)
|
|
123
135
|
events.done("done", f" Installed: {inst.ok_count} ok {inst.fail_count} failed",
|
|
124
136
|
installed_ok=inst.ok_count, installed_failed=inst.fail_count)
|
|
125
137
|
if inst.reboot_required:
|
|
126
138
|
events.done("done", " Reboot : REQUIRED (a driver needs a reboot to take effect)",
|
|
127
139
|
reboot_required=True)
|
|
140
|
+
if buckets:
|
|
141
|
+
inbox = len(buckets["inbox"])
|
|
142
|
+
genuine = len(buckets["driverless"]) + len(buckets["unknown"])
|
|
143
|
+
if inbox:
|
|
144
|
+
events.done("done",
|
|
145
|
+
f" Inbox : {inbox} HWID(s) — Windows inbox driver, not applicable",
|
|
146
|
+
inbox=inbox)
|
|
147
|
+
events.done("done",
|
|
148
|
+
f" Missing : {genuine} genuinely missing (WU candidates)",
|
|
149
|
+
genuinely_missing=genuine)
|
|
128
150
|
if r.wu and r.wu.wu_triggered:
|
|
129
151
|
events.done("done",
|
|
130
152
|
f" WU : {wu.submitted} captured {wu.still_missing} still missing",
|
|
131
153
|
wu_submitted=wu.submitted, wu_still_missing=wu.still_missing)
|
|
132
|
-
else:
|
|
133
|
-
missing = len(r.resolve.not_found) if r.resolve else 0
|
|
134
|
-
if missing:
|
|
135
|
-
events.done("done", f" Missing : {missing} HWID(s) not in repo (WU not run)",
|
|
136
|
-
missing=missing)
|
|
137
154
|
events.done("done", "=" * 60)
|
|
@@ -40,7 +40,7 @@ from driverclient.config import get
|
|
|
40
40
|
from driverclient.core.hardware import compute_fingerprint, get_model_profile
|
|
41
41
|
from driverclient.core.http import BadRequestError, http_post_retry, http_upload_package, pack_driver_archive
|
|
42
42
|
from driverclient.ops.resolve import ResolveResult, resolve
|
|
43
|
-
from driverclient.ops.scan import Device, ScanResult, scan
|
|
43
|
+
from driverclient.ops.scan import Device, ScanResult, _looks_like_hwid, scan
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def _post_wu_session(cfg: dict, wu_start, hwids_tried: list, captured: int) -> None:
|
|
@@ -153,26 +153,35 @@ def capture_missing(force: bool = False) -> CaptureResult:
|
|
|
153
153
|
total=0)
|
|
154
154
|
return CaptureResult()
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
buckets = classify_missing(res_result.not_found, scan_result)
|
|
157
|
+
genuinely_missing = len(buckets["driverless"]) + len(buckets["unknown"])
|
|
158
|
+
events.progress(
|
|
159
|
+
"capture",
|
|
160
|
+
f"[capture-missing] {len(res_result.not_found)} HWID(s) not in repo — "
|
|
161
|
+
f"{len(buckets['inbox'])} inbox (not applicable) "
|
|
162
|
+
f"{len(buckets['oem_local'])} with local OEM driver "
|
|
163
|
+
f"{genuinely_missing} genuinely missing",
|
|
164
|
+
total=len(res_result.not_found), inbox=len(buckets["inbox"]),
|
|
165
|
+
oem_local=len(buckets["oem_local"]), genuinely_missing=genuinely_missing)
|
|
166
|
+
|
|
167
|
+
to_export = _find_installed_packages_for_hwids(set(buckets["oem_local"]), scan_result, cfg)
|
|
162
168
|
if not to_export:
|
|
163
169
|
events.done("capture",
|
|
164
|
-
"[capture-missing]
|
|
165
|
-
|
|
166
|
-
|
|
170
|
+
f"[capture-missing] Nothing capturable — {len(buckets['inbox'])} inbox, "
|
|
171
|
+
f"{genuinely_missing} genuinely missing (WU candidates)",
|
|
172
|
+
total=0, still_missing=genuinely_missing)
|
|
173
|
+
return CaptureResult(still_missing=genuinely_missing)
|
|
167
174
|
|
|
168
175
|
events.progress("capture",
|
|
169
176
|
f"[capture-missing] Exporting {len(to_export)} locally-installed package(s)…",
|
|
170
177
|
total=len(to_export))
|
|
171
178
|
result = _export_and_submit(to_export, cfg)
|
|
172
|
-
result.still_missing =
|
|
179
|
+
result.still_missing = genuinely_missing
|
|
173
180
|
events.done("capture",
|
|
174
181
|
f"[capture-missing] Done — {result.submitted} submitted "
|
|
175
|
-
f"{result.skipped} skipped {result.failed} failed"
|
|
182
|
+
f"{result.skipped} skipped {result.failed} failed "
|
|
183
|
+
f"({len(buckets['inbox'])} inbox not applicable, "
|
|
184
|
+
f"{genuinely_missing} genuinely missing)",
|
|
176
185
|
total=len(to_export), submitted=result.submitted,
|
|
177
186
|
skipped=result.skipped, failed=result.failed,
|
|
178
187
|
still_missing=result.still_missing)
|
|
@@ -198,24 +207,21 @@ def wu_update(force: bool = False) -> CaptureResult:
|
|
|
198
207
|
total=0)
|
|
199
208
|
return CaptureResult()
|
|
200
209
|
|
|
201
|
-
wu_hwids = _filter_wu_hwids(res_result.not_found, scan_result, cfg)
|
|
202
|
-
|
|
210
|
+
wu_hwids, skip_reasons = _filter_wu_hwids(res_result.not_found, scan_result, cfg)
|
|
211
|
+
skip_detail = (f"{skip_reasons['inbox']} inbox (not applicable) "
|
|
212
|
+
f"{skip_reasons['local_driver']} covered by local driver "
|
|
213
|
+
f"{skip_reasons['ignored_class']} ignored class")
|
|
203
214
|
|
|
204
215
|
if not wu_hwids:
|
|
205
216
|
events.done("windows_update",
|
|
206
|
-
"[wu-update]
|
|
207
|
-
total=0, still_missing=
|
|
208
|
-
return CaptureResult(still_missing=
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
total=len(wu_hwids), skipped=skipped)
|
|
215
|
-
else:
|
|
216
|
-
events.start("windows_update",
|
|
217
|
-
f"[wu-update] {len(wu_hwids)} HWID(s) not in repo — triggering Windows Update…",
|
|
218
|
-
total=len(wu_hwids))
|
|
217
|
+
f"[wu-update] WU skipped — 0 genuinely missing ({skip_detail})",
|
|
218
|
+
total=0, still_missing=0, **skip_reasons)
|
|
219
|
+
return CaptureResult(still_missing=0)
|
|
220
|
+
|
|
221
|
+
events.start("windows_update",
|
|
222
|
+
f"[wu-update] {len(wu_hwids)} genuinely missing HWID(s) — "
|
|
223
|
+
f"triggering Windows Update… ({skip_detail})",
|
|
224
|
+
total=len(wu_hwids), **skip_reasons)
|
|
219
225
|
|
|
220
226
|
if not _trigger_wu():
|
|
221
227
|
return CaptureResult(still_missing=len(wu_hwids))
|
|
@@ -257,41 +263,72 @@ def wu_full() -> CaptureResult:
|
|
|
257
263
|
return total
|
|
258
264
|
|
|
259
265
|
|
|
266
|
+
# ══ Missing-HWID classification ═══════════════════════════════════════════════
|
|
267
|
+
|
|
268
|
+
def classify_missing(not_found: list[str], scan_result: ScanResult) -> dict[str, list[str]]:
|
|
269
|
+
"""Bucket repo-missing HWIDs by what the machine says about their device:
|
|
270
|
+
inbox (Windows ships the driver — not applicable), oem_local (OEM driver
|
|
271
|
+
installed locally — capturable), driverless (genuinely missing — WU
|
|
272
|
+
candidate), unknown (HWID not in scan)."""
|
|
273
|
+
hwid_to_device: dict[str, Device] = {}
|
|
274
|
+
for dev in scan_result.devices:
|
|
275
|
+
for h in dev.hwids:
|
|
276
|
+
hwid_to_device[_norm_hwid(h)] = dev
|
|
277
|
+
|
|
278
|
+
buckets: dict[str, list[str]] = {
|
|
279
|
+
"inbox": [], "oem_local": [], "driverless": [], "unknown": [],
|
|
280
|
+
}
|
|
281
|
+
for hwid in not_found:
|
|
282
|
+
dev = hwid_to_device.get(_norm_hwid(hwid))
|
|
283
|
+
if dev is None:
|
|
284
|
+
buckets["unknown"].append(hwid)
|
|
285
|
+
elif not dev.has_driver:
|
|
286
|
+
buckets["driverless"].append(hwid)
|
|
287
|
+
elif dev.is_inbox:
|
|
288
|
+
buckets["inbox"].append(hwid)
|
|
289
|
+
else:
|
|
290
|
+
buckets["oem_local"].append(hwid)
|
|
291
|
+
return buckets
|
|
292
|
+
|
|
293
|
+
|
|
260
294
|
# ══ WU HWID filtering ═════════════════════════════════════════════════════════
|
|
261
295
|
|
|
262
296
|
def _filter_wu_hwids(
|
|
263
297
|
not_found: list[str],
|
|
264
298
|
scan_result: ScanResult,
|
|
265
299
|
cfg: dict,
|
|
266
|
-
) -> list[str]:
|
|
300
|
+
) -> tuple[list[str], dict[str, int]]:
|
|
267
301
|
"""
|
|
268
|
-
Return
|
|
269
|
-
|
|
270
|
-
-
|
|
271
|
-
-
|
|
302
|
+
Return (wu_hwids, skipped_reasons) — only genuinely driverless, non-inbox,
|
|
303
|
+
non-ignored devices go to Windows Update:
|
|
304
|
+
- inbox driver bound → skip (Windows covers it — not applicable)
|
|
305
|
+
- local OEM driver bound → skip (capture_missing handles repo upload)
|
|
306
|
+
- class in wu_ignore_classes → skip
|
|
307
|
+
- not in scan at all → include (unknown device — safer to attempt)
|
|
272
308
|
"""
|
|
273
309
|
ignore_classes: set[str] = {
|
|
274
310
|
c.lower() for c in cfg.get("wu_ignore_classes", [])
|
|
275
311
|
}
|
|
312
|
+
buckets = classify_missing(not_found, scan_result)
|
|
276
313
|
|
|
314
|
+
result: list[str] = list(buckets["unknown"])
|
|
315
|
+
skipped = {
|
|
316
|
+
"inbox": len(buckets["inbox"]),
|
|
317
|
+
"local_driver": len(buckets["oem_local"]),
|
|
318
|
+
"ignored_class": 0,
|
|
319
|
+
}
|
|
277
320
|
hwid_to_device: dict[str, Device] = {}
|
|
278
321
|
for dev in scan_result.devices:
|
|
279
|
-
for
|
|
280
|
-
hwid_to_device[
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
322
|
+
for h in dev.hwids:
|
|
323
|
+
hwid_to_device[_norm_hwid(h)] = dev
|
|
324
|
+
for hwid in buckets["driverless"]:
|
|
325
|
+
dev = hwid_to_device.get(_norm_hwid(hwid))
|
|
326
|
+
if dev and ignore_classes and dev.class_name.lower() in ignore_classes:
|
|
327
|
+
skipped["ignored_class"] += 1
|
|
328
|
+
else:
|
|
286
329
|
result.append(hwid)
|
|
287
|
-
continue
|
|
288
|
-
if dev.has_driver:
|
|
289
|
-
continue
|
|
290
|
-
if ignore_classes and dev.class_name.lower() in ignore_classes:
|
|
291
|
-
continue
|
|
292
|
-
result.append(hwid)
|
|
293
330
|
|
|
294
|
-
return result
|
|
331
|
+
return result, skipped
|
|
295
332
|
|
|
296
333
|
|
|
297
334
|
# ══ Package enumeration ════════════════════════════════════════════════════════
|
|
@@ -433,7 +470,7 @@ def _parse_hwid_line(line: str, stripped: str, state: dict) -> None:
|
|
|
433
470
|
state["in_hwids"] = False
|
|
434
471
|
elif low.startswith(("hardware ids", "compatible ids")):
|
|
435
472
|
state["in_hwids"] = True
|
|
436
|
-
elif state["in_hwids"] and
|
|
473
|
+
elif state["in_hwids"] and _looks_like_hwid(stripped):
|
|
437
474
|
state["hwids"].append(stripped.upper())
|
|
438
475
|
elif ":" in stripped and not line.startswith(" "):
|
|
439
476
|
state["in_hwids"] = False
|
|
@@ -30,6 +30,8 @@ class ResolveResult:
|
|
|
30
30
|
trace_id: str = ""
|
|
31
31
|
machine_id: str = ""
|
|
32
32
|
timestamp: str = ""
|
|
33
|
+
total_hwids: int = 0
|
|
34
|
+
already_current: int = 0
|
|
33
35
|
|
|
34
36
|
@property
|
|
35
37
|
def has_drivers(self) -> bool:
|
|
@@ -89,10 +91,12 @@ def _query_repo(scan_result: ScanResult, cfg: dict, force: bool) -> ResolveResul
|
|
|
89
91
|
trace_id=resp.get("trace_id", ""),
|
|
90
92
|
machine_id=scan_result.machine_id,
|
|
91
93
|
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
94
|
+
total_hwids=resp.get("total_hwids", len(scan_result.all_hwids)),
|
|
95
|
+
already_current=resp.get("already_current", 0),
|
|
92
96
|
)
|
|
93
97
|
|
|
94
|
-
total_hwids =
|
|
95
|
-
already_current =
|
|
98
|
+
total_hwids = result.total_hwids
|
|
99
|
+
already_current = result.already_current
|
|
96
100
|
|
|
97
101
|
events.done(
|
|
98
102
|
"resolve",
|
|
@@ -33,6 +33,13 @@ class Device:
|
|
|
33
33
|
has_driver: bool
|
|
34
34
|
installed_version: str = ""
|
|
35
35
|
class_name: str = "" # e.g. "System", "Net", "Display adapters"
|
|
36
|
+
driver_name: str = "" # published INF name, e.g. "oem42.inf" or "machine.inf"
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def is_inbox(self) -> bool:
|
|
40
|
+
"""Driver ships with Windows (published name is not oem*.inf) — the repo
|
|
41
|
+
never needs to cover it, so it's 'not applicable' rather than 'missing'."""
|
|
42
|
+
return self.has_driver and not self.driver_name.lower().startswith("oem")
|
|
36
43
|
|
|
37
44
|
|
|
38
45
|
@dataclass
|
|
@@ -66,6 +73,17 @@ class ScanResult:
|
|
|
66
73
|
_ACPI_VENDEV = re.compile(r"^ACPI\\VEN_([^&]+)&DEV_(.+)$")
|
|
67
74
|
|
|
68
75
|
|
|
76
|
+
def _looks_like_hwid(stripped: str) -> bool:
|
|
77
|
+
"""A pnputil ID line: enumerator form (BUS\\...), star compatible ID
|
|
78
|
+
(*PNP0A0A), bare GUID, or a bare token (WireGuard software devices) —
|
|
79
|
+
never a 'Key:' header line. IDs never contain whitespace."""
|
|
80
|
+
if stripped.endswith(":"):
|
|
81
|
+
return False
|
|
82
|
+
if "\\" in stripped or stripped.startswith(("*", "{")):
|
|
83
|
+
return True
|
|
84
|
+
return len(stripped) >= 3 and not any(c.isspace() for c in stripped)
|
|
85
|
+
|
|
86
|
+
|
|
69
87
|
def _canon_hwid(hwid: str) -> str:
|
|
70
88
|
"""Fold the two HWID forms Windows reports for one device into one key.
|
|
71
89
|
|
|
@@ -307,7 +325,7 @@ def _apply_hwid_line(line: str, stripped: str, current: dict, in_hwids: bool) ->
|
|
|
307
325
|
if low.startswith(("hardware ids", "compatible ids")):
|
|
308
326
|
current.setdefault("hwids", [])
|
|
309
327
|
return True
|
|
310
|
-
if in_hwids and
|
|
328
|
+
if in_hwids and _looks_like_hwid(stripped):
|
|
311
329
|
current.setdefault("hwids", []).append(stripped.upper())
|
|
312
330
|
return True
|
|
313
331
|
if ":" in stripped and not line.startswith(" "):
|
|
@@ -322,6 +340,7 @@ def _flush_device(current: dict, devices: list[Device]) -> None:
|
|
|
322
340
|
device_name=current.get("name", ""),
|
|
323
341
|
hwids=list(dict.fromkeys(current.get("hwids", []))),
|
|
324
342
|
has_driver=bool(current.get("driver_name")),
|
|
343
|
+
driver_name=current.get("driver_name", ""),
|
|
325
344
|
))
|
|
326
345
|
|
|
327
346
|
|
|
@@ -372,7 +391,17 @@ def _print_summary(r: ScanResult) -> None:
|
|
|
372
391
|
if r.missing_driver:
|
|
373
392
|
events.progress("scan", "[scan] Missing drivers:")
|
|
374
393
|
for d in r.missing_driver:
|
|
375
|
-
name
|
|
376
|
-
|
|
377
|
-
events.progress("scan", f" ✗ {name}{cls}",
|
|
394
|
+
name = _device_label(d)
|
|
395
|
+
events.progress("scan", f" ✗ {name}",
|
|
378
396
|
device_name=name, class_name=d.class_name)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _device_label(d: Device) -> str:
|
|
400
|
+
"""Human line for a device — 'Unknown [Unknown]' tells the operator nothing,
|
|
401
|
+
so fall back to instance ID + first HWID."""
|
|
402
|
+
name = d.device_name
|
|
403
|
+
if not name or name.lower() == "unknown":
|
|
404
|
+
first_hwid = d.hwids[0] if d.hwids else ""
|
|
405
|
+
name = f"{d.instance_id} {first_hwid}".strip()
|
|
406
|
+
cls = d.class_name if d.class_name.lower() not in ("", "unknown") else ""
|
|
407
|
+
return f"{name} [{cls}]" if cls else name
|
|
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
|