driverclient 0.2.26__tar.gz → 0.2.28__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.26 → driverclient-0.2.28}/PKG-INFO +1 -1
- driverclient-0.2.28/pyproject.toml +123 -0
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/__init__.py +3 -2
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/__main__.py +1 -0
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/config.py +22 -35
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/core/hardware.py +79 -45
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/core/http.py +57 -34
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/core/hwid.py +15 -5
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/core/proc.py +2 -0
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/events.py +44 -29
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/main.py +14 -11
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/ops/__init__.py +10 -5
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/ops/automate.py +83 -54
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/ops/capture.py +391 -247
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/ops/diagnose.py +2 -1
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/ops/download.py +10 -11
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/ops/install.py +156 -98
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/ops/offline.py +29 -18
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/ops/resolve.py +46 -36
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/ops/scan.py +63 -50
- driverclient-0.2.26/pyproject.toml +0 -42
- {driverclient-0.2.26 → driverclient-0.2.28}/.gitignore +0 -0
- {driverclient-0.2.26 → driverclient-0.2.28}/README.md +0 -0
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/config.json +0 -0
- {driverclient-0.2.26 → driverclient-0.2.28}/src/driverclient/core/__init__.py +0 -0
- {driverclient-0.2.26 → driverclient-0.2.28}/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.28
|
|
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>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "driverclient"
|
|
7
|
+
version = "0.2.28"
|
|
8
|
+
description = "Driver Server client with an embeddable connector for config injection at runtime."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Raja Sanaullah", email = "sanaullah@99technologies.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["driver", "windows", "pnputil", "deployment", "pxe"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Operating System :: Microsoft :: Windows",
|
|
20
|
+
"Intended Audience :: System Administrators",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"zstandard>=0.22.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://example.com/driverclient"
|
|
28
|
+
|
|
29
|
+
# ── Dependency groups (PEP 735) ───────────────────────────────────────────────
|
|
30
|
+
# Tooling only — never shipped in the wheel (dependency groups are not part of
|
|
31
|
+
# the built distribution). ruff is pinned to the same version as the repo-root
|
|
32
|
+
# .pre-commit-config.yaml so a local commit and a manual run apply identical rules.
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
dev = [
|
|
35
|
+
"ruff==0.15.22",
|
|
36
|
+
"mypy",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/driverclient"]
|
|
41
|
+
# Ship non-Python package data (config template + type marker) inside the wheel.
|
|
42
|
+
artifacts = [
|
|
43
|
+
"src/driverclient/config.json",
|
|
44
|
+
"src/driverclient/py.typed",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.sdist]
|
|
48
|
+
include = [
|
|
49
|
+
"src/driverclient",
|
|
50
|
+
"README.md",
|
|
51
|
+
"pyproject.toml",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# ── Ruff (lint + format) ──────────────────────────────────────────────────────
|
|
55
|
+
# Mirrors backend/pyproject.toml so both halves of the repo read the same, with
|
|
56
|
+
# two deliberate differences: target-version follows this package's
|
|
57
|
+
# requires-python (>=3.10, not the backend's 3.12), and the ASYNC/FastAPI rules
|
|
58
|
+
# are dropped — the client is synchronous (threads + concurrent.futures).
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
target-version = "py310" # floor of requires-python; the client runs on stock Windows Pythons
|
|
61
|
+
line-length = 100
|
|
62
|
+
extend-exclude = [".venv", ".venv-smoke", "dist", "build"]
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = [
|
|
66
|
+
"E", "W", # pycodestyle
|
|
67
|
+
"F", # pyflakes (unused imports/names)
|
|
68
|
+
"I", # isort (sorts imports on --fix)
|
|
69
|
+
"N", # pep8-naming
|
|
70
|
+
"B", # flake8-bugbear (common bug patterns)
|
|
71
|
+
"UP", # pyupgrade (modernize syntax)
|
|
72
|
+
"SIM", # flake8-simplify
|
|
73
|
+
"C4", # flake8-comprehensions
|
|
74
|
+
"RUF", # ruff-specific
|
|
75
|
+
"LOG", # flake8-logging — logging hygiene
|
|
76
|
+
]
|
|
77
|
+
ignore = [
|
|
78
|
+
"E501", # line length is enforced by the formatter, not the linter
|
|
79
|
+
"RUF002", # ambiguous unicode (×, –) in docstrings — intentional typography
|
|
80
|
+
"RUF003", # ambiguous unicode (×, –) in comments — intentional typography
|
|
81
|
+
# ── Parked: promote to enforced after a dedicated cleanup pass ──
|
|
82
|
+
"N818", # exception *Error suffix — renaming exception classes is cross-cutting churn
|
|
83
|
+
"UP042", # str-Enum -> StrEnum — behavior-sensitive under JSON serialization
|
|
84
|
+
"UP046", # PEP 695 generic class
|
|
85
|
+
"UP047", # PEP 695 generic function
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
[tool.ruff.lint.per-file-ignores]
|
|
89
|
+
"src/driverclient/__init__.py" = ["F401"] # re-exports are intentional in the package entry point
|
|
90
|
+
"examples/run_client.py" = ["E402"] # stdout/stderr must be reconfigured before the driverclient import
|
|
91
|
+
|
|
92
|
+
[tool.ruff.lint.isort]
|
|
93
|
+
known-first-party = ["driverclient"]
|
|
94
|
+
combine-as-imports = true
|
|
95
|
+
|
|
96
|
+
[tool.ruff.format]
|
|
97
|
+
quote-style = "double"
|
|
98
|
+
indent-style = "space"
|
|
99
|
+
|
|
100
|
+
# ── Mypy (non-strict baseline; ratchet strictness per-module later) ───────────
|
|
101
|
+
# The package ships py.typed, so these annotations are part of its public
|
|
102
|
+
# contract for embedding apps — worth tightening (disallow_untyped_defs) once
|
|
103
|
+
# the current signatures are annotated.
|
|
104
|
+
[tool.mypy]
|
|
105
|
+
python_version = "3.10"
|
|
106
|
+
# src layout: the import root is src/, not the package directory. mypy_path lets
|
|
107
|
+
# `import driverclient` resolve from examples/ and the root scripts without an
|
|
108
|
+
# editable install. (mypy accepts only ONE of packages/modules/files — so the
|
|
109
|
+
# package is listed here as a path, not via `packages`.)
|
|
110
|
+
mypy_path = "src"
|
|
111
|
+
files = ["src/driverclient", "examples", "smoke_test.py", "bump_version.py"]
|
|
112
|
+
# Analyse as Windows. Without this, mypy assumes the host platform, treats every
|
|
113
|
+
# `if sys.platform == "win32":` block as unreachable, and silently skips the bulk
|
|
114
|
+
# of the client's real logic — and `import winreg` fails to resolve on macOS/Linux.
|
|
115
|
+
platform = "win32"
|
|
116
|
+
warn_redundant_casts = true
|
|
117
|
+
warn_unused_ignores = true
|
|
118
|
+
warn_unused_configs = true
|
|
119
|
+
|
|
120
|
+
[[tool.mypy.overrides]]
|
|
121
|
+
# Third-party packages without type stubs.
|
|
122
|
+
module = ["zstandard.*"]
|
|
123
|
+
ignore_missing_imports = true
|
|
@@ -19,13 +19,14 @@ Typical use from an embedding app (e.g. a PyQt front-end):
|
|
|
19
19
|
Config merge priority (highest wins):
|
|
20
20
|
DEFAULTS < DS_CLIENT_CONFIG env file < config_path file < kwargs
|
|
21
21
|
"""
|
|
22
|
+
|
|
22
23
|
from driverclient import config as _config
|
|
23
24
|
from driverclient.events import ClientEvent, using_sink
|
|
24
25
|
from driverclient.main import run as _run
|
|
25
26
|
|
|
26
|
-
__all__ = ["
|
|
27
|
+
__all__ = ["ClientEvent", "DriverClient"]
|
|
27
28
|
|
|
28
|
-
__version__ = "0.2.
|
|
29
|
+
__version__ = "0.2.28"
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
class DriverClient:
|
|
@@ -4,15 +4,15 @@ driverclient/config.py — Load client settings.
|
|
|
4
4
|
Merge priority (highest wins):
|
|
5
5
|
DEFAULTS < DS_CLIENT_CONFIG env file < file passed to load() < overrides
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
import json
|
|
8
9
|
import os
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
11
12
|
DEFAULTS: dict = {
|
|
12
13
|
# ── Server ────────────────────────────────────────────────────────────
|
|
13
|
-
"local_repo_url":
|
|
14
|
-
"node_key":
|
|
15
|
-
|
|
14
|
+
"local_repo_url": "http://localhost:8000",
|
|
15
|
+
"node_key": "",
|
|
16
16
|
# ── Mode ──────────────────────────────────────────────────────────────
|
|
17
17
|
# "live" = SA / first-boot: batched pnputil install + reboot loop + WU +
|
|
18
18
|
# capture (today's behaviour; handles the post-boot device tail).
|
|
@@ -20,36 +20,29 @@ DEFAULTS: dict = {
|
|
|
20
20
|
# offline_driver_dir. No install, no reboot, no WU, no capture —
|
|
21
21
|
# win11.bat.j2's DISM step injects the files into the image.
|
|
22
22
|
"mode": "live",
|
|
23
|
-
|
|
24
23
|
# ── Offline (pre-boot / WinPE) ────────────────────────────────────────
|
|
25
24
|
# Where offline mode writes the decompressed driver files. DISM
|
|
26
25
|
# (win11.bat.j2) injects them into the applied image at W:\ afterward — the
|
|
27
26
|
# client only downloads; it never runs DISM itself.
|
|
28
27
|
"offline_driver_dir": "W:\\drivers",
|
|
29
|
-
|
|
30
28
|
# Request zstd-compressed downloads (decompressed on-the-fly in core/http.py):
|
|
31
29
|
# GPU ≈ 450 MB instead of 1.3 GB. True everywhere by default; set False only
|
|
32
30
|
# as a WinPE fallback if zstd ever fails to load there. Gates the
|
|
33
31
|
# Accept-Encoding: zstd request header sent by http_download.
|
|
34
32
|
"compression": True,
|
|
35
|
-
|
|
36
33
|
# ── Command ───────────────────────────────────────────────────────────
|
|
37
|
-
"default_command":
|
|
38
|
-
|
|
34
|
+
"default_command": "automate",
|
|
39
35
|
# ── Parallelism ───────────────────────────────────────────────────────
|
|
40
36
|
"parallel_downloads": 6,
|
|
41
|
-
"parallel_uploads":
|
|
42
|
-
"parallel_exports":
|
|
43
|
-
|
|
37
|
+
"parallel_uploads": 8,
|
|
38
|
+
"parallel_exports": 4,
|
|
44
39
|
# ── Paths ─────────────────────────────────────────────────────────────
|
|
45
40
|
"driver_root": "C:\\DriverServer",
|
|
46
|
-
"cache_dir":
|
|
47
|
-
"work_dir":
|
|
48
|
-
|
|
41
|
+
"cache_dir": "C:\\DriverServer\\client",
|
|
42
|
+
"work_dir": "C:\\DriverServer\\client\\staging",
|
|
49
43
|
# ── Cache TTL ─────────────────────────────────────────────────────────
|
|
50
44
|
# ds_scan.json / ds_resolve.json are reused if younger than this many minutes
|
|
51
45
|
"cache_ttl_minutes": 15,
|
|
52
|
-
|
|
53
46
|
# ── Capture ───────────────────────────────────────────────────────────
|
|
54
47
|
# Skip packages larger than this (MB) during capture — keeps the repo light.
|
|
55
48
|
# A complete driver = its full size; a few (Intel graphics ~1 GB) are huge and
|
|
@@ -60,60 +53,54 @@ DEFAULTS: dict = {
|
|
|
60
53
|
# non-blocking loop that retries transient failures, so big packages don't
|
|
61
54
|
# block or get lost). Set >0 only if you want a lighter repo.
|
|
62
55
|
"capture_max_package_mb": 0,
|
|
63
|
-
|
|
64
56
|
# [".inf", ".sys", ".cat"] — minimum for server to redistribute and install
|
|
65
57
|
# [".inf", ".sys", ".cat", ".dll"] — adds co-installer DLLs
|
|
66
58
|
# ["*"] — all files in the driver package directory
|
|
67
|
-
"dump_extensions":
|
|
68
|
-
|
|
59
|
+
"dump_extensions": [".inf", ".sys", ".cat"],
|
|
69
60
|
# Per-category extension overrides — key = class_name from pnputil (lowercase)
|
|
70
61
|
# Categories not listed here use dump_extensions above
|
|
71
62
|
"dump_extensions_overrides": {
|
|
72
|
-
"system":
|
|
73
|
-
"usb":
|
|
63
|
+
"system": [".inf", ".sys", ".cat", ".dll"],
|
|
64
|
+
"usb": [".inf", ".sys", ".cat", ".dll"],
|
|
74
65
|
},
|
|
75
|
-
|
|
76
66
|
# ── Windows Update ────────────────────────────────────────────────────
|
|
77
67
|
# ABSOLUTE ceiling for the WU wait — a safety backstop, NOT the expected
|
|
78
68
|
# wait. The loop stops as soon as WU actually goes idle (see below); this
|
|
79
69
|
# only bounds a stuck/hanging WU so SA can never stall.
|
|
80
|
-
"wu_wait_minutes":
|
|
70
|
+
"wu_wait_minutes": 30,
|
|
81
71
|
"wu_poll_interval_seconds": 30,
|
|
82
72
|
# Empty polls required AFTER Windows Update has gone idle before we stop.
|
|
83
73
|
# While WU is still working (install process alive), empty polls are NOT
|
|
84
74
|
# counted — so a scan/download that takes 2 min or 8 min both get exactly
|
|
85
75
|
# the time they need. This is what makes the wait adaptive instead of a
|
|
86
76
|
# fixed timer.
|
|
87
|
-
"wu_quiescent_polls":
|
|
77
|
+
"wu_quiescent_polls": 2,
|
|
88
78
|
# Classes to skip when deciding which not-found HWIDs trigger WU.
|
|
89
79
|
# Empty list = no filtering (default — make no assumptions).
|
|
90
80
|
# Use lowercase class names as reported by pnputil, e.g. "hidclass", "bluetooth".
|
|
91
|
-
"wu_ignore_classes":
|
|
92
|
-
|
|
81
|
+
"wu_ignore_classes": ["hidclass"],
|
|
93
82
|
# ── Automate flags ────────────────────────────────────────────────────
|
|
94
83
|
"automate_wu_fallback": True,
|
|
95
|
-
"automate_re_resolve":
|
|
84
|
+
"automate_re_resolve": False,
|
|
96
85
|
# Extra same-boot scan→resolve→install passes after the first install, to
|
|
97
86
|
# pick up devices exposed only once a parent driver binds (chained deps).
|
|
98
87
|
# 0 disables the loop (single install pass). Stops early when a pass finds
|
|
99
88
|
# no new repo drivers or installs nothing.
|
|
100
89
|
"automate_rescan_passes": 3,
|
|
101
|
-
"force_resolve":
|
|
90
|
+
"force_resolve": False,
|
|
102
91
|
# The server now offers every driver matching each device (primary + extensions
|
|
103
92
|
# + companions). Before downloading each, skip it if its INF is already in the
|
|
104
93
|
# Windows driver store at the same-or-newer version — so a still-current big
|
|
105
94
|
# driver (e.g. graphics) is never re-downloaded, while missing/extension drivers
|
|
106
95
|
# still install. Set False to force-reinstall everything the server offers.
|
|
107
96
|
"skip_installed_drivers": True,
|
|
108
|
-
|
|
109
97
|
# ── HTTP timeouts (seconds) ───────────────────────────────────────────
|
|
110
|
-
"timeout_tiny":
|
|
111
|
-
"timeout_short":
|
|
112
|
-
"timeout_medium":
|
|
113
|
-
"timeout_long":
|
|
114
|
-
"timeout_install":
|
|
115
|
-
"timeout_wmi_slow":
|
|
116
|
-
|
|
98
|
+
"timeout_tiny": 10,
|
|
99
|
+
"timeout_short": 30,
|
|
100
|
+
"timeout_medium": 60,
|
|
101
|
+
"timeout_long": 120,
|
|
102
|
+
"timeout_install": 600,
|
|
103
|
+
"timeout_wmi_slow": 90,
|
|
117
104
|
# Package-upload timeout scales with archive size instead of a flat 60s so a
|
|
118
105
|
# ~1 GB graphics package never times out mid-transfer. This is the assumed
|
|
119
106
|
# worst-case sustained upload throughput (MB/s) used to derive the socket
|
|
@@ -8,6 +8,7 @@ Strategy:
|
|
|
8
8
|
Still works on Win10/11; deprecated in Win11 22H2+ but warnings
|
|
9
9
|
go to stderr which is captured and discarded.
|
|
10
10
|
"""
|
|
11
|
+
|
|
11
12
|
import hashlib
|
|
12
13
|
import json
|
|
13
14
|
import uuid
|
|
@@ -60,32 +61,34 @@ Get-CimInstance -ClassName Win32_PnPSignedDriver |
|
|
|
60
61
|
|
|
61
62
|
# ── Public API ─────────────────────────────────────────────────────────────────
|
|
62
63
|
|
|
64
|
+
|
|
63
65
|
def compute_fingerprint() -> str:
|
|
64
66
|
"""Stable machine UUID derived from MB serial + CPU ID + first NIC MAC."""
|
|
65
67
|
data = _get_profile_data()
|
|
66
68
|
if data:
|
|
67
|
-
mb
|
|
69
|
+
mb = data.get("serial_board", "") or data.get("serial_bios", "")
|
|
68
70
|
cpu = data.get("cpu_id", "")
|
|
69
71
|
mac = data.get("mac", "")
|
|
70
72
|
else:
|
|
71
73
|
with ThreadPoolExecutor(max_workers=3) as ex:
|
|
72
|
-
mb_f
|
|
74
|
+
mb_f = ex.submit(_wmic, "baseboard get SerialNumber")
|
|
73
75
|
cpu_f = ex.submit(_wmic, "cpu get ProcessorId")
|
|
74
76
|
mac_f = ex.submit(_wmic, "nic where AdapterTypeId=0 get MACAddress")
|
|
75
77
|
mb, cpu, mac = mb_f.result(), cpu_f.result(), mac_f.result()
|
|
76
|
-
raw
|
|
78
|
+
raw = f"{mb}|{cpu}|{mac}"
|
|
77
79
|
digest = hashlib.sha256(raw.encode()).digest()
|
|
78
80
|
return str(uuid.UUID(bytes=digest[:16]))
|
|
79
81
|
|
|
80
82
|
|
|
81
83
|
def get_hwids() -> list[str]:
|
|
82
84
|
"""Return all connected device HWIDs via pnputil."""
|
|
83
|
-
cfg
|
|
85
|
+
cfg = get()
|
|
84
86
|
hwids = []
|
|
85
87
|
try:
|
|
86
88
|
r = proc.run(
|
|
87
89
|
["pnputil", "/enum-devices", "/connected", "/ids"],
|
|
88
|
-
capture_output=True,
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True,
|
|
89
92
|
timeout=cfg["timeout_short"],
|
|
90
93
|
)
|
|
91
94
|
for line in r.stdout.splitlines():
|
|
@@ -114,9 +117,9 @@ def get_installed_versions(hwids: list[str]) -> dict[str, str]:
|
|
|
114
117
|
Tries PowerShell first, falls back to wmic CSV.
|
|
115
118
|
Caches result keyed by sorted HWID list hash.
|
|
116
119
|
"""
|
|
117
|
-
cfg
|
|
120
|
+
cfg = get()
|
|
118
121
|
hwid_hash = hashlib.sha256("|".join(sorted(hwids)).encode()).hexdigest()[:16]
|
|
119
|
-
cache
|
|
122
|
+
cache = _load_versions_cache(cfg, hwid_hash)
|
|
120
123
|
if cache is not None:
|
|
121
124
|
return cache
|
|
122
125
|
|
|
@@ -131,13 +134,23 @@ def get_installed_versions(hwids: list[str]) -> dict[str, str]:
|
|
|
131
134
|
|
|
132
135
|
# ── PowerShell helpers ─────────────────────────────────────────────────────────
|
|
133
136
|
|
|
137
|
+
|
|
134
138
|
def _run_ps(script: str, timeout: int) -> str:
|
|
135
139
|
"""Run a PowerShell script inline. Returns stdout or '' on failure."""
|
|
136
140
|
try:
|
|
137
141
|
r = proc.run(
|
|
138
|
-
[
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
[
|
|
143
|
+
"powershell",
|
|
144
|
+
"-NoProfile",
|
|
145
|
+
"-NonInteractive",
|
|
146
|
+
"-ExecutionPolicy",
|
|
147
|
+
"Bypass",
|
|
148
|
+
"-Command",
|
|
149
|
+
script,
|
|
150
|
+
],
|
|
151
|
+
capture_output=True,
|
|
152
|
+
text=True,
|
|
153
|
+
timeout=timeout,
|
|
141
154
|
)
|
|
142
155
|
return r.stdout.strip() if r.returncode == 0 else ""
|
|
143
156
|
except Exception:
|
|
@@ -146,8 +159,8 @@ def _run_ps(script: str, timeout: int) -> str:
|
|
|
146
159
|
|
|
147
160
|
def _get_profile_data() -> dict:
|
|
148
161
|
"""Run _PROFILE_PS and return parsed JSON dict, or {} on failure."""
|
|
149
|
-
cfg
|
|
150
|
-
out
|
|
162
|
+
cfg = get()
|
|
163
|
+
out = _run_ps(_PROFILE_PS, cfg.get("timeout_wmi_slow", 90))
|
|
151
164
|
if not out:
|
|
152
165
|
return {}
|
|
153
166
|
try:
|
|
@@ -163,7 +176,11 @@ def _profile_from_ps(data: dict) -> dict:
|
|
|
163
176
|
# human name in ComputerSystemProduct.Version / SystemFamily (e.g. "ThinkPad T460s").
|
|
164
177
|
# Prefer the first meaningful candidate that isn't just the machine-type code.
|
|
165
178
|
friendly = ""
|
|
166
|
-
for cand in (
|
|
179
|
+
for cand in (
|
|
180
|
+
data.get("product_version"),
|
|
181
|
+
data.get("system_family"),
|
|
182
|
+
data.get("friendly_model"),
|
|
183
|
+
):
|
|
167
184
|
cand = _clean(cand or "")
|
|
168
185
|
if _is_meaningful(cand) and cand.lower() != model.lower():
|
|
169
186
|
friendly = cand
|
|
@@ -171,21 +188,21 @@ def _profile_from_ps(data: dict) -> dict:
|
|
|
171
188
|
if not friendly:
|
|
172
189
|
friendly = _clean(data.get("friendly_model", "") or "")
|
|
173
190
|
return {
|
|
174
|
-
"vendor":
|
|
175
|
-
"model":
|
|
176
|
-
"friendly_model":
|
|
177
|
-
"serial":
|
|
178
|
-
"chassis_type":
|
|
179
|
-
"os_build":
|
|
180
|
-
"arch":
|
|
191
|
+
"vendor": _clean(data.get("vendor", "Unknown")),
|
|
192
|
+
"model": model,
|
|
193
|
+
"friendly_model": _clean(friendly),
|
|
194
|
+
"serial": _clean(data.get("serial_bios") or data.get("serial_board", "")),
|
|
195
|
+
"chassis_type": _parse_chassis(data.get("chassis", "")),
|
|
196
|
+
"os_build": _safe_int(data.get("os_build", "0")),
|
|
197
|
+
"arch": "x64",
|
|
181
198
|
"firmware_version": _clean(data.get("firmware", "")),
|
|
182
199
|
}
|
|
183
200
|
|
|
184
201
|
|
|
185
202
|
def _installed_versions_ps() -> dict[str, str]:
|
|
186
203
|
"""Get installed driver versions via PowerShell. Returns {} on failure."""
|
|
187
|
-
cfg
|
|
188
|
-
out
|
|
204
|
+
cfg = get()
|
|
205
|
+
out = _run_ps(_INSTALLED_PS, cfg.get("timeout_wmi_slow", 90))
|
|
189
206
|
if not out:
|
|
190
207
|
return {}
|
|
191
208
|
try:
|
|
@@ -203,23 +220,24 @@ def _installed_versions_ps() -> dict[str, str]:
|
|
|
203
220
|
|
|
204
221
|
# ── wmic fallback helpers ──────────────────────────────────────────────────────
|
|
205
222
|
|
|
223
|
+
|
|
206
224
|
def _profile_from_wmic() -> dict:
|
|
207
225
|
with ThreadPoolExecutor(max_workers=6) as ex:
|
|
208
|
-
vendor_f
|
|
209
|
-
model_f
|
|
226
|
+
vendor_f = ex.submit(_wmic, "computersystem get Manufacturer")
|
|
227
|
+
model_f = ex.submit(_wmic, "computersystem get Model")
|
|
210
228
|
friendly_f = ex.submit(_get_friendly_model_wmic)
|
|
211
|
-
serial_f
|
|
212
|
-
chassis_f
|
|
213
|
-
os_f
|
|
214
|
-
fw_f
|
|
229
|
+
serial_f = ex.submit(_get_serial_wmic)
|
|
230
|
+
chassis_f = ex.submit(_get_chassis_wmic)
|
|
231
|
+
os_f = ex.submit(_wmic, "os get BuildNumber")
|
|
232
|
+
fw_f = ex.submit(_wmic, "bios get SMBIOSBIOSVersion")
|
|
215
233
|
return {
|
|
216
|
-
"vendor":
|
|
217
|
-
"model":
|
|
218
|
-
"friendly_model":
|
|
219
|
-
"serial":
|
|
220
|
-
"chassis_type":
|
|
221
|
-
"os_build":
|
|
222
|
-
"arch":
|
|
234
|
+
"vendor": vendor_f.result() or "Unknown",
|
|
235
|
+
"model": model_f.result() or "Unknown",
|
|
236
|
+
"friendly_model": friendly_f.result(),
|
|
237
|
+
"serial": serial_f.result(),
|
|
238
|
+
"chassis_type": chassis_f.result(),
|
|
239
|
+
"os_build": _safe_int(os_f.result()),
|
|
240
|
+
"arch": "x64",
|
|
223
241
|
"firmware_version": fw_f.result() or "",
|
|
224
242
|
}
|
|
225
243
|
|
|
@@ -228,16 +246,23 @@ def _installed_versions_wmic(cfg: dict) -> dict[str, str]:
|
|
|
228
246
|
versions: dict[str, str] = {}
|
|
229
247
|
try:
|
|
230
248
|
r = proc.run(
|
|
231
|
-
[
|
|
232
|
-
|
|
233
|
-
|
|
249
|
+
[
|
|
250
|
+
"wmic",
|
|
251
|
+
"path",
|
|
252
|
+
"Win32_PnPSignedDriver",
|
|
253
|
+
"get",
|
|
254
|
+
"HardWareID,DriverVersion",
|
|
255
|
+
"/format:csv",
|
|
256
|
+
],
|
|
257
|
+
capture_output=True,
|
|
258
|
+
text=True,
|
|
234
259
|
timeout=cfg.get("timeout_wmi_slow", 90),
|
|
235
260
|
)
|
|
236
261
|
for line in r.stdout.splitlines()[1:]:
|
|
237
262
|
parts = line.strip().split(",")
|
|
238
263
|
if len(parts) >= 3:
|
|
239
264
|
hwid = parts[1].strip().upper()
|
|
240
|
-
ver
|
|
265
|
+
ver = parts[2].strip()
|
|
241
266
|
if hwid and ver:
|
|
242
267
|
versions[hwid] = ver
|
|
243
268
|
except Exception as e:
|
|
@@ -267,8 +292,9 @@ def _wmic(query: str) -> str:
|
|
|
267
292
|
cfg = get()
|
|
268
293
|
try:
|
|
269
294
|
r = proc.run(
|
|
270
|
-
["wmic"
|
|
271
|
-
capture_output=True,
|
|
295
|
+
["wmic", *query.split(), "/format:value"],
|
|
296
|
+
capture_output=True,
|
|
297
|
+
text=True,
|
|
272
298
|
timeout=cfg.get("timeout_tiny", 10),
|
|
273
299
|
)
|
|
274
300
|
for line in r.stdout.splitlines():
|
|
@@ -283,7 +309,7 @@ def _wmic(query: str) -> str:
|
|
|
283
309
|
|
|
284
310
|
# ── Chassis parsing ────────────────────────────────────────────────────────────
|
|
285
311
|
|
|
286
|
-
_LAPTOP_CHASSIS
|
|
312
|
+
_LAPTOP_CHASSIS = {8, 9, 10, 11, 12, 14, 30, 31, 32}
|
|
287
313
|
_DESKTOP_CHASSIS = {3, 4, 5, 6, 7, 13, 15, 16, 35, 36}
|
|
288
314
|
|
|
289
315
|
|
|
@@ -292,7 +318,7 @@ def _parse_chassis(raw: str) -> str:
|
|
|
292
318
|
try:
|
|
293
319
|
cleaned = raw.replace("{", "").replace("}", "").replace(" ", "")
|
|
294
320
|
nums = {int(x) for x in cleaned.split(",") if x.strip().isdigit()}
|
|
295
|
-
if any(n in _LAPTOP_CHASSIS
|
|
321
|
+
if any(n in _LAPTOP_CHASSIS for n in nums):
|
|
296
322
|
return "laptop"
|
|
297
323
|
if any(n in _DESKTOP_CHASSIS for n in nums):
|
|
298
324
|
return "desktop"
|
|
@@ -303,8 +329,16 @@ def _parse_chassis(raw: str) -> str:
|
|
|
303
329
|
|
|
304
330
|
# ── Shared utilities ───────────────────────────────────────────────────────────
|
|
305
331
|
|
|
306
|
-
_JUNK_VALUES = {
|
|
307
|
-
|
|
332
|
+
_JUNK_VALUES = {
|
|
333
|
+
"",
|
|
334
|
+
"none",
|
|
335
|
+
"unknown",
|
|
336
|
+
"to be filled by o.e.m.",
|
|
337
|
+
"not specified",
|
|
338
|
+
"name",
|
|
339
|
+
"default string",
|
|
340
|
+
"system product name",
|
|
341
|
+
}
|
|
308
342
|
|
|
309
343
|
|
|
310
344
|
def _is_meaningful(val: str) -> bool:
|