93 lines
4.2 KiB
Python
93 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""r1292_live.py — R-1292 live on a box with a TESTED line (Tester 1; 9202 has none, so the rule never acts there):
|
|
a THROWAWAY qBittorrent; the household's „alternative speed" switched ON with an alternative limit of 500 KiB/s; then
|
|
the controller's 50 % rule (its 15-minute tick) must set the NORMAL limit (`up_limit`) and leave the alternative alone;
|
|
then the household switches back to normal speed, and the next tick must NOT read that as „the household changed it".
|
|
qBittorrent's own API from inside its container (the admin password rides an env var, never printed). Removed through
|
|
the product at the end (data and backups).
|
|
|
|
usage: SC=<dir with .ctlpw> SWEEP_EV=<evidence dir> TARGET=tester-1 DRIVE_ROOT=<drive> python3 r1292_live.py"""
|
|
import json, os, sys, time
|
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../catalog-sweep-2026-10-24/tools"))
|
|
import sweep as sw # noqa: E402
|
|
|
|
w = sw.w
|
|
APP = "qbittorrent"
|
|
|
|
|
|
def qb(path, data=None):
|
|
"""One qBittorrent API call inside its container, through /root/qbtool.sh in the guest (the password via env QP)."""
|
|
pw = (w.GENERATED.get(APP) or {}).get("QBT_ADMIN_PASSWORD", "")
|
|
arg = " '" + data + "'" if data else ""
|
|
return sw.g(f"QP='{pw}' sh /root/qbtool.sh {path}{arg}").strip()
|
|
|
|
|
|
def record():
|
|
"""The 50 % rule's record (settings.json upwatch.qb_limit), parsed — never a grep on a JSON file."""
|
|
raw = sw.g("docker exec felhom-controller cat /opt/docker/felhom-controller/data/settings.json")
|
|
try:
|
|
return (json.loads(raw).get("upwatch") or {}).get("qb_limit")
|
|
except ValueError:
|
|
return "unreadable"
|
|
|
|
|
|
def prefs():
|
|
try:
|
|
p = json.loads(qb("app/preferences"))
|
|
return {"up_limit": p.get("up_limit"), "alt_up_limit": p.get("alt_up_limit"), "mode_alt": qb("transfer/speedLimitsMode") == "1"}
|
|
except ValueError:
|
|
return {"error": "unreadable"}
|
|
|
|
|
|
def rule_lines(since):
|
|
return sw.g(f"docker logs --since {since} felhom-controller 2>&1 | grep -E 'qBittorrent (upload )?limit' | cut -c1-200").strip()
|
|
|
|
|
|
def wait_tick(since, say, label, cap=1100):
|
|
for _ in range(cap // 20):
|
|
time.sleep(20)
|
|
out = sw.g(f"docker logs --since {since} felhom-controller 2>&1 | grep -E 'upwatch' | grep -vE 'DEBUG' | tail -1")
|
|
# the 15-minute tick also logs the hold seeder; any upwatch line after `since` with a qBittorrent verdict counts
|
|
ln = rule_lines(since)
|
|
if ln:
|
|
break
|
|
st = record()
|
|
say(f" [{label}] rule lines: {rule_lines(since) or '(none at INFO)'} | record: {st or '(none)'}")
|
|
return st
|
|
|
|
|
|
def main():
|
|
say = print
|
|
w.say = say
|
|
w.login()
|
|
t0 = int(time.time())
|
|
try:
|
|
ok = w.deploy(APP, "torrent") and w.wait_app("torrent", "/", tries=90, delay=5)
|
|
say("1. qBittorrent installed:", ok)
|
|
before = prefs()
|
|
say(" before:", before)
|
|
if "error" in before:
|
|
raise SystemExit("INSTRUMENT: qBittorrent's preferences are unreadable — no verdict (fix the probe first)")
|
|
say("2. alternative speed ON, alternative limit 500 KiB/s (the household's):",
|
|
qb("app/setPreferences", "json=" + json.dumps({"alt_up_limit": 512000})) or "ok",
|
|
qb("transfer/toggleSpeedLimitsMode", "x=1") or "ok")
|
|
say(" now:", prefs())
|
|
rec1 = wait_tick(t0, say, "3. first tick")
|
|
p1 = prefs()
|
|
say(" after the first tick:", p1)
|
|
say("4. the household switches back to normal speed:", qb("transfer/toggleSpeedLimitsMode", "x=1") or "ok", prefs())
|
|
t1 = int(time.time())
|
|
time.sleep(960) # one full 15-minute tick; a "kept" decision logs nothing, a "household" one logs at INFO
|
|
rec2 = record()
|
|
say(" after the next tick:", prefs(), "| record:", rec2, "| lines:", rule_lines(t1) or "(none at INFO)")
|
|
verdict = ("PASS" if p1.get("up_limit", 0) and abs(p1["up_limit"] - 94.3 * 0.5 * 1e6 / 8) < 2048 and p1.get("alt_up_limit") == 512000
|
|
and isinstance(rec2, dict) and not rec2.get("household") else "FAIL")
|
|
say("VERDICT:", verdict)
|
|
finally:
|
|
sw.remove(APP, keep=False, say=say)
|
|
say("5. leftovers:", sw.leftovers(APP, "torrent"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|