Add opt-in cmpunlocker unlock step with post-unlock Gen2 x16 acceptance

Insert a "cmpunlocker unlock (install)" step after unlock verification:
y/N confirmation (default n) or --unlock runs install.sh from the
harness clone (main branch, ff-only pull first) with live terminal
output and full log to cards/<serial>/install_unlocker.log. On success
the Vulkan stress is skipped until after the cold reboot.

Score now enforces the post-unlock invariant: when verify reports
unlocked-ok, any final link other than Gen2 x16 fails with a red
[ERROR] and exit 1. README safety section and AGENTS.md harness
cloning invariant updated to match.
This commit is contained in:
2026-09-28 15:39:10 +02:00
parent a84aa494d0
commit 7f3cfcf058
4 changed files with 220 additions and 27 deletions
+136 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""CMP 170HX checker - shared library (preflight, detection, PCIe checks, BIOS, verify, Vulkan)."""
"""CMP 170HX checker - shared library (preflight, detection, PCIe checks, BIOS, verify, unlock install, Vulkan)."""
import datetime
import hashlib
import os
@@ -7,6 +7,7 @@ import re
import shutil
import subprocess
import sys
import threading
import time
from pathlib import Path
@@ -19,6 +20,7 @@ REPOS = {
"cmpunlocker": "https://github.com/amoghmunikote/cmpunlocker.git",
}
VERIFY_SH = HARNESS_DIR / "cmpunlocker" / "verify.sh"
INSTALL_SH = HARNESS_DIR / "cmpunlocker" / "install.sh"
GPU_IDS = ("20c2", "2082")
VULKAN_TEST_SECONDS = 300
@@ -76,9 +78,9 @@ def sanitize(name):
# ---------------------------------------------------------------- subprocess
def run(cmd, timeout=60, env=None):
def run(cmd, timeout=60, env=None, cwd=None):
try:
p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, env=env)
p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, env=env, cwd=cwd)
return {"rc": p.returncode, "out": p.stdout, "err": p.stderr, "cmd": cmd}
except FileNotFoundError:
return {"rc": 127, "out": "", "err": f"not found: {cmd[0]}", "cmd": cmd}
@@ -87,11 +89,76 @@ def run(cmd, timeout=60, env=None):
return {"rc": 124, "out": out, "err": "timeout", "cmd": cmd}
def as_root(cmd, timeout=60):
def run_live(cmd, cwd=None, timeout=600, log_path=None):
"""Run cmd while echoing every output line to the terminal as it arrives.
The complete output is also written to log_path (operator keeps the full log,
the terminal keeps the progress). stderr is merged into stdout, so out holds
everything. Returns the same {rc, out, err, cmd} shape as run(): rc 127 =
not found, rc 124 = timeout.
"""
try:
p = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, bufsize=1)
except FileNotFoundError:
return {"rc": 127, "out": "", "err": f"not found: {cmd[0]}", "cmd": cmd}
except OSError as e:
return {"rc": 126, "out": "", "err": str(e), "cmd": cmd}
lines, done = [], threading.Event()
def _pump():
try:
for line in p.stdout or "":
lines.append(line)
print(line, end="", flush=True)
except (ValueError, OSError):
pass
done.set()
pump = threading.Thread(target=_pump, daemon=True)
pump.start()
timed_out = interrupted = False
try:
finished = done.wait(timeout)
except KeyboardInterrupt:
finished, interrupted = True, True
if not finished:
timed_out = True
if timed_out or interrupted:
try:
p.kill()
except OSError:
pass
pump.join(timeout=15)
rc = p.wait()
out = "".join(lines)
err = ""
if timed_out:
rc, err = 124, "timeout"
elif interrupted:
err = "interrupted"
if log_path:
path = Path(log_path)
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(f"$ {' '.join(str(c) for c in cmd)}\n"
f"cwd: {cwd or os.getcwd()}\n"
f"{out}\nrc={rc}{f' ({err})' if err else ''}\n")
except OSError as e:
warn(f"could not write log {path}: {e}")
if interrupted:
raise KeyboardInterrupt
return {"rc": rc, "out": out, "err": err, "cmd": cmd}
def as_root(cmd, timeout=60, cwd=None):
if os.geteuid() == 0:
return run(cmd, timeout=timeout)
return run(cmd, timeout=timeout, cwd=cwd)
if shutil.which("sudo"):
r = run(["sudo", "-n"] + cmd, timeout=timeout)
r = run(["sudo", "-n"] + cmd, timeout=timeout, cwd=cwd)
if r["rc"] != 0 and "password is required" in (r["err"] + r["out"]).lower():
r["err"] = "sudo requires a password (sudo -n failed)"
return r
@@ -644,6 +711,69 @@ def run_verify_unlocker():
return {"ran": True, "rc": r["rc"], "status": status, "tail": lines[-30:]}
# ---------------------------------------------------------------- unlock (opt-in install)
def update_cmpunlocker():
"""Bring harness/cmpunlocker up to date with upstream main before installing.
A failed `git pull --ff-only` (diverged checkout, offline host, ...) is not
fatal: we warn, keep the existing checkout, and let the install proceed.
"""
dest = HARNESS_DIR / "cmpunlocker"
if not (dest / ".git").exists():
info("harness/cmpunlocker missing - cloning ...")
if not ensure_repos():
return {"ok": False, "note": "harness/cmpunlocker not cloned and git clone failed"}
return {"ok": True, "note": "harness/cmpunlocker cloned"}
r = run(["git", "-C", str(dest), "pull", "--ff-only"], timeout=180)
if r["rc"] != 0:
msg = (r["err"] or r["out"]).strip().splitlines()
msg = msg[-1] if msg else f"rc={r['rc']}"
warn(f"git pull --ff-only failed: {msg} - continuing with the existing checkout")
return {"ok": False, "note": f"git pull --ff-only failed ({msg}) - using existing checkout"}
last = (r["out"].strip().splitlines() or ["already up to date"])[-1]
ok(f"cmpunlocker harness updated: {last}")
return {"ok": True, "note": f"updated: {last}"}
def run_installer(log_path):
"""Run install.sh from the harness clone (opt-in, root required).
Output streams live to the terminal and is fully saved to log_path.
Returns {"attempted", "ok", "rc", "note", "log"}; attempted=False when a
precondition (clone present, sudo usable) fails.
"""
log_path = Path(log_path)
result = {"attempted": False, "ok": False, "rc": None, "note": "", "log": str(log_path)}
if not INSTALL_SH.exists():
result["note"] = "harness/cmpunlocker not cloned"
warn(f"{result['note']} - run --preflight first")
return result
ok_sudo, why = sudo_precondition()
if not ok_sudo:
result["note"] = f"sudo precondition failed: {why}"
warn(result["note"])
return result
cmd = ["bash", str(INSTALL_SH)]
if os.geteuid() != 0:
cmd = ["sudo", "-n"] + cmd # credentials cached by sudo_precondition()
info(f"running install.sh in {INSTALL_SH.parent} - kernel module build takes 5-15 min, "
f"live output -> {log_path}")
r = run_live(cmd, cwd=str(INSTALL_SH.parent), timeout=1800, log_path=log_path)
result.update(attempted=True, rc=r["rc"])
if r["rc"] == 0:
result.update(ok=True, note="install.sh completed")
ok("cmpunlocker installed - cold reboot required: sudo shutdown -h now (then power on), "
"then re-run this script for the final PCIe check")
else:
tail = (r["err"] or r["out"]).strip().splitlines()
tail = tail[-1] if tail else ""
result["note"] = f"install.sh failed rc={r['rc']}" + (f": {tail}" if tail else "")
warn(f"{result['note']} (see {log_path})")
return result
# ---------------------------------------------------------------- Vulkan test
def vulkan_tool():