gates / gates (push) Successful in 6m34s
- 15-media-library.md §10: what the box does on each drive event, per app, with the measured call; the guards that ARE needed and the ones that are NOT (K1: nothing lost while a drive is gone). - Evidence K4 (dead-drive check: mountinfo `shutdown`, stat unreliable), K5 (restart), K6 (unclaimed Plex), K7 (old Sonarr/Radarr repair on the bench), K8 (hub received the drive-return events and mailed none), K9 (files identical). - nightly-rotation.md runs scripts/https_only_check.py first every night; scripts/CHANGELOG.md entry. - STATUS, CONTEXT (decisions 244-246), REPORT-night-2026-10-14.md. Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
31 lines
1.7 KiB
Python
31 lines
1.7 KiB
Python
#!/usr/bin/env python3
|
|
"""k4.py [label] — K4: tell a LIVE drive from a DEAD one (mountpoint yes, reads I/O error), run INSIDE the guest.
|
|
|
|
Two checks per /mnt/felhom-drives/<name>:
|
|
A. NO disk I/O (the controller's rule: health checks issue no block I/O — gates.md): /proc/self/mountinfo only.
|
|
How many mounts are stacked on the path, and the TOP one's super options (a dead ext4 says `shutdown`;
|
|
the stacked dead one under a returned drive is listed too).
|
|
B. A timed read in a SEPARATE process: `stat` of <path>/userdata with a 3 s timeout — the caller never blocks; the
|
|
result and the time it took.
|
|
"""
|
|
import os, subprocess, sys, time
|
|
|
|
label = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
mi = open("/proc/self/mountinfo").read().splitlines()
|
|
print(f"=== {label} {time.strftime('%H:%M:%S')}")
|
|
for d in sorted(os.listdir("/mnt/felhom-drives")):
|
|
p = f"/mnt/felhom-drives/{d}"
|
|
rows = [l for l in mi if l.split()[4] == p]
|
|
top = rows[-1] if rows else ""
|
|
sup = top.split(" - ", 1)[1].split()[2] if " - " in top else "-"
|
|
t0 = time.time()
|
|
try:
|
|
r = subprocess.run(["timeout", "3", "stat", "-c", "%i", f"{p}/userdata"], capture_output=True, text=True)
|
|
res = "ok" if r.returncode == 0 else ("TIMEOUT" if r.returncode == 124 else "ERR " + r.stderr.strip().split(":")[-1].strip())
|
|
except Exception as e:
|
|
res = f"EXC {e}"
|
|
dt = (time.time() - t0) * 1000
|
|
shut = "shutdown" in sup.split(",")
|
|
verdict = "DEAD" if (shut or res.startswith("ERR")) else ("absent" if not rows else "live")
|
|
print(f" {d:10} A: mounts stacked={len(rows)} top-super={sup[:40]:40} shutdown={shut} | B: stat {res} in {dt:.0f} ms | => {verdict}")
|