03-oct: superguardado automático
This commit is contained in:
91
scripts/monitor_estado/lock.py
Normal file
91
scripts/monitor_estado/lock.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""lock.py — Instancia unica del monitor (RF-22).
|
||||
|
||||
Usa un fichero `.lock` en el directorio de datos del monitor con el PID y la hora de
|
||||
arranque de la instancia viva. Antes de arrancar, si hay un PID vivo, se aborta; si el
|
||||
lock es de un proceso muerto (apagado/crash), se recupera.
|
||||
|
||||
Motivo: la tarea programada y el plugin de OpenCode pueden coincidir; este lock evita
|
||||
que convivan dos monitores recogiendo a la vez.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from . import config
|
||||
|
||||
LOCK_PATH: Path = config.MONITOR_DIR / ".lock"
|
||||
|
||||
try:
|
||||
import psutil # type: ignore
|
||||
except Exception: # pragma: no cover - psutil esta disponible en el entorno
|
||||
psutil = None
|
||||
|
||||
|
||||
def pid_vivo(pid: int) -> bool:
|
||||
"""Devuelve True si el PID corresponde a un proceso vivo (no zombie)."""
|
||||
if pid <= 0:
|
||||
return False
|
||||
if psutil is not None:
|
||||
try:
|
||||
return psutil.pid_exists(pid) and psutil.Process(pid).status() != psutil.STATUS_ZOMBIE
|
||||
except Exception:
|
||||
return False
|
||||
# Fallback sin psutil.
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _leer_pid() -> int | None:
|
||||
"""Lee el PID del lock actual, o None si no se puede."""
|
||||
try:
|
||||
with open(LOCK_PATH, "r", encoding="utf-8") as f:
|
||||
return int(json.load(f).get("pid", 0)) or None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _escribir_lock() -> None:
|
||||
"""Escribe el lock con el PID y la hora de arranque actuales."""
|
||||
LOCK_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(LOCK_PATH, "w", encoding="utf-8") as f:
|
||||
json.dump({"pid": os.getpid(), "arranque": datetime.now().isoformat()}, f, indent=2)
|
||||
|
||||
|
||||
def adquirir() -> bool:
|
||||
"""Intenta adquirir el lock. Devuelve False si ya hay una instancia viva."""
|
||||
propio = os.getpid()
|
||||
if LOCK_PATH.exists():
|
||||
pid = _leer_pid()
|
||||
if pid and pid_vivo(pid):
|
||||
# Viva (propia o ajena) -> no arrancar otra instancia.
|
||||
return False
|
||||
# Lock caducado (proceso muerto): se elimina y se reintenta.
|
||||
try:
|
||||
LOCK_PATH.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
_escribir_lock()
|
||||
return True
|
||||
|
||||
|
||||
def liberar() -> None:
|
||||
"""Elimina el lock solo si pertenece a nuestro propio PID."""
|
||||
try:
|
||||
if LOCK_PATH.exists() and _leer_pid() == os.getpid():
|
||||
LOCK_PATH.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def esta_bloqueado() -> bool:
|
||||
"""True si hay una instancia viva (distinta o igual) ocupando el lock."""
|
||||
pid = _leer_pid() if LOCK_PATH.exists() else None
|
||||
return bool(pid and pid_vivo(pid))
|
||||
Reference in New Issue
Block a user