driverclient 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,679 @@
1
+ """
2
+ client/ops/capture.py — Three capture operations for the Driver Server client.
3
+
4
+ capture_all() Dump every package in DriverStore to the repo.
5
+ Server deduplicates by SHA. No resolve needed.
6
+
7
+ capture_missing() Scan + resolve to find HWIDs the repo lacks, then export
8
+ any of those that are already installed locally and submit
9
+ them. No Windows Update.
10
+
11
+ wu_update() Scan + resolve for not-found HWIDs, trigger Windows Update,
12
+ poll incrementally (submit each new batch as it appears),
13
+ stop after wu_wait_minutes or 2 empty polls.
14
+
15
+ Upload pipeline (all three operations):
16
+ export pool (parallel_exports workers) — pnputil /export-driver + pack
17
+ ↓ queue
18
+ upload pool (parallel_uploads workers) — POST /submit (8 workers default)
19
+
20
+ Public API:
21
+ capture_all() -> CaptureResult
22
+ capture_missing(force=False) -> CaptureResult
23
+ wu_update(force=False) -> CaptureResult
24
+ """
25
+ import queue
26
+ import shutil
27
+ import subprocess
28
+ import tempfile
29
+ import threading
30
+ import time
31
+ from concurrent.futures import ThreadPoolExecutor
32
+ from dataclasses import dataclass, field
33
+ from datetime import datetime, timedelta, timezone
34
+ from pathlib import Path
35
+
36
+ import functools
37
+
38
+ from driverclient import events
39
+ from driverclient.config import get
40
+ from driverclient.core.hardware import compute_fingerprint, get_model_profile
41
+ from driverclient.core.http import BadRequestError, http_post_retry, http_upload_package, pack_driver_archive
42
+ from driverclient.ops.resolve import ResolveResult, resolve
43
+ from driverclient.ops.scan import Device, ScanResult, scan
44
+
45
+
46
+ def _post_wu_session(cfg: dict, wu_start, hwids_tried: list, captured: int) -> None:
47
+ """Report a Windows Update session so server WU analytics has data.
48
+
49
+ Best-effort — never blocks or raises into the capture flow.
50
+ """
51
+ try:
52
+ duration = max(0, int((datetime.now(timezone.utc) - wu_start).total_seconds()))
53
+ http_post_retry(
54
+ f"{cfg['local_repo_url']}/submit/wu-session",
55
+ {
56
+ "machine_id": _machine_meta().get("machine_id", ""),
57
+ "ran_at": wu_start.isoformat(),
58
+ "hwids_tried": hwids_tried,
59
+ "wu_installed_count": captured,
60
+ "captured_count": captured,
61
+ "duration_seconds": duration,
62
+ },
63
+ timeout=cfg.get("timeout_short", 30),
64
+ )
65
+ except Exception as e:
66
+ events.warn("windows_update", f" [warn] wu-session report failed: {e}",
67
+ error=str(e))
68
+
69
+
70
+ @functools.lru_cache(maxsize=1)
71
+ def _machine_meta() -> dict:
72
+ """Detect this machine's vendor/model/serial/chassis + fingerprint once.
73
+
74
+ Threaded through every capture upload so the server can populate
75
+ submitter_* fields — otherwise the inventory shows model as 'unknown'.
76
+ """
77
+ try:
78
+ prof = get_model_profile()
79
+ except Exception:
80
+ prof = {}
81
+ try:
82
+ machine_id = compute_fingerprint()
83
+ except Exception:
84
+ machine_id = ""
85
+ return {
86
+ "vendor": prof.get("vendor", ""),
87
+ "model": prof.get("model", ""),
88
+ "friendly_model": prof.get("friendly_model", ""),
89
+ "serial": prof.get("serial", ""),
90
+ "chassis": prof.get("chassis_type", ""),
91
+ "machine_id": machine_id,
92
+ }
93
+
94
+ _WINDOWS_INF = Path("C:\\Windows\\INF")
95
+
96
+ _PNPUTIL_KEYS: dict[str, str] = {
97
+ "published name": "published_name",
98
+ "original name": "original_name",
99
+ "provider name": "provider_name",
100
+ "class name": "class_name",
101
+ "driver version": "driver_version",
102
+ }
103
+
104
+
105
+ @dataclass
106
+ class CaptureResult:
107
+ submitted: int = 0
108
+ skipped: int = 0
109
+ failed: int = 0
110
+ still_missing: int = 0
111
+ wu_triggered: bool = False
112
+
113
+ def merge(self, other: "CaptureResult") -> None:
114
+ self.submitted += other.submitted
115
+ self.skipped += other.skipped
116
+ self.failed += other.failed
117
+
118
+
119
+ # ══ Public operations ══════════════════════════════════════════════════════════
120
+
121
+ def capture_all() -> CaptureResult:
122
+ """Export every DriverStore package and submit. Server deduplicates."""
123
+ cfg = get()
124
+ events.start("capture", "[capture-all] Enumerating DriverStore packages…")
125
+ packages = _enumerate_all_packages(cfg)
126
+ if not packages:
127
+ events.done("capture", "[capture-all] No packages found", total=0)
128
+ return CaptureResult()
129
+ events.progress("capture",
130
+ f"[capture-all] {len(packages)} package(s) — exporting + submitting…",
131
+ total=len(packages))
132
+ result = _export_and_submit(packages, cfg)
133
+ events.done("capture",
134
+ f"[capture-all] Done — {result.submitted} submitted "
135
+ f"{result.skipped} skipped {result.failed} failed",
136
+ total=len(packages), submitted=result.submitted,
137
+ skipped=result.skipped, failed=result.failed)
138
+ return result
139
+
140
+
141
+ def capture_missing(force: bool = False) -> CaptureResult:
142
+ """
143
+ Find HWIDs the repo has no package for, export + submit any that are
144
+ already installed locally. No Windows Update.
145
+ """
146
+ cfg = get()
147
+ scan_result = scan(force=force)
148
+ res_result = resolve(force=force)
149
+
150
+ if not res_result.has_missing:
151
+ events.done("capture",
152
+ "[capture-missing] Repo is complete for this machine — nothing to submit",
153
+ total=0)
154
+ return CaptureResult()
155
+
156
+ not_found_set = set(res_result.not_found)
157
+ events.progress("capture",
158
+ f"[capture-missing] {len(not_found_set)} HWID(s) missing from repo",
159
+ total=len(not_found_set))
160
+
161
+ to_export = _find_installed_packages_for_hwids(not_found_set, scan_result, cfg)
162
+ if not to_export:
163
+ events.done("capture",
164
+ "[capture-missing] None of the missing HWIDs have locally-installed drivers",
165
+ total=0, still_missing=len(not_found_set))
166
+ return CaptureResult(still_missing=len(not_found_set))
167
+
168
+ events.progress("capture",
169
+ f"[capture-missing] Exporting {len(to_export)} locally-installed package(s)…",
170
+ total=len(to_export))
171
+ result = _export_and_submit(to_export, cfg)
172
+ result.still_missing = max(0, len(not_found_set) - result.submitted)
173
+ events.done("capture",
174
+ f"[capture-missing] Done — {result.submitted} submitted "
175
+ f"{result.skipped} skipped {result.failed} failed",
176
+ total=len(to_export), submitted=result.submitted,
177
+ skipped=result.skipped, failed=result.failed,
178
+ still_missing=result.still_missing)
179
+ return result
180
+
181
+
182
+ def wu_update(force: bool = False) -> CaptureResult:
183
+ """
184
+ Scan → resolve → filter not-found HWIDs to those with no local driver
185
+ and not in wu_ignore_classes → trigger WU → poll → submit new packages.
186
+
187
+ Gate logic (applied before WU is triggered):
188
+ has_driver=True → skip (driver on machine; capture_missing handles repo upload)
189
+ class in ignore → skip (wu_ignore_classes config)
190
+ not in scan → include (unknown device — safer to attempt)
191
+ """
192
+ cfg = get()
193
+ scan_result = scan(force=force)
194
+ res_result = resolve(force=force)
195
+
196
+ if not res_result.has_missing:
197
+ events.done("windows_update", "[wu-update] No missing HWIDs — nothing to trigger WU for",
198
+ total=0)
199
+ return CaptureResult()
200
+
201
+ wu_hwids = _filter_wu_hwids(res_result.not_found, scan_result, cfg)
202
+ skipped = len(res_result.not_found) - len(wu_hwids)
203
+
204
+ if not wu_hwids:
205
+ events.done("windows_update",
206
+ "[wu-update] All missing HWIDs have local drivers or are ignored — skipping WU",
207
+ total=0, still_missing=len(res_result.not_found))
208
+ return CaptureResult(still_missing=len(res_result.not_found))
209
+
210
+ if skipped:
211
+ events.start("windows_update",
212
+ f"[wu-update] {len(wu_hwids)} HWID(s) need WU "
213
+ f"({skipped} skipped — has local driver or ignored class)",
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))
219
+
220
+ if not _trigger_wu():
221
+ return CaptureResult(still_missing=len(wu_hwids))
222
+
223
+ wu_start = datetime.now(timezone.utc)
224
+ total = _poll_and_submit_incremental(wu_start, cfg)
225
+ total.wu_triggered = True
226
+ total.still_missing = max(0, len(wu_hwids) - total.submitted)
227
+ _post_wu_session(cfg, wu_start, list(wu_hwids), total.submitted)
228
+ events.done("windows_update",
229
+ f"[wu-update] Done — {total.submitted} captured "
230
+ f"{total.still_missing} still missing",
231
+ submitted=total.submitted, still_missing=total.still_missing)
232
+ return total
233
+
234
+
235
+ def wu_full() -> CaptureResult:
236
+ """
237
+ Trigger a full Windows Update scan with no HWID filtering.
238
+ Captures ALL packages WU installs and submits them to the repo.
239
+ Use to harvest WU drivers onto the repo regardless of current repo state.
240
+ No scan or resolve needed.
241
+ """
242
+ cfg = get()
243
+ events.start("windows_update",
244
+ "[wu-full] Triggering full Windows Update scan (no HWID filtering)…")
245
+
246
+ if not _trigger_wu():
247
+ return CaptureResult()
248
+
249
+ wu_start = datetime.now(timezone.utc)
250
+ total = _poll_and_submit_incremental(wu_start, cfg)
251
+ total.wu_triggered = True
252
+ _post_wu_session(cfg, wu_start, [], total.submitted)
253
+ events.done("windows_update",
254
+ f"[wu-full] Done — {total.submitted} submitted "
255
+ f"{total.skipped} skipped {total.failed} failed",
256
+ submitted=total.submitted, skipped=total.skipped, failed=total.failed)
257
+ return total
258
+
259
+
260
+ # ══ WU HWID filtering ═════════════════════════════════════════════════════════
261
+
262
+ def _filter_wu_hwids(
263
+ not_found: list[str],
264
+ scan_result: ScanResult,
265
+ cfg: dict,
266
+ ) -> list[str]:
267
+ """
268
+ Return only the not_found HWIDs that genuinely need Windows Update:
269
+ - Device has no driver installed locally (has_driver=False)
270
+ - Device class is not in wu_ignore_classes config
271
+ - If HWID is not in scan at all → include (unknown device, safer to try)
272
+ """
273
+ ignore_classes: set[str] = {
274
+ c.lower() for c in cfg.get("wu_ignore_classes", [])
275
+ }
276
+
277
+ hwid_to_device: dict[str, Device] = {}
278
+ for dev in scan_result.devices:
279
+ for hwid in dev.hwids:
280
+ hwid_to_device[hwid.upper()] = dev
281
+
282
+ result = []
283
+ for hwid in not_found:
284
+ dev = hwid_to_device.get(hwid.upper())
285
+ if dev is None:
286
+ 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
+
294
+ return result
295
+
296
+
297
+ # ══ Package enumeration ════════════════════════════════════════════════════════
298
+
299
+ def _enumerate_all_packages(cfg: dict) -> list[dict]:
300
+ try:
301
+ r = subprocess.run(
302
+ ["pnputil", "/enum-drivers"],
303
+ capture_output=True, text=True,
304
+ timeout=cfg.get("timeout_short", 30),
305
+ )
306
+ except FileNotFoundError:
307
+ events.error("capture", "[capture] pnputil not found — must run on Windows",
308
+ error="pnputil_not_found")
309
+ return []
310
+ except Exception as e:
311
+ events.error("capture", f"[capture] pnputil failed: {e}", error=str(e))
312
+ return []
313
+ return _parse_enum_drivers(r.stdout)
314
+
315
+
316
+ def _parse_enum_drivers(output: str) -> list[dict]:
317
+ packages: list[dict] = []
318
+ current: dict = {}
319
+ for line in output.splitlines():
320
+ stripped = line.strip()
321
+ if not stripped:
322
+ if current.get("published_name"):
323
+ packages.append(dict(current))
324
+ current = {}
325
+ elif ":" in stripped:
326
+ raw_key, _, val = stripped.partition(":")
327
+ mapped = _PNPUTIL_KEYS.get(raw_key.strip().lower())
328
+ if mapped:
329
+ current[mapped] = val.strip()
330
+ if current.get("published_name"):
331
+ packages.append(current)
332
+ return packages
333
+
334
+
335
+ def _enumerate_packages_since(cutoff: datetime, cfg: dict) -> list[dict]:
336
+ """Return packages whose C:\\Windows\\INF mtime >= cutoff."""
337
+ result = []
338
+ for pkg in _enumerate_all_packages(cfg):
339
+ inf_path = _WINDOWS_INF / pkg["published_name"]
340
+ if not inf_path.exists():
341
+ continue
342
+ mtime = datetime.fromtimestamp(inf_path.stat().st_mtime, tz=timezone.utc)
343
+ if mtime >= cutoff:
344
+ result.append(pkg)
345
+ return result
346
+
347
+
348
+ def _find_installed_packages_for_hwids(
349
+ not_found_hwids: set[str],
350
+ scan_result: ScanResult,
351
+ cfg: dict,
352
+ ) -> list[dict]:
353
+ locally_installed = {
354
+ hwid for hwid in not_found_hwids
355
+ if hwid in scan_result.installed_versions
356
+ }
357
+ if not locally_installed:
358
+ return []
359
+
360
+ hwid_to_published = _map_hwids_to_published(locally_installed, cfg)
361
+ all_packages = {
362
+ p["published_name"]: p
363
+ for p in _enumerate_all_packages(cfg)
364
+ if p.get("published_name")
365
+ }
366
+ return [
367
+ all_packages[pub]
368
+ for pub in set(hwid_to_published.values())
369
+ if pub in all_packages
370
+ ]
371
+
372
+
373
+ def _map_hwids_to_published(hwids: set[str], cfg: dict) -> dict[str, str]:
374
+ mapping: dict[str, str] = {}
375
+ try:
376
+ r = subprocess.run(
377
+ ["pnputil", "/enum-devices", "/drivers"],
378
+ capture_output=True, text=True,
379
+ timeout=cfg.get("timeout_short", 30),
380
+ )
381
+ except Exception:
382
+ return mapping
383
+
384
+ state: dict = {"published": "", "hwids": [], "in_hwids": False}
385
+ for line in r.stdout.splitlines():
386
+ stripped = line.strip()
387
+ if not stripped:
388
+ _commit_hwid_block(state, hwids, mapping)
389
+ else:
390
+ _parse_hwid_line(line, stripped, state)
391
+
392
+ return mapping
393
+
394
+
395
+ def _commit_hwid_block(state: dict, hwids: set[str], mapping: dict) -> None:
396
+ """Flush accumulated HWIDs for the current device into mapping."""
397
+ if state["published"]:
398
+ for h in state["hwids"]:
399
+ if h in hwids:
400
+ mapping[h] = state["published"]
401
+ state["published"] = ""
402
+ state["hwids"] = []
403
+ state["in_hwids"] = False
404
+
405
+
406
+ def _parse_hwid_line(line: str, stripped: str, state: dict) -> None:
407
+ """Update parser state from one non-empty pnputil /enum-devices line."""
408
+ low = stripped.lower()
409
+ if low.startswith("published name") and ":" in stripped:
410
+ state["published"] = stripped.split(":", 1)[1].strip()
411
+ state["in_hwids"] = False
412
+ elif low.startswith(("hardware ids", "compatible ids")):
413
+ state["in_hwids"] = True
414
+ elif state["in_hwids"] and "\\" in stripped and not stripped.endswith(":"):
415
+ state["hwids"].append(stripped.upper())
416
+ elif ":" in stripped and not line.startswith(" "):
417
+ state["in_hwids"] = False
418
+
419
+
420
+ # ══ Windows Update ═════════════════════════════════════════════════════════════
421
+
422
+ def _trigger_wu() -> bool:
423
+ for cmd in (["usoclient", "StartScan"], ["wuauclt", "/detectnow"]):
424
+ try:
425
+ subprocess.run(cmd, capture_output=True, timeout=30)
426
+ events.ok("windows_update", "[wu-update] Windows Update scan triggered")
427
+ return True
428
+ except Exception:
429
+ continue
430
+ events.error("windows_update", "[wu-update] Could not trigger Windows Update",
431
+ error="trigger_failed")
432
+ return False
433
+
434
+
435
+ def _poll_and_submit_incremental(wu_start: datetime, cfg: dict) -> CaptureResult:
436
+ """
437
+ Poll every wu_poll_interval_seconds for new DriverStore packages.
438
+ Submit each new batch immediately. Stop after wu_wait_minutes total
439
+ OR after 2 consecutive polls with no new packages.
440
+ """
441
+ wait_min = cfg.get("wu_wait_minutes", 10)
442
+ poll_secs = cfg.get("wu_poll_interval_seconds", 30)
443
+ deadline = wu_start + timedelta(minutes=wait_min)
444
+ seen:set[str] = set()
445
+ total = CaptureResult()
446
+ empty_polls = 0
447
+
448
+ events.progress("windows_update",
449
+ f"[wu-update] Polling every {poll_secs}s, max {wait_min} min…")
450
+
451
+ while datetime.now(timezone.utc) < deadline:
452
+ all_new = _enumerate_packages_since(wu_start, cfg)
453
+ new_batch = [
454
+ p for p in all_new
455
+ if p.get("published_name", "") not in seen
456
+ ]
457
+
458
+ if new_batch:
459
+ empty_polls = 0
460
+ for p in new_batch:
461
+ seen.add(p.get("published_name", ""))
462
+ events.progress("windows_update",
463
+ f"[wu-update] {len(new_batch)} new package(s) — submitting…",
464
+ current=len(new_batch))
465
+ total.merge(_export_and_submit(new_batch, cfg))
466
+ else:
467
+ empty_polls += 1
468
+ if empty_polls >= 2:
469
+ events.progress("windows_update",
470
+ "[wu-update] 2 consecutive empty polls — stopping")
471
+ break
472
+ remaining = int((deadline - datetime.now(timezone.utc)).total_seconds())
473
+ events.progress("windows_update",
474
+ f"[wu-update] no new drivers — {remaining}s remaining…")
475
+
476
+ time.sleep(poll_secs)
477
+
478
+ return total
479
+
480
+
481
+ # ══ Export pool → Upload pool pipeline ════════════════════════════════════════
482
+
483
+ def _export_and_submit(packages: list[dict], cfg: dict) -> CaptureResult:
484
+ """
485
+ Two-stage pipeline:
486
+ export pool (parallel_exports) → pnputil /export-driver + pack
487
+ upload pool (parallel_uploads) → POST /submit
488
+
489
+ Deduplicates by published_name before starting.
490
+ """
491
+ parallel_exports = cfg.get("parallel_exports", 4)
492
+ parallel_uploads = cfg.get("parallel_uploads", 8)
493
+
494
+ # Dedup by published_name
495
+ seen: set[str] = set()
496
+ unique: list[dict] = []
497
+ for pkg in packages:
498
+ name = pkg.get("published_name", "")
499
+ if name and name not in seen:
500
+ seen.add(name)
501
+ unique.append(pkg)
502
+
503
+ if not unique:
504
+ return CaptureResult()
505
+
506
+ counts = {"submitted": 0, "skipped": 0, "failed": 0}
507
+ lock = threading.Lock()
508
+ upload_q: queue.Queue = queue.Queue(maxsize=parallel_uploads * 2)
509
+
510
+ # Start upload workers
511
+ upload_threads = [
512
+ threading.Thread(target=_upload_worker, args=(upload_q, counts, lock, cfg),
513
+ daemon=True)
514
+ for _ in range(parallel_uploads)
515
+ ]
516
+ for t in upload_threads:
517
+ t.start()
518
+
519
+ # Run export pool — each worker exports + packs then puts payload into upload_q
520
+ with ThreadPoolExecutor(max_workers=parallel_exports) as ex:
521
+ futures = [ex.submit(_export_one, pkg, upload_q, counts, lock, cfg)
522
+ for pkg in unique]
523
+ for f in futures:
524
+ try:
525
+ f.result()
526
+ except Exception as e:
527
+ events.error("dump", f" [error] export: {e}", error=str(e))
528
+ with lock:
529
+ counts["failed"] += 1
530
+
531
+ # Signal upload workers to stop
532
+ for _ in upload_threads:
533
+ upload_q.put(None)
534
+ upload_q.join()
535
+
536
+ return CaptureResult(
537
+ submitted=counts["submitted"],
538
+ skipped=counts["skipped"],
539
+ failed=counts["failed"],
540
+ )
541
+
542
+
543
+ def _export_one(pkg: dict, upload_q: queue.Queue,
544
+ counts: dict, lock: threading.Lock, cfg: dict) -> None:
545
+ """Thin wrapper: export + pack via _do_export, then enqueue for upload."""
546
+ published = pkg["published_name"]
547
+ class_name = pkg.get("class_name", "").lower()
548
+ events.progress("dump",
549
+ f" [export] {published} ({pkg.get('provider_name', '?')} {class_name})",
550
+ published_name=published, provider=pkg.get("provider_name", "?"),
551
+ class_name=class_name)
552
+
553
+ payload = _do_export(pkg, cfg)
554
+ if payload is None:
555
+ with lock:
556
+ counts["failed"] += 1
557
+ return
558
+ upload_q.put(payload)
559
+
560
+
561
+ def _do_export(pkg: dict, cfg: dict) -> dict | None:
562
+ """
563
+ Run pnputil /export-driver, pack the files, return an upload payload dict.
564
+ TemporaryDirectory cleans up after packing completes — no double-export.
565
+ Returns None on any error.
566
+ """
567
+ published = pkg["published_name"]
568
+ original = pkg.get("original_name", published)
569
+ class_name = pkg.get("class_name", "").lower()
570
+ exts = _pick_extensions(class_name, cfg)
571
+ timeout = cfg.get("timeout_long", 120)
572
+
573
+ with tempfile.TemporaryDirectory() as tmp:
574
+ dest = Path(tmp) / "pkg"
575
+ dest.mkdir()
576
+
577
+ try:
578
+ r = subprocess.run(
579
+ ["pnputil", "/export-driver", published, str(dest)],
580
+ capture_output=True, text=True, timeout=timeout,
581
+ )
582
+ except subprocess.TimeoutExpired:
583
+ events.warn("dump", f" [warn] export timed out: {published}",
584
+ published_name=published, error="timeout")
585
+ return None
586
+ except Exception as e:
587
+ events.warn("dump", f" [warn] export error {published}: {e}",
588
+ published_name=published, error=str(e))
589
+ return None
590
+
591
+ if r.returncode != 0:
592
+ events.warn("dump", f" [warn] export failed {published}: {r.stderr[:120]}",
593
+ published_name=published, error=r.stderr[:120])
594
+ return None
595
+
596
+ try:
597
+ archive_bytes, archive_name, encoding = pack_driver_archive(
598
+ dest, Path(original).stem, exts
599
+ )
600
+ except Exception as e:
601
+ events.warn("dump", f" [warn] pack error {published}: {e}",
602
+ published_name=published, error=str(e))
603
+ return None
604
+
605
+ return {
606
+ "published": published,
607
+ "original": original,
608
+ "provider": pkg.get("provider_name", "Unknown"),
609
+ "class_name": class_name,
610
+ "driver_version": pkg.get("driver_version", ""),
611
+ "archive_bytes": archive_bytes,
612
+ "archive_name": archive_name,
613
+ "encoding": encoding,
614
+ }
615
+
616
+
617
+ def _upload_worker(upload_q: queue.Queue, counts: dict,
618
+ lock: threading.Lock, cfg: dict) -> None:
619
+ """Upload worker: pulls payloads from queue and POSTs to /submit."""
620
+ repo_url = cfg["local_repo_url"]
621
+ node_key = cfg.get("node_key", "")
622
+ timeout = cfg.get("timeout_medium", 60)
623
+
624
+ while True:
625
+ item = upload_q.get()
626
+ if item is None:
627
+ upload_q.task_done()
628
+ return
629
+ try:
630
+ resp = http_upload_package(
631
+ url=f"{repo_url}/submit/upload",
632
+ node_key=node_key,
633
+ metadata={
634
+ "published_name": item["published"],
635
+ "original_name": item["original"],
636
+ "provider": item["provider"],
637
+ "driver_version": item["driver_version"],
638
+ "class_name": item["class_name"],
639
+ "source_type": "captured_live",
640
+ **_machine_meta(),
641
+ },
642
+ archive_bytes=item["archive_bytes"],
643
+ archive_name=item["archive_name"],
644
+ content_encoding=item["encoding"],
645
+ timeout=timeout,
646
+ )
647
+ # The client is done the instant the local repo acks. It does NOT
648
+ # interpret upstream queue/scan state — "accepted/queued/buffered"
649
+ # means handed off successfully; only explicit dedup statuses skip.
650
+ status = resp.get("status")
651
+ if status in ("already_approved", "already_present", "already_queued"):
652
+ key = "skipped"
653
+ else: # submitted / accepted / queued / buffered → handed off
654
+ key = "submitted"
655
+ with lock:
656
+ counts[key] += 1
657
+ except BadRequestError as e:
658
+ events.warn("upload",
659
+ f" [skip] {item.get('published', '?')} rejected by local_repo: {e}",
660
+ published_name=item.get("published", "?"), error=str(e))
661
+ with lock:
662
+ counts["failed"] += 1
663
+ except Exception as e:
664
+ events.error("upload", f" [error] upload {item.get('published', '?')}: {e}",
665
+ published_name=item.get("published", "?"), error=str(e))
666
+ with lock:
667
+ counts["failed"] += 1
668
+ finally:
669
+ upload_q.task_done()
670
+
671
+
672
+ # ══ Extension selection ════════════════════════════════════════════════════════
673
+
674
+ def _pick_extensions(class_name: str, cfg: dict) -> list[str]:
675
+ overrides: dict[str, list[str]] = cfg.get("dump_extensions_overrides", {})
676
+ for key, exts in overrides.items():
677
+ if key.lower() in class_name:
678
+ return exts
679
+ return cfg.get("dump_extensions", [".inf", ".sys", ".cat"])