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:
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user