Files
felhom.eu/documentation/audits/media-drive-events-2026-10-14/tools/k4.py
T
admin 0e88b7eb99
gates / gates (push) Successful in 6m34s
Media drive events, night 2026-10-14: 15 §10 (event x app, measured), K4-K8 evidence, rows R-1044..R-1049, narrowed R-1022/1023/1024/1026/1028/1037/1038, night report
- 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
2026-10-14 23:40:26 +02:00

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}")