#!/usr/bin/env python3 """Thread keepalive timer entry point (runs on bl). Reads keepalive-config.json, ticks every registered thread: ensure reachable -> ping if idle past threshold -> recreate if dead. Designed to run from a systemd timer (e.g. every 5 minutes). Each tick is idempotent and cheap: threads that are alive and recently active are a single navigate+verify (no ping sent). Usage: keepalive-timer.py [--config PATH] [--state PATH] [--dry-run] [--status] --status print the registry with idle times and exit (no changes) Exit codes: 0 = all ok, 1 = one or more threads failed, 2 = config error. """ import argparse import json import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from keepalive import Keepalive NETVM_ROOT = os.environ.get("NETVM_ROOT", "/home/super/Projects/NetVM") DEFAULT_CONFIG = os.path.join(NETVM_ROOT, "keepalive-config.json") DEFAULT_STATE = os.path.join(NETVM_ROOT, "keepalive-threads.json") def load_config(path): try: with open(path) as f: cfg = json.load(f) except FileNotFoundError: print(f"keepalive: config not found: {path}", file=sys.stderr) return None except Exception as e: print(f"keepalive: bad config {path}: {e}", file=sys.stderr) return None threads = cfg.get("threads", []) if not isinstance(threads, list): print("keepalive: config 'threads' must be a list", file=sys.stderr) return None return threads def main(): ap = argparse.ArgumentParser() ap.add_argument("--config", default=DEFAULT_CONFIG) ap.add_argument("--state", default=DEFAULT_STATE) ap.add_argument("--dry-run", action="store_true") ap.add_argument("--status", action="store_true") args = ap.parse_args() ka = Keepalive(state_path=args.state, dry_run=args.dry_run) if args.status: print(json.dumps(ka.status(), indent=2)) return 0 threads = load_config(args.config) if threads is None: return 2 results = {} for t in threads: key = t.get("key") agent = t.get("agent", "opm") if not key or not t.get("enabled", True): continue try: status = ka.tick( key, agent, idle_threshold_s=int(t.get("idle_threshold_s", 3600)), max_retries=int(t.get("max_retries", 3)), ping_message=t.get("ping_message"), ) except Exception as e: status = f"error: {e}" ka.log_event("keepalive_tick_error", {"key": key, "agent": agent, "error": str(e)[:200]}) results[key] = status print(f"keepalive: {key}@{agent} -> {status}") failed = [k for k, v in results.items() if v in ("failed",) or v.startswith("error")] return 1 if failed else 0 if __name__ == "__main__": sys.exit(main())