driverclient 0.2.20__tar.gz → 0.2.21__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.20 → driverclient-0.2.21}/PKG-INFO +1 -1
- {driverclient-0.2.20 → driverclient-0.2.21}/pyproject.toml +1 -1
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/__init__.py +1 -1
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/config.json +7 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/config.py +20 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/core/http.py +6 -1
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/main.py +2 -0
- driverclient-0.2.21/src/driverclient/ops/download.py +63 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/ops/install.py +5 -19
- driverclient-0.2.21/src/driverclient/ops/offline.py +101 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/ops/resolve.py +15 -5
- {driverclient-0.2.20 → driverclient-0.2.21}/.gitignore +0 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/README.md +0 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/__main__.py +0 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/core/__init__.py +0 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/core/hardware.py +0 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/core/hwid.py +0 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/core/proc.py +0 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/events.py +0 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/ops/__init__.py +0 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/ops/automate.py +0 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/ops/capture.py +0 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/ops/diagnose.py +0 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/src/driverclient/ops/scan.py +0 -0
- {driverclient-0.2.20 → driverclient-0.2.21}/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.21
|
|
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.21"
|
|
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"
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
{
|
|
2
|
+
"_comment_mode": "mode = 'live' (default): SA/first-boot install+reboot+WU+capture. mode = 'offline': WinPE download-only to offline_driver_dir, then win11.bat.j2 DISM-injects. See examples/config.json for the full prod-deploy note.",
|
|
3
|
+
"mode": "live",
|
|
2
4
|
"local_repo_url": "http://localhost:8000",
|
|
3
5
|
"node_key": "REPLACE_WITH_YOUR_NODE_KEY",
|
|
4
6
|
|
|
5
7
|
"default_command": "automate",
|
|
6
8
|
|
|
9
|
+
"_comment_compression": "zstd downloads decompressed on-the-fly; keep true, set false only if zstd fails in WinPE.",
|
|
10
|
+
"compression": true,
|
|
7
11
|
"parallel_downloads": 6,
|
|
8
12
|
"parallel_uploads": 8,
|
|
9
13
|
"parallel_exports": 4,
|
|
10
14
|
|
|
15
|
+
"_comment_offline_driver_dir": "offline mode writes files here (WinPE); DISM injects into the applied image W:\\. Ignored in live mode.",
|
|
16
|
+
"offline_driver_dir": "W:\\drivers",
|
|
17
|
+
|
|
11
18
|
"driver_root": "C:\\DriverServer",
|
|
12
19
|
"cache_dir": "C:\\DriverServer\\client",
|
|
13
20
|
"work_dir": "C:\\DriverServer\\client\\staging",
|
|
@@ -13,6 +13,26 @@ DEFAULTS: dict = {
|
|
|
13
13
|
"local_repo_url": "http://localhost:8000",
|
|
14
14
|
"node_key": "",
|
|
15
15
|
|
|
16
|
+
# ── Mode ──────────────────────────────────────────────────────────────
|
|
17
|
+
# "live" = SA / first-boot: batched pnputil install + reboot loop + WU +
|
|
18
|
+
# capture (today's behaviour; handles the post-boot device tail).
|
|
19
|
+
# "offline" = pre-boot / WinPE: scan → resolve → DOWNLOAD ONLY to
|
|
20
|
+
# offline_driver_dir. No install, no reboot, no WU, no capture —
|
|
21
|
+
# win11.bat.j2's DISM step injects the files into the image.
|
|
22
|
+
"mode": "live",
|
|
23
|
+
|
|
24
|
+
# ── Offline (pre-boot / WinPE) ────────────────────────────────────────
|
|
25
|
+
# Where offline mode writes the decompressed driver files. DISM
|
|
26
|
+
# (win11.bat.j2) injects them into the applied image at W:\ afterward — the
|
|
27
|
+
# client only downloads; it never runs DISM itself.
|
|
28
|
+
"offline_driver_dir": "W:\\drivers",
|
|
29
|
+
|
|
30
|
+
# Request zstd-compressed downloads (decompressed on-the-fly in core/http.py):
|
|
31
|
+
# GPU ≈ 450 MB instead of 1.3 GB. True everywhere by default; set False only
|
|
32
|
+
# as a WinPE fallback if zstd ever fails to load there. Gates the
|
|
33
|
+
# Accept-Encoding: zstd request header sent by http_download.
|
|
34
|
+
"compression": True,
|
|
35
|
+
|
|
16
36
|
# ── Command ───────────────────────────────────────────────────────────
|
|
17
37
|
"default_command": "automate",
|
|
18
38
|
|
|
@@ -194,7 +194,12 @@ def http_download(url: str, dest: Path, sha256_expected: str,
|
|
|
194
194
|
"""
|
|
195
195
|
cfg = get()
|
|
196
196
|
t = timeout or cfg["timeout_long"]
|
|
197
|
-
|
|
197
|
+
# Only advertise zstd when compression is enabled. Offline/WinPE keeps it on
|
|
198
|
+
# (GPU ≈ 450 MB vs 1.3 GB); set compression=false as a fallback if zstd ever
|
|
199
|
+
# fails to load in WinPE, and the server serves the file uncompressed.
|
|
200
|
+
headers = dict(_headers())
|
|
201
|
+
if cfg.get("compression", True):
|
|
202
|
+
headers["Accept-Encoding"] = "zstd"
|
|
198
203
|
|
|
199
204
|
req = urllib.request.Request(url, headers=headers)
|
|
200
205
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -28,6 +28,7 @@ from driverclient.ops.capture import (
|
|
|
28
28
|
)
|
|
29
29
|
from driverclient.ops.diagnose import run_diagnostics
|
|
30
30
|
from driverclient.ops.install import resolve_and_install
|
|
31
|
+
from driverclient.ops.offline import resolve_and_download
|
|
31
32
|
from driverclient.ops.resolve import resolve
|
|
32
33
|
from driverclient.ops.scan import scan
|
|
33
34
|
|
|
@@ -35,6 +36,7 @@ _OPERATIONS: dict[str, callable] = {
|
|
|
35
36
|
"scan": scan,
|
|
36
37
|
"resolve": resolve,
|
|
37
38
|
"resolve-and-install": resolve_and_install,
|
|
39
|
+
"offline": resolve_and_download,
|
|
38
40
|
"capture-all": capture_all,
|
|
39
41
|
"capture-missing": capture_missing,
|
|
40
42
|
"capture-delta": capture_delta,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
client/ops/download.py — Shared driver-file download primitives.
|
|
3
|
+
|
|
4
|
+
Both the LIVE install path (ops/install.py) and the OFFLINE / pre-boot path
|
|
5
|
+
(ops/offline.py) fetch the same package files from the repo:
|
|
6
|
+
|
|
7
|
+
GET {repo_url}/files/{package_id}/{filename} (streamed, zstd on-the-fly,
|
|
8
|
+
sha256-verified — see core/http.py)
|
|
9
|
+
|
|
10
|
+
Factored out here so the two modes share one download implementation:
|
|
11
|
+
• download_driver() — fetch every file for ONE driver into base/<binding_id>/
|
|
12
|
+
• download_drivers() — fan the above out across a driver list in parallel
|
|
13
|
+
"""
|
|
14
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from driverclient.core.http import http_download_retry
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def download_driver(driver: dict, base: Path, repo_url: str,
|
|
21
|
+
cfg: dict) -> tuple[Path, bool]:
|
|
22
|
+
"""Download all files for one driver into base/<binding_id>/.
|
|
23
|
+
Returns (dest_dir, success). success is False if ANY file failed."""
|
|
24
|
+
bid = driver.get("binding_id") or driver.get("package_id", "")
|
|
25
|
+
pkg_id = driver["package_id"]
|
|
26
|
+
dest = base / bid
|
|
27
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
|
|
29
|
+
ok = True
|
|
30
|
+
for f in driver.get("files", []):
|
|
31
|
+
url = f"{repo_url}/files/{pkg_id}/{f['filename']}"
|
|
32
|
+
path = dest / f["filename"]
|
|
33
|
+
if not http_download_retry(url, path, f["sha256"],
|
|
34
|
+
timeout=cfg["timeout_long"]):
|
|
35
|
+
ok = False
|
|
36
|
+
|
|
37
|
+
return dest, ok
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def download_drivers(drivers: list[dict], base: Path,
|
|
41
|
+
cfg: dict) -> list[tuple[dict, Path | None, bool]]:
|
|
42
|
+
"""Download a list of drivers into `base` in parallel.
|
|
43
|
+
|
|
44
|
+
Returns a list of (driver, dest_dir | None, success) in completion order.
|
|
45
|
+
A driver that raised during download yields (driver, None, False) rather
|
|
46
|
+
than propagating — the caller decides how to report failures.
|
|
47
|
+
"""
|
|
48
|
+
repo_url = cfg["local_repo_url"]
|
|
49
|
+
parallel = cfg.get("parallel_downloads", 6)
|
|
50
|
+
|
|
51
|
+
results: list[tuple[dict, Path | None, bool]] = []
|
|
52
|
+
with ThreadPoolExecutor(max_workers=parallel) as executor:
|
|
53
|
+
fut_map = {executor.submit(download_driver, d, base, repo_url, cfg): d
|
|
54
|
+
for d in drivers}
|
|
55
|
+
for fut in as_completed(fut_map):
|
|
56
|
+
driver = fut_map[fut]
|
|
57
|
+
try:
|
|
58
|
+
dest, ok = fut.result()
|
|
59
|
+
except Exception:
|
|
60
|
+
results.append((driver, None, False))
|
|
61
|
+
continue
|
|
62
|
+
results.append((driver, dest, ok))
|
|
63
|
+
return results
|
|
@@ -26,7 +26,8 @@ from pathlib import Path
|
|
|
26
26
|
from driverclient import events
|
|
27
27
|
from driverclient.config import get
|
|
28
28
|
from driverclient.core import proc
|
|
29
|
-
from driverclient.core.http import
|
|
29
|
+
from driverclient.core.http import http_post_retry
|
|
30
|
+
from driverclient.ops.download import download_driver
|
|
30
31
|
from driverclient.ops.resolve import ResolveResult, resolve
|
|
31
32
|
|
|
32
33
|
# Install order: lower number first (hardware dependency)
|
|
@@ -248,7 +249,7 @@ def _stream_install(drivers: list[dict], base: Path, cfg: dict) -> list[DriverRe
|
|
|
248
249
|
results: list[DriverResult] = []
|
|
249
250
|
downloaded: list[tuple[dict, Path]] = []
|
|
250
251
|
with ThreadPoolExecutor(max_workers=parallel) as executor:
|
|
251
|
-
fut_map = {executor.submit(
|
|
252
|
+
fut_map = {executor.submit(download_driver, d, base, repo_url, cfg): d
|
|
252
253
|
for d in ordered}
|
|
253
254
|
for fut in as_completed(fut_map):
|
|
254
255
|
driver = fut_map[fut]
|
|
@@ -364,23 +365,8 @@ def _install_one_exe(driver: dict, driver_dir: Path, cfg: dict) -> DriverResult:
|
|
|
364
365
|
duration_ms=dur, reboot_required=reboot)
|
|
365
366
|
|
|
366
367
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
"""Download all files for one driver. Returns (dest_dir, success)."""
|
|
370
|
-
bid = driver.get("binding_id") or driver.get("package_id", "")
|
|
371
|
-
pkg_id = driver["package_id"]
|
|
372
|
-
dest = base / bid
|
|
373
|
-
dest.mkdir(parents=True, exist_ok=True)
|
|
374
|
-
|
|
375
|
-
ok = True
|
|
376
|
-
for f in driver.get("files", []):
|
|
377
|
-
url = f"{repo_url}/files/{pkg_id}/{f['filename']}"
|
|
378
|
-
path = dest / f["filename"]
|
|
379
|
-
if not http_download_retry(url, path, f["sha256"],
|
|
380
|
-
timeout=cfg["timeout_long"]):
|
|
381
|
-
ok = False
|
|
382
|
-
|
|
383
|
-
return dest, ok
|
|
368
|
+
# Driver-file download lives in ops/download.py (download_driver), shared with
|
|
369
|
+
# the offline / pre-boot path.
|
|
384
370
|
|
|
385
371
|
|
|
386
372
|
# ── Driver installation ─────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
client/ops/offline.py — Offline / pre-boot (WinPE) driver staging.
|
|
3
|
+
|
|
4
|
+
The FAST path, matching the old system: run inside WinPE against a freshly
|
|
5
|
+
applied Windows image, enumerate the machine's real HWIDs, resolve them against
|
|
6
|
+
the driverserver, and DOWNLOAD the matched driver files to offline_driver_dir
|
|
7
|
+
(default W:\\drivers). That's ALL this op does — it never installs.
|
|
8
|
+
|
|
9
|
+
win11.bat.j2 runs the injection afterward:
|
|
10
|
+
|
|
11
|
+
X:\\tools\\driverclient\\driverclient.exe (this op, mode=offline)
|
|
12
|
+
dism /image:W:\\ /add-driver /driver:W:\\drivers /recurse /forceunsigned
|
|
13
|
+
|
|
14
|
+
So there is NO live install, NO reboot loop, NO Windows Update, NO capture here —
|
|
15
|
+
everything is injected offline into the image and is present instantly at first
|
|
16
|
+
boot. The small post-boot device tail that WinPE can't see is handled later by
|
|
17
|
+
the LIVE mode (SA finalization).
|
|
18
|
+
|
|
19
|
+
Layout: each driver's files land in offline_driver_dir/<binding_id>/, so
|
|
20
|
+
duplicate INF filenames across packages never collide and DISM /recurse picks
|
|
21
|
+
them all up.
|
|
22
|
+
|
|
23
|
+
Public API:
|
|
24
|
+
resolve_and_download() -> OfflineResult
|
|
25
|
+
"""
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from driverclient import events
|
|
30
|
+
from driverclient.config import get
|
|
31
|
+
from driverclient.ops.download import download_drivers
|
|
32
|
+
from driverclient.ops.resolve import resolve
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class OfflineResult:
|
|
37
|
+
downloaded_ok: list[str] = field(default_factory=list)
|
|
38
|
+
download_failed: list[str] = field(default_factory=list)
|
|
39
|
+
not_found: list[str] = field(default_factory=list)
|
|
40
|
+
trace_id: str = ""
|
|
41
|
+
target_dir: str = ""
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def ok_count(self) -> int: return len(self.downloaded_ok)
|
|
45
|
+
@property
|
|
46
|
+
def fail_count(self) -> int: return len(self.download_failed)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resolve_and_download() -> OfflineResult:
|
|
50
|
+
"""
|
|
51
|
+
Scan → resolve (empty installed_versions → full matched set) → download every
|
|
52
|
+
matched driver's files to offline_driver_dir. No install. Returns OfflineResult.
|
|
53
|
+
"""
|
|
54
|
+
cfg = get()
|
|
55
|
+
target = Path(cfg.get("offline_driver_dir", "W:\\drivers"))
|
|
56
|
+
|
|
57
|
+
# force=True: fresh WinPE session, no meaningful cache to reuse — always scan
|
|
58
|
+
# the real hardware. offline=True: send empty installed_versions.
|
|
59
|
+
result = resolve(force=True, offline=True)
|
|
60
|
+
|
|
61
|
+
if not result.has_drivers:
|
|
62
|
+
events.done("offline",
|
|
63
|
+
"[offline] Nothing to download — no matched drivers",
|
|
64
|
+
total=0, not_found=len(result.not_found))
|
|
65
|
+
return OfflineResult(trace_id=result.trace_id,
|
|
66
|
+
not_found=result.not_found,
|
|
67
|
+
target_dir=str(target))
|
|
68
|
+
|
|
69
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
|
|
71
|
+
drivers = result.drivers
|
|
72
|
+
events.start("offline",
|
|
73
|
+
f"[offline] {len(drivers)} driver(s) → downloading to {target} …",
|
|
74
|
+
total=len(drivers))
|
|
75
|
+
|
|
76
|
+
downloaded = download_drivers(drivers, target, cfg)
|
|
77
|
+
|
|
78
|
+
ok: list[str] = []
|
|
79
|
+
failed: list[str] = []
|
|
80
|
+
for driver, _dest, success in downloaded:
|
|
81
|
+
bid = driver.get("binding_id") or driver.get("package_id", "")
|
|
82
|
+
(ok if success else failed).append(bid)
|
|
83
|
+
|
|
84
|
+
for bid in failed:
|
|
85
|
+
events.error("offline", f" ✗ {bid}: download_failed", binding_id=bid)
|
|
86
|
+
|
|
87
|
+
events.done(
|
|
88
|
+
"offline",
|
|
89
|
+
f"[offline] Done — {len(ok)} downloaded {len(failed)} failed "
|
|
90
|
+
f"→ {target} (DISM injects offline next)",
|
|
91
|
+
total=len(downloaded), ok=len(ok), failed=len(failed),
|
|
92
|
+
target_dir=str(target),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return OfflineResult(
|
|
96
|
+
downloaded_ok=ok,
|
|
97
|
+
download_failed=failed,
|
|
98
|
+
not_found=result.not_found,
|
|
99
|
+
trace_id=result.trace_id,
|
|
100
|
+
target_dir=str(target),
|
|
101
|
+
)
|
|
@@ -54,11 +54,17 @@ class ResolveResult:
|
|
|
54
54
|
return bool(self.not_found or self.stale)
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def resolve(force: bool = False) -> ResolveResult:
|
|
57
|
+
def resolve(force: bool = False, offline: bool = False) -> ResolveResult:
|
|
58
58
|
"""
|
|
59
59
|
Load (or run) a hardware scan, then POST to /resolve.
|
|
60
60
|
Returns ResolveResult with drivers[] and not_found[].
|
|
61
61
|
Saves result to ds_resolve.json.
|
|
62
|
+
|
|
63
|
+
offline: pre-boot / WinPE mode. The scan runs against WinPE, not the applied
|
|
64
|
+
image, so any "installed" versions it sees are irrelevant to the target. Send
|
|
65
|
+
installed_versions empty (and no inbox hints) so the server offers the FULL
|
|
66
|
+
matched set — every physically-present device's driver, GPU included — to be
|
|
67
|
+
DISM-injected into the fresh image. See ops/offline.py.
|
|
62
68
|
"""
|
|
63
69
|
cfg = get()
|
|
64
70
|
|
|
@@ -69,10 +75,11 @@ def resolve(force: bool = False) -> ResolveResult:
|
|
|
69
75
|
return cached
|
|
70
76
|
|
|
71
77
|
scan_result: ScanResult = scan(force=force)
|
|
72
|
-
return _query_repo(scan_result, cfg, force)
|
|
78
|
+
return _query_repo(scan_result, cfg, force, offline)
|
|
73
79
|
|
|
74
80
|
|
|
75
|
-
def _query_repo(scan_result: ScanResult, cfg: dict, force: bool
|
|
81
|
+
def _query_repo(scan_result: ScanResult, cfg: dict, force: bool,
|
|
82
|
+
offline: bool = False) -> ResolveResult:
|
|
76
83
|
repo_url = cfg["local_repo_url"]
|
|
77
84
|
session_id = str(uuid.uuid4())
|
|
78
85
|
|
|
@@ -112,8 +119,11 @@ def _query_repo(scan_result: ScanResult, cfg: dict, force: bool) -> ResolveResul
|
|
|
112
119
|
"firmware_version": scan_result.firmware_version,
|
|
113
120
|
"hwids": scan_result.all_hwids,
|
|
114
121
|
"hwid_groups": hwid_groups,
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
# Offline: no inbox hints and empty installed_versions → server offers the
|
|
123
|
+
# full matched set for the fresh image (nothing counts as already-current).
|
|
124
|
+
"inbox_hwids": [] if offline else inbox_ids,
|
|
125
|
+
"installed_versions": {} if offline
|
|
126
|
+
else {**scan_result.installed_versions, **canon_versions},
|
|
117
127
|
"force": force or cfg.get("force_resolve", False),
|
|
118
128
|
}
|
|
119
129
|
|
|
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
|