#!/usr/bin/env python3 """servizio-gate-watch — sorveglianza meccanica dell'esecuzione job gate-ufficio, nessun LLM. Sostituisce la vecchia sentinella persistente `sentinella-gate-ufficio` (04/09/2026): fa girare PYTHON/gate-ufficio/nave/monitor_executions.py invariato (riusato, non reimplementato) e reagisce alle righe classificate che stampa: - [avviato]/[warning]: nessuna azione, solo tenute a mente (servono solo se il job fallisce). - [concluso] successo: scritto direttamente in mailbox (sender "gate-ufficio") — evento meccanico, nessun LLM coinvolto. - [concluso] fallimento: scritto in mailbox (sender "gate-watch", job/exit_code/duration/log path/warning già viste) — nessuna slave lanciata qui. Da quando questa cartella è entrata nel perimetro di gemello-nave (11/09/2026, vedi CLAUDE.md di ruolo), è lei — già invocata a freddo dal demone di trigger su questo stesso evento — a leggere il log e spiegare la causa, non più una slave dedicata con SendMessage diretto ad Adrian. Vedi archivio/Adrian/progetti/project_gemello_nave.md per la diagnosi/decisione che ha motivato lo spostamento (stesso pattern già applicato a servizio-inbox-watch/servizio-gmail-check). Girato come servizio systemd (vedi servizio-gate-watch.service), Restart=on-failure. """ import re import subprocess import sys from collections import defaultdict from datetime import datetime, timezone from pathlib import Path ROOT = Path("/mnt/ssd/data/Dropbox/adrian") MONITOR_SCRIPT = ROOT / "PYTHON" / "gate-ufficio" / "nave" / "monitor_executions.py" LOG_DIR = ROOT / "PYTHON" / "gate-ufficio" / "ufficio" / "logs" LOG = Path(__file__).parent / "watch.log" MAILBOX_WRITE = ROOT / "scripts" / "slave_mailbox_write.sh" AVVIATO_RE = re.compile(r"^\[avviato\] (\S+)$") WARNING_RE = re.compile(r"^\[warning\] (\S+): (.*)$") CONCLUSO_RE = re.compile(r"^\[concluso\] (\S+): status=(\w+) exit_code=(-?\d+) duration=([\d.]+)s$") MAX_WARNINGS_NEL_PROMPT = 10 def log(msg: str) -> None: line = f"{datetime.now(timezone.utc).isoformat()} {msg}" print(line, flush=True) with open(LOG, "a") as f: f.write(line + "\n") def write_mailbox(text: str) -> None: subprocess.run(["bash", str(MAILBOX_WRITE), "gate-ufficio", text], cwd=str(ROOT), check=False) def report_failure_mailbox(job: str, exit_code: str, duration: str, warnings: list[str]) -> None: log_path = LOG_DIR / job warn_lines = warnings[-MAX_WARNINGS_NEL_PROMPT:] text = f"{job}: fallito (exit_code={exit_code}, duration={duration}s), log={log_path}" if warn_lines: text += "\n" + "\n".join(f" {w}" for w in warn_lines) log(f"fallimento rilevato: {job} (exit_code={exit_code}) — segnalato in mailbox") subprocess.run(["bash", str(MAILBOX_WRITE), "gate-watch", text], cwd=str(ROOT), check=False) def main() -> None: log("avvio") warnings_per_job: dict[str, list[str]] = defaultdict(list) proc = subprocess.Popen( [sys.executable, str(MONITOR_SCRIPT)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, ) assert proc.stdout is not None for line in proc.stdout: line = line.rstrip("\n") m = AVVIATO_RE.match(line) if m: warnings_per_job[m.group(1)] = [] continue m = WARNING_RE.match(line) if m: job, riga = m.groups() warnings_per_job[job].append(riga) continue m = CONCLUSO_RE.match(line) if m: job, status, exit_code, duration = m.groups() if status == "completed" and exit_code == "0": write_mailbox(f"{job}: completato regolarmente (durata {duration}s)") else: report_failure_mailbox(job, exit_code, duration, warnings_per_job.get(job, [])) warnings_per_job.pop(job, None) continue # riga non riconosciuta (es. traceback di monitor_executions.py stesso) — solo log locale if line: log(f"[monitor_executions] {line}") log(f"monitor_executions.py terminato inaspettatamente (exit {proc.wait()})") if __name__ == "__main__": main()