driverclient 0.2.17__tar.gz → 0.2.19__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.17 → driverclient-0.2.19}/PKG-INFO +1 -1
- {driverclient-0.2.17 → driverclient-0.2.19}/pyproject.toml +1 -1
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/__init__.py +1 -1
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/config.json +2 -1
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/ops/capture.py +60 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/ops/install.py +54 -9
- {driverclient-0.2.17 → driverclient-0.2.19}/.gitignore +0 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/README.md +0 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/__main__.py +0 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/config.py +0 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/core/__init__.py +0 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/core/hardware.py +0 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/core/http.py +0 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/core/hwid.py +0 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/core/proc.py +0 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/events.py +0 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/main.py +0 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/ops/__init__.py +0 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/ops/automate.py +0 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/ops/diagnose.py +0 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/ops/resolve.py +0 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/src/driverclient/ops/scan.py +0 -0
- {driverclient-0.2.17 → driverclient-0.2.19}/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.19
|
|
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.19"
|
|
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,8 +19,9 @@
|
|
|
19
19
|
"usb": [".inf", ".sys", ".cat", ".dll"]
|
|
20
20
|
},
|
|
21
21
|
|
|
22
|
-
"wu_wait_minutes":
|
|
22
|
+
"wu_wait_minutes": 30,
|
|
23
23
|
"wu_poll_interval_seconds": 30,
|
|
24
|
+
"wu_quiescent_polls": 2,
|
|
24
25
|
"wu_ignore_classes": ["hidclass", "bluetooth", "printer"],
|
|
25
26
|
|
|
26
27
|
"automate_wu_fallback": true,
|
|
@@ -714,6 +714,63 @@ def _trigger_wu() -> "_WuTrigger | None":
|
|
|
714
714
|
return None
|
|
715
715
|
|
|
716
716
|
|
|
717
|
+
def _emit_wu_log_line(line: str) -> None:
|
|
718
|
+
"""Turn one raw status line from the WU PowerShell into a friendly GUI event
|
|
719
|
+
so the operator sees WU's real phase (searching / downloading / installing)
|
|
720
|
+
live, instead of only a generic 'still working' tick."""
|
|
721
|
+
up = line.upper()
|
|
722
|
+
if up.startswith("SERVICE MICROSOFT_UPDATE"):
|
|
723
|
+
events.ok("windows_update",
|
|
724
|
+
"[wu-update] Using the Microsoft Update service (drivers + Microsoft products)")
|
|
725
|
+
elif up.startswith("SERVICE WINDOWS_UPDATE_FALLBACK"):
|
|
726
|
+
detail = line.split(None, 2)[2] if len(line.split(None, 2)) > 2 else ""
|
|
727
|
+
events.warn("windows_update",
|
|
728
|
+
f"[wu-update] Microsoft Update unavailable — falling back to default "
|
|
729
|
+
f"Windows Update service {detail}".rstrip())
|
|
730
|
+
elif up == "SEARCHING":
|
|
731
|
+
events.progress("windows_update",
|
|
732
|
+
"[wu-update] Searching Windows Update for applicable updates… "
|
|
733
|
+
"(the first scan can take several minutes)")
|
|
734
|
+
elif up == "NO_UPDATES":
|
|
735
|
+
events.progress("windows_update", "[wu-update] Search complete — no applicable updates found")
|
|
736
|
+
elif up.startswith("DOWNLOADING"):
|
|
737
|
+
n = line.split()[-1] if line.split() else "?"
|
|
738
|
+
events.progress("windows_update", f"[wu-update] Downloading {n} update(s)…")
|
|
739
|
+
elif up == "NONE_DOWNLOADED":
|
|
740
|
+
events.warn("windows_update", "[wu-update] Nothing downloaded")
|
|
741
|
+
elif up.startswith("INSTALLING"):
|
|
742
|
+
n = line.split()[-1] if line.split() else "?"
|
|
743
|
+
events.progress("windows_update", f"[wu-update] Installing {n} update(s)…")
|
|
744
|
+
elif up.startswith("INSTALLED"):
|
|
745
|
+
events.ok("windows_update", f"[wu-update] {line}")
|
|
746
|
+
elif up.startswith("WU_ERROR"):
|
|
747
|
+
detail = line[len("WU_ERROR"):].strip()
|
|
748
|
+
events.warn("windows_update", f"[wu-update] Windows Update error: {detail}", error=detail)
|
|
749
|
+
else:
|
|
750
|
+
events.progress("windows_update", f"[wu-update] {line}")
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def _drain_wu_log(trigger: "_WuTrigger | None", pos: int) -> int:
|
|
754
|
+
"""Emit any new lines the WU PowerShell wrote to its log since offset `pos`,
|
|
755
|
+
return the new offset. Best-effort — a read failure just leaves `pos`
|
|
756
|
+
unchanged and never raises into the poll loop. Only the COM path writes a
|
|
757
|
+
log (trigger.log_path); the usoclient/wuauclt fallbacks have none."""
|
|
758
|
+
if trigger is None or not trigger.log_path:
|
|
759
|
+
return pos
|
|
760
|
+
try:
|
|
761
|
+
with open(trigger.log_path, "r", encoding="utf-8", errors="replace") as fh:
|
|
762
|
+
fh.seek(pos)
|
|
763
|
+
chunk = fh.read()
|
|
764
|
+
new_pos = fh.tell()
|
|
765
|
+
except Exception:
|
|
766
|
+
return pos
|
|
767
|
+
for line in chunk.splitlines():
|
|
768
|
+
line = line.strip()
|
|
769
|
+
if line:
|
|
770
|
+
_emit_wu_log_line(line)
|
|
771
|
+
return new_pos
|
|
772
|
+
|
|
773
|
+
|
|
717
774
|
def _poll_and_submit_incremental(wu_start: datetime, cfg: dict,
|
|
718
775
|
trigger: "_WuTrigger | None") -> CaptureResult:
|
|
719
776
|
"""
|
|
@@ -739,12 +796,14 @@ def _poll_and_submit_incremental(wu_start: datetime, cfg: dict,
|
|
|
739
796
|
seen: set[str] = set()
|
|
740
797
|
total = CaptureResult()
|
|
741
798
|
empty_polls = 0
|
|
799
|
+
log_pos = 0
|
|
742
800
|
|
|
743
801
|
events.progress("windows_update",
|
|
744
802
|
f"[wu-update] Waiting on Windows Update — polling every {poll_secs}s, "
|
|
745
803
|
f"stops early once WU goes idle (hard cap {wait_min} min)")
|
|
746
804
|
|
|
747
805
|
while datetime.now(timezone.utc) < deadline:
|
|
806
|
+
log_pos = _drain_wu_log(trigger, log_pos) # surface WU's live phase
|
|
748
807
|
all_new = _enumerate_packages_since(wu_start, cfg)
|
|
749
808
|
new_batch = [
|
|
750
809
|
p for p in all_new
|
|
@@ -789,6 +848,7 @@ def _poll_and_submit_incremental(wu_start: datetime, cfg: dict,
|
|
|
789
848
|
events.warn("windows_update",
|
|
790
849
|
f"[wu-update] Reached {wait_min} min hard cap — stopping WU wait")
|
|
791
850
|
|
|
851
|
+
_drain_wu_log(trigger, log_pos) # flush any final lines before cleanup
|
|
792
852
|
if trigger is not None:
|
|
793
853
|
trigger.terminate()
|
|
794
854
|
return total
|
|
@@ -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,17 @@ 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
|
+
# 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).
|
|
164
|
+
skip_installed = cfg.get("skip_installed_drivers", True)
|
|
165
|
+
idx: dict = _installed_inf_index() if skip_installed else {}
|
|
166
|
+
|
|
157
167
|
drivers = result.drivers
|
|
158
|
-
if
|
|
159
|
-
idx = _installed_inf_index()
|
|
168
|
+
if skip_installed:
|
|
160
169
|
kept = [d for d in drivers if not _already_in_store(d, idx)]
|
|
161
170
|
skipped = len(drivers) - len(kept)
|
|
162
171
|
if skipped:
|
|
@@ -179,21 +188,24 @@ def resolve_and_install(force: bool = False) -> InstallResult:
|
|
|
179
188
|
events.start("install",
|
|
180
189
|
f"[install] {len(drivers)} driver(s) — starting parallel downloads…",
|
|
181
190
|
total=len(drivers))
|
|
182
|
-
all_results = _stream_install(drivers, work_dir, cfg)
|
|
191
|
+
all_results = _stream_install(drivers, work_dir, cfg, idx, skip_installed)
|
|
183
192
|
|
|
184
193
|
shutil.rmtree(work_dir, ignore_errors=True)
|
|
185
194
|
|
|
186
|
-
installed_ok = [r for r in all_results if r.success]
|
|
195
|
+
installed_ok = [r for r in all_results if r.success and not r.skipped]
|
|
196
|
+
skipped_now = [r for r in all_results if r.skipped]
|
|
187
197
|
install_failed = [r for r in all_results if not r.success]
|
|
188
198
|
|
|
189
|
-
|
|
199
|
+
# Skipped drivers ARE satisfied on the machine — report them upstream as success.
|
|
200
|
+
_confirm(result.trace_id, installed_ok + skipped_now, install_failed, cfg)
|
|
190
201
|
|
|
191
202
|
reboot_required = any(r.reboot_required for r in installed_ok)
|
|
192
203
|
reboot_tag = " — reboot required" if reboot_required else ""
|
|
204
|
+
skip_tag = f" ({len(skipped_now)} already in store)" if skipped_now else ""
|
|
193
205
|
events.done("install",
|
|
194
|
-
f"[install] Done — {len(installed_ok)} ok {len(install_failed)} failed{reboot_tag}",
|
|
206
|
+
f"[install] Done — {len(installed_ok)} ok {len(install_failed)} failed{skip_tag}{reboot_tag}",
|
|
195
207
|
total=len(all_results), ok=len(installed_ok), failed=len(install_failed),
|
|
196
|
-
reboot_required=reboot_required)
|
|
208
|
+
skipped=len(skipped_now), reboot_required=reboot_required)
|
|
197
209
|
for r in install_failed:
|
|
198
210
|
events.error("install", f" ✗ {r.binding_id}: {r.error}",
|
|
199
211
|
binding_id=r.binding_id, error=r.error)
|
|
@@ -207,10 +219,16 @@ def resolve_and_install(force: bool = False) -> InstallResult:
|
|
|
207
219
|
|
|
208
220
|
# ── Streaming pipeline ──────────────────────────────────────────────────────────
|
|
209
221
|
|
|
210
|
-
def _stream_install(drivers: list[dict], base: Path, cfg: dict
|
|
222
|
+
def _stream_install(drivers: list[dict], base: Path, cfg: dict,
|
|
223
|
+
idx: dict, skip_installed: bool) -> list[DriverResult]:
|
|
211
224
|
"""
|
|
212
225
|
Submit ALL downloads (all levels) simultaneously, then install level-by-level.
|
|
213
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.
|
|
214
232
|
"""
|
|
215
233
|
repo_url = cfg["local_repo_url"]
|
|
216
234
|
parallel = cfg.get("parallel_downloads", 6)
|
|
@@ -225,7 +243,7 @@ def _stream_install(drivers: list[dict], base: Path, cfg: dict) -> list[DriverRe
|
|
|
225
243
|
f"[install] {_level_label(level)} ({len(level_futs[level])} driver(s))…",
|
|
226
244
|
category=_level_label(level), count=len(level_futs[level]))
|
|
227
245
|
for fut in as_completed(level_futs[level]):
|
|
228
|
-
all_results.append(_process_completed(fut, future_info, cfg))
|
|
246
|
+
all_results.append(_process_completed(fut, future_info, cfg, idx, skip_installed))
|
|
229
247
|
|
|
230
248
|
executor.shutdown(wait=False)
|
|
231
249
|
return all_results
|
|
@@ -266,12 +284,23 @@ def _process_completed(
|
|
|
266
284
|
fut: Future,
|
|
267
285
|
future_info: dict[Future, tuple[int, dict]],
|
|
268
286
|
cfg: dict,
|
|
287
|
+
idx: dict,
|
|
288
|
+
skip_installed: bool,
|
|
269
289
|
) -> DriverResult:
|
|
270
290
|
"""Handle one completed download Future: install if ok, return DriverResult."""
|
|
271
291
|
_, driver = future_info[fut]
|
|
272
292
|
bid = driver.get("binding_id", "")
|
|
273
293
|
cat = driver.get("category", "other")
|
|
274
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
|
+
|
|
275
304
|
try:
|
|
276
305
|
dest, dl_ok = fut.result()
|
|
277
306
|
except Exception as e:
|
|
@@ -283,6 +312,8 @@ def _process_completed(
|
|
|
283
312
|
t0 = time.monotonic()
|
|
284
313
|
ok, err, reboot = _install_driver(driver, dest, cfg)
|
|
285
314
|
dur = int((time.monotonic() - t0) * 1000)
|
|
315
|
+
if ok:
|
|
316
|
+
_record_installed(driver, idx) # update live index so same-family dupes skip
|
|
286
317
|
tag = " (reboot required)" if reboot else ""
|
|
287
318
|
line = f" {'✓' if ok else '✗'} [{cat}] {bid} ({dur}ms){tag}"
|
|
288
319
|
emit = events.ok if ok else events.error
|
|
@@ -292,6 +323,20 @@ def _process_completed(
|
|
|
292
323
|
duration_ms=dur, reboot_required=reboot)
|
|
293
324
|
|
|
294
325
|
|
|
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
|
+
|
|
295
340
|
def _download_driver(driver: dict, base: Path, repo_url: str,
|
|
296
341
|
cfg: dict) -> tuple[Path, bool]:
|
|
297
342
|
"""Download all files for one driver. Returns (dest_dir, success)."""
|
|
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
|