From 2db0d92d5a703559ebf2ea8082c0171c4204ef93 Mon Sep 17 00:00:00 2001 From: operator-main Date: Sun, 4 Oct 2026 18:34:09 +0000 Subject: [PATCH] Add chrome-error-scan.sh and box chrome-errors action Self-contained per-profile chrome log scanner with watermark-based new-match detection. Box CLI action returns JSON per-profile counts. Session: sidechat/chromebox-ops --- bin/box-ctl.py | 44 ++++++++++++++++++++++ bin/chrome-error-scan.sh | 81 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100755 bin/chrome-error-scan.sh diff --git a/bin/box-ctl.py b/bin/box-ctl.py index 8458c73..33d1505 100755 --- a/bin/box-ctl.py +++ b/bin/box-ctl.py @@ -650,6 +650,25 @@ def act_notify(agent, message, sidechat=None, allow_main_chat=False): out(True, agent=agent, target=target, sent=True) +def act_watchdog_alerts(): + """Check for new failed browser relaunches since the watermark. + + Runs bin/watchdog-alert-check.sh, which exits 0 (no new failures) + or 1 (new failures printed to stdout) and maintains its own + watermark at NETVM_ROOT/watchdog-alert-watermark.txt. + """ + audit("watchdog-alerts") + script = BIN / "watchdog-alert-check.sh" + r = subprocess.run([str(script)], capture_output=True, text=True, timeout=30) + failures = [l for l in (r.stdout or "").splitlines() if l.strip()] + # Exit 0 = no new failures, exit 1 = new failures found. Any other + # exit code is a script error. + if r.returncode not in (0, 1): + fail("WATCHDOG_CHECK_ERROR", "watchdog-alert-check.sh failed", + {"stderr": (r.stderr or "").strip()[-500:]}) + out(True, new_failures=len(failures), failures=failures) + + def act_fleet_status(): audit("fleet-status") cmd = [sys.executable, str(BIN / "super-cli.py"), "fleet", "status", "--json"] @@ -678,6 +697,21 @@ def act_relay_health(): out(all_healthy, relays=results, healthy=all_healthy) +def act_chrome_errors(): + """Run chrome-error-scan.sh and return per-profile error counts as JSON.""" + audit("chrome-errors") + cmd = ["/bin/bash", str(BIN / "chrome-error-scan.sh"), "--json"] + r = subprocess.run(cmd, capture_output=True, text=True) + if r.returncode == 0: + try: + data = json.loads(r.stdout) + out(True, profiles=data) + return + except Exception: + pass + fail("SCAN_ERROR", "chrome-error-scan.sh failed", {"stderr": r.stderr}) + + def act_dm_log(limit=50): audit("dm-log", str(limit)) cmd = [sys.executable, str(BIN / "super-cli.py"), "dm", "log", "--json", "-n", str(limit)] @@ -1064,6 +1098,8 @@ USAGE = """usage: box-ctl.py [args] fleet: fleet-status + chrome-errors per-profile chrome FATAL/crash counts (new since watermark) + watchdog-alerts relay-health timer actions: @@ -1291,8 +1327,16 @@ def main(argv): fail("BAD_NAME", "usage: policy [check |show]") elif action == "fleet-status": act_fleet_status() + elif action == "watchdog-alerts": + if rest: + fail("BAD_NAME", "usage: watchdog-alerts") + act_watchdog_alerts() elif action == "relay-health": act_relay_health() + elif action == "chrome-errors": + if rest: + fail("BAD_ARGS", "usage: chrome-errors") + act_chrome_errors() elif action == "dm-log": limit = 50 if rest: diff --git a/bin/chrome-error-scan.sh b/bin/chrome-error-scan.sh new file mode 100755 index 0000000..ede06d8 --- /dev/null +++ b/bin/chrome-error-scan.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# chrome-error-scan.sh - scan per-profile chrome logs for concerning patterns. +# Self-contained: scans, compares against watermark, reports only NEW matches. +# +# Usage: chrome-error-scan.sh [--json] +# Default output: "profile:new_count" lines for profiles with new matches, +# or "OK: no new errors" if clean. +# --json: output JSON {"profile": {"total": N, "new": M}, ...} +# +# Watermark: /home/super/Projects/NetVM/chrome-error-watermark.json +# Patterns: FATAL, crash, segfault, out of memory (case-insensitive) + +LOGDIR="/home/super/Projects/NetVM" +WATERMARK="$LOGDIR/chrome-error-watermark.json" + +# Gather current counts per profile (grep -c prints 0 with exit 1 on no match; +# the || true masks the exit code while preserving the "0" on stdout) +get_count() { + local f="$LOGDIR/chromebox-$1.log" + if [ -f "$f" ]; then + grep -ciE "FATAL|crash|segfault|out of memory" "$f" 2>/dev/null || true + else + echo 0 + fi +} + +MUSE_C=$(get_count muse) +PIP_C=$(get_count pip) +N646_C=$(get_count 646) +OPM_C=$(get_count opm) + +python3 - "$WATERMARK" "$MUSE_C" "$PIP_C" "$N646_C" "$OPM_C" "$1" <<'PYEOF' +import json, sys, os + +watermark_path = sys.argv[1] +current = { + "muse": int(sys.argv[2]), + "pip": int(sys.argv[3]), + "646": int(sys.argv[4]), + "opm": int(sys.argv[5]), +} +as_json = len(sys.argv) > 6 and sys.argv[6] == "--json" + +# Load watermark (tolerate missing/corrupt file -> treat as all-zero) +watermark = {} +if os.path.exists(watermark_path): + try: + with open(watermark_path) as f: + data = json.load(f) + watermark = data.get("counts", data) if isinstance(data, dict) else {} + except (ValueError, IOError, OSError): + watermark = {} + +new_counts = {} +for prof in ("muse", "pip", "646", "opm"): + old = watermark.get(prof, 0) + try: + old = int(old) + except (TypeError, ValueError): + old = 0 + delta = current[prof] - old + new_counts[prof] = max(delta, 0) + +if as_json: + out = {p: {"total": current[p], "new": new_counts[p]} for p in current} + print(json.dumps(out)) +else: + any_new = False + for prof in ("muse", "pip", "646", "opm"): + if new_counts[prof] > 0: + print("%s:%d" % (prof, new_counts[prof])) + any_new = True + if not any_new: + print("OK: no new errors") + +# Update watermark atomically +tmp = watermark_path + ".tmp" +with open(tmp, "w") as f: + json.dump({"counts": current}, f, indent=2) +os.replace(tmp, watermark_path) +PYEOF