feature/generador-instaladores-linux-windows
This commit is contained in:
@@ -2,18 +2,21 @@
|
||||
|
||||
import platform
|
||||
import socket
|
||||
from threading import Thread
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
from PySide6.QtCore import QObject, Signal, QThreadPool
|
||||
|
||||
from .file_watcher import FileWatcher, FileStabilityChecker, calculate_file_hash
|
||||
from .maintenance_scheduler import DailyMaintenanceScheduler
|
||||
from .restore_worker import RestoreWorker
|
||||
from .retention import RetentionCleaner
|
||||
from .. import __version__
|
||||
from ..db.job_repository import JobRepository
|
||||
from ..db.event_repository import EventRepository
|
||||
from ..db.config_repository import ConfigRepository
|
||||
from ..config.env_loader import apply_env_overrides, load_env_file
|
||||
from ..constants import JobStatus, DEFAULT_CONFIG
|
||||
from ..constants import APP_ARCH, APP_DIR, APP_PLATFORM, JobStatus, DEFAULT_CONFIG
|
||||
from ..panel import panel_client
|
||||
from ..utils.logger import app_logger
|
||||
|
||||
@@ -39,7 +42,10 @@ class RestoreEngine(QObject):
|
||||
|
||||
# File watcher
|
||||
self._file_watcher: Optional[FileWatcher] = None
|
||||
|
||||
|
||||
# Mantenimiento diario (retención de respaldos obsoletos)
|
||||
self._maintenance: Optional[DailyMaintenanceScheduler] = None
|
||||
|
||||
# Estado
|
||||
self._running = False
|
||||
self._paused = False
|
||||
@@ -75,9 +81,10 @@ class RestoreEngine(QObject):
|
||||
app_logger.info("Configuración guardada")
|
||||
self._report_instance_config_to_panel()
|
||||
|
||||
# Reconfigurar file watcher si está corriendo
|
||||
# Reconfigurar file watcher y mantenimiento si está corriendo
|
||||
if self._running:
|
||||
self._restart_file_watcher()
|
||||
self._restart_maintenance()
|
||||
|
||||
def get_config(self) -> dict:
|
||||
"""Obtiene la configuración actual."""
|
||||
@@ -93,7 +100,12 @@ class RestoreEngine(QObject):
|
||||
if not self._validate_config():
|
||||
app_logger.error("Configuración inválida, no se puede iniciar el motor")
|
||||
return
|
||||
|
||||
|
||||
# Barrido de Temp: al arrancar no hay jobs corriendo, así que cualquier subcarpeta en
|
||||
# extract_folder es un remanente huérfano de una corrida previa (los caminos de
|
||||
# fallo/diferido no siempre limpiaban). Se elimina para que Temp no se acumule.
|
||||
self._purge_stale_temp()
|
||||
|
||||
# Configurar thread pool
|
||||
extract_workers = self._config["concurrency"]["extract_workers"]
|
||||
restore_workers = self._config["concurrency"]["restore_workers"]
|
||||
@@ -108,7 +120,10 @@ class RestoreEngine(QObject):
|
||||
# Iniciar file watcher
|
||||
if self._config["features"]["auto_scan_enabled"]:
|
||||
self._start_file_watcher()
|
||||
|
||||
|
||||
# Iniciar el mantenimiento diario (limpieza de respaldos obsoletos)
|
||||
self._start_maintenance()
|
||||
|
||||
self._running = True
|
||||
self._paused = False
|
||||
self._report_instance_config_to_panel()
|
||||
@@ -116,6 +131,28 @@ class RestoreEngine(QObject):
|
||||
EventRepository.create("INFO", "Motor iniciado")
|
||||
app_logger.info("Motor iniciado")
|
||||
|
||||
def _purge_stale_temp(self):
|
||||
"""Elimina subcarpetas huérfanas en la carpeta temporal de extracción (al arrancar)."""
|
||||
import shutil
|
||||
|
||||
extract_folder = (self._config.get("paths") or {}).get("extract_folder")
|
||||
if not extract_folder:
|
||||
return
|
||||
base = Path(extract_folder)
|
||||
if not base.is_dir():
|
||||
return
|
||||
removed = 0
|
||||
for child in base.iterdir():
|
||||
if not child.is_dir():
|
||||
continue
|
||||
try:
|
||||
shutil.rmtree(child, ignore_errors=True)
|
||||
removed += 1
|
||||
except Exception as e:
|
||||
app_logger.warning(f"No se pudo limpiar Temp huérfano {child}: {e}")
|
||||
if removed:
|
||||
app_logger.info(f"Temp: {removed} carpeta(s) huérfana(s) eliminada(s) al iniciar")
|
||||
|
||||
def stop(self):
|
||||
"""Detiene el motor."""
|
||||
if not self._running:
|
||||
@@ -125,7 +162,10 @@ class RestoreEngine(QObject):
|
||||
if self._file_watcher:
|
||||
self._file_watcher.stop()
|
||||
self._file_watcher = None
|
||||
|
||||
|
||||
# Detener el mantenimiento diario
|
||||
self._stop_maintenance()
|
||||
|
||||
# Esperar a que terminen los workers
|
||||
self._thread_pool.waitForDone(msecs=30000) # 30s timeout
|
||||
|
||||
@@ -207,28 +247,45 @@ class RestoreEngine(QObject):
|
||||
if not api_url or not api_token:
|
||||
return
|
||||
|
||||
input_folder = (self._config.get("paths") or {}).get("input_folder") or ""
|
||||
paths = self._config.get("paths") or {}
|
||||
input_folder = paths.get("input_folder") or ""
|
||||
processed_folder = paths.get("processed_folder") or ""
|
||||
try:
|
||||
host_name = socket.gethostname() or platform.node()
|
||||
except Exception:
|
||||
host_name = platform.node()
|
||||
|
||||
instance_key = (panel_cfg.get("instance_key") or "").strip() or None
|
||||
# platform/arch le dicen al PANEL qué artefacto le toca a este servidor cuando
|
||||
# instala o actualiza (a24c.cras_releases se llavea por version+platform+arch).
|
||||
panel_client.report_instance_config(
|
||||
api_url=api_url,
|
||||
api_token=api_token,
|
||||
input_folder=input_folder,
|
||||
processed_folder=processed_folder,
|
||||
host_name=host_name,
|
||||
app_version=__version__,
|
||||
instance_key=instance_key,
|
||||
platform_name=APP_PLATFORM,
|
||||
arch=APP_ARCH,
|
||||
# Dónde vive el ejecutable. El PANEL actualiza en esta ruta; si instalara en la
|
||||
# default crearía una segunda instalación y dejaría huérfano este config/.env.
|
||||
install_path=str(APP_DIR),
|
||||
)
|
||||
|
||||
def _validate_config(self) -> bool:
|
||||
"""Valida que la configuración sea correcta."""
|
||||
paths = self._config["paths"]
|
||||
|
||||
# Validar carpetas requeridas
|
||||
required_paths = ["input_folder", "extract_folder", "data_sql_folder"]
|
||||
# Validar carpetas requeridas. processed_folder/failed_folder se incluyen para que la
|
||||
# reubicación de ZIP (éxito/fallo) nunca falle por carpeta inexistente.
|
||||
required_paths = [
|
||||
"input_folder",
|
||||
"extract_folder",
|
||||
"data_sql_folder",
|
||||
"processed_folder",
|
||||
"failed_folder",
|
||||
]
|
||||
for key in required_paths:
|
||||
if not paths.get(key):
|
||||
app_logger.error(f"Falta configurar: {key}")
|
||||
@@ -280,6 +337,52 @@ class RestoreEngine(QObject):
|
||||
"""Reinicia el file watcher con nueva configuración."""
|
||||
if self._running and not self._paused:
|
||||
self._start_file_watcher()
|
||||
|
||||
def _start_maintenance(self):
|
||||
"""Inicia el mantenimiento diario si la retención está habilitada."""
|
||||
retention_cfg = self._config.get("retention", {})
|
||||
if not retention_cfg.get("enabled", True):
|
||||
app_logger.info("Retención deshabilitada; no se inicia el mantenimiento diario")
|
||||
return
|
||||
self._maintenance = DailyMaintenanceScheduler(
|
||||
task=self._run_retention,
|
||||
check_interval_seconds=retention_cfg.get("check_interval_seconds", 3600),
|
||||
run_at_hour=retention_cfg.get("run_at_hour", 3),
|
||||
)
|
||||
self._maintenance.start()
|
||||
|
||||
def _stop_maintenance(self):
|
||||
"""Detiene el mantenimiento diario."""
|
||||
if self._maintenance:
|
||||
self._maintenance.stop()
|
||||
self._maintenance = None
|
||||
|
||||
def _restart_maintenance(self):
|
||||
"""Reinicia el mantenimiento diario con nueva configuración."""
|
||||
if self._running:
|
||||
self._stop_maintenance()
|
||||
self._start_maintenance()
|
||||
|
||||
def _run_retention(self):
|
||||
"""Ejecuta una corrida de retención (tarea del mantenimiento diario)."""
|
||||
try:
|
||||
RetentionCleaner(self._config).run()
|
||||
except Exception as e:
|
||||
app_logger.error(f"Error en la retención: {e}", exc_info=True)
|
||||
EventRepository.create("ERROR", f"Error en la retención: {e}")
|
||||
|
||||
def trigger_maintenance_now(self):
|
||||
"""Corre la retención de inmediato en segundo plano (acción manual de la UI).
|
||||
|
||||
Respeta el dry_run de la configuración y no consume el turno del día calendario.
|
||||
"""
|
||||
def _run():
|
||||
if self._maintenance:
|
||||
self._maintenance.trigger_now()
|
||||
else:
|
||||
self._run_retention()
|
||||
|
||||
Thread(target=_run, daemon=True).start()
|
||||
|
||||
def _on_file_ready(self, file_path: str):
|
||||
"""
|
||||
@@ -296,10 +399,18 @@ class RestoreEngine(QObject):
|
||||
# Calcular hash para evitar duplicados
|
||||
file_hash = calculate_file_hash(file_path)
|
||||
|
||||
if JobRepository.exists_by_hash(file_hash):
|
||||
app_logger.warning(f"Archivo ya procesado (hash duplicado): {file_path}")
|
||||
if JobRepository.has_blocking_job_by_hash(file_hash):
|
||||
app_logger.warning(f"Archivo ya procesado o en curso (hash duplicado): {file_path}")
|
||||
return
|
||||
|
||||
|
||||
# Limpia intentos FALLIDOS previos con este hash para permitir un reintento fresco
|
||||
# (antes un FAILED transitorio bloqueaba el reproceso de forma permanente).
|
||||
removed = JobRepository.delete_failed_by_hash(file_hash)
|
||||
if removed:
|
||||
app_logger.info(
|
||||
f"Reintento de {Path(file_path).name}: {removed} job(s) fallido(s) previo(s) eliminado(s)"
|
||||
)
|
||||
|
||||
# Crear job
|
||||
file_name = Path(file_path).name
|
||||
job_id = JobRepository.create(file_path, file_name, file_hash)
|
||||
|
||||
Reference in New Issue
Block a user