""" modules/app_watchdog.py — Phase 4: Auto-restart crashed apps + excessive resource detection. This is real: - Watch a configured set of process names and relaunch commands. - Detect "excessive resource" processes and report top offenders. Configuration is stored in data/app_watchdog.json. """ from __future__ import annotations import json import subprocess import time from dataclasses import dataclass from pathlib import Path import psutil DATA_DIR = Path(__file__).resolve().parents[1] / "data" CFG_PATH = DATA_DIR / "app_watchdog.json" @dataclass(frozen=True, slots=True) class WatchResult: ok: bool message: str def _load_cfg() -> dict: if CFG_PATH.exists(): try: return json.loads(CFG_PATH.read_text(encoding="utf-8")) except Exception: return {} return {} def add_watch(process_name: str, launch_cmd: str) -> WatchResult: pn = (process_name or "").strip() cmd = (launch_cmd or "").strip() if not pn or not cmd: return WatchResult(False, "Usage: watch app ") DATA_DIR.mkdir(parents=True, exist_ok=True) cfg = _load_cfg() cfg.setdefault("apps", {})[pn.lower()] = cmd CFG_PATH.write_text(json.dumps(cfg, ensure_ascii=False), encoding="utf-8") return WatchResult(True, f"Watching {pn}.") def remove_watch(process_name: str) -> WatchResult: pn = (process_name or "").strip().lower() cfg = _load_cfg() apps = cfg.get("apps") or {} if pn in apps: apps.pop(pn, None) cfg["apps"] = apps CFG_PATH.write_text(json.dumps(cfg, ensure_ascii=False), encoding="utf-8") return WatchResult(True, f"Removed watch: {pn}.") return WatchResult(False, "Not watched.") def list_watch() -> WatchResult: cfg = _load_cfg() apps = cfg.get("apps") or {} if not apps: return WatchResult(True, "No watched apps.") rows = [f"{k} -> {v}" for k, v in list(apps.items())[:10]] return WatchResult(True, " | ".join(rows)[:240]) def excessive_resources(cpu_threshold: float = 35.0, limit: int = 5) -> WatchResult: try: # Snapshot CPU usage over a short window procs = list(psutil.process_iter(["name"])) for p in procs: try: p.cpu_percent(None) except Exception: pass time.sleep(0.4) rows = [] for p in procs: try: c = p.cpu_percent(None) if c >= cpu_threshold: rows.append((c, p.pid, p.info.get("name") or "")) except Exception: continue rows.sort(reverse=True) if not rows: return WatchResult(True, "No heavy processes right now.") msg = " | ".join(f"{n}({pid}) {c:.0f}%" for c, pid, n in rows[:limit]) return WatchResult(True, msg[:240]) except Exception as e: return WatchResult(False, f"Resource scan failed: {e}") def run_loop(interval_s: float = 2.5) -> None: cfg = _load_cfg() apps: dict[str, str] = cfg.get("apps") or {} if not apps: return interval = max(1.0, float(interval_s)) while True: try: alive = { (p.info.get("name") or "").lower() for p in psutil.process_iter(["name"]) } for pn, cmd in apps.items(): if pn not in alive: try: subprocess.Popen(cmd, shell=True) except Exception: pass except Exception: pass time.sleep(interval)