ParamManagerScripts/lib/script_executor.py

234 lines
9.8 KiB
Python

import os
import json
import subprocess
import re
import traceback
from typing import Dict, Any, List, Optional, Callable
import sys
import time
from datetime import datetime
# Import necessary handlers/managers
from .directory_manager import DirectoryManager
from .config_handler import ConfigHandler
from .logger import Logger
class ScriptExecutor:
def __init__(
self,
script_groups_path: str,
dir_manager: DirectoryManager,
config_handler: ConfigHandler,
app_logger: Logger,
get_exec_state_func: Callable[
[], Dict[str, Any]
], # Func to get {last_time, interval}
set_last_exec_time_func: Callable[[float], None], # Func to set last exec time
):
self.script_groups_path = script_groups_path
self.dir_manager = dir_manager
self.config_handler = config_handler
self.app_logger = app_logger # Central application logger instance
self._get_exec_state = get_exec_state_func
self._set_last_exec_time = set_last_exec_time_func
def execute_script(
self,
group: str,
script_name: str,
broadcast_fn: Optional[Callable[[str], None]] = None,
) -> Dict[str, Any]:
"""
Execute script, broadcast output in real-time, and save final log
to a script-specific file in the script's directory.
"""
exec_state = self._get_exec_state()
last_execution_time = exec_state.get("last_time", 0)
min_execution_interval = exec_state.get("interval", 1)
current_time = time.time()
time_since_last = current_time - last_execution_time
if time_since_last < min_execution_interval:
msg = f"Por favor espere {min_execution_interval - time_since_last:.1f} segundo(s) más entre ejecuciones"
self.app_logger.append_log(f"Warning: {msg}") # Log throttling attempt
if broadcast_fn:
broadcast_fn(msg)
return {"status": "throttled", "error": msg}
self._set_last_exec_time(current_time) # Update last execution time
script_path = os.path.join(self.script_groups_path, group, script_name)
script_dir = os.path.dirname(script_path)
script_base_name = os.path.splitext(script_name)[0]
script_log_path = os.path.join(script_dir, f"log_{script_base_name}.txt")
if not os.path.exists(script_path):
msg = f"Error Fatal: Script no encontrado en {script_path}"
self.app_logger.append_log(msg)
if broadcast_fn:
broadcast_fn(msg)
return {"status": "error", "error": "Script not found"}
# Get working directory using DirectoryManager
working_dir = self.dir_manager.get_work_dir_for_group(group)
if not working_dir:
msg = f"Error Fatal: Directorio de trabajo no configurado o inválido para el grupo '{group}'"
self.app_logger.append_log(msg)
if broadcast_fn:
broadcast_fn(msg)
return {"status": "error", "error": "Working directory not set"}
if not os.path.isdir(working_dir): # Double check validity
msg = f"Error Fatal: El directorio de trabajo '{working_dir}' no es válido o no existe."
self.app_logger.append_log(msg)
if broadcast_fn:
broadcast_fn(msg)
return {"status": "error", "error": "Invalid working directory"}
# Aggregate configurations using ConfigHandler
configs = {
"level1": self.config_handler.get_config("1"),
"level2": self.config_handler.get_config("2", group),
"level3": self.config_handler.get_config(
"3", group
), # Relies on workdir set in main manager
"working_directory": working_dir,
}
print(
f"Debug: Aggregated configs for script execution: {configs}"
) # Keep for debug
config_file_path = os.path.join(script_dir, "script_config.json")
try:
with open(config_file_path, "w", encoding="utf-8") as f:
json.dump(configs, f, indent=2, ensure_ascii=False)
except Exception as e:
msg = f"Error Fatal: No se pudieron guardar las configuraciones temporales en {config_file_path}: {str(e)}"
self.app_logger.append_log(msg)
if broadcast_fn:
broadcast_fn(msg)
# Optionally return error here
stdout_capture = []
stderr_capture = ""
process = None
start_time = datetime.now()
try:
if broadcast_fn:
start_msg = f"[{start_time.strftime('%H:%M:%S')}] Iniciando ejecución de {script_name} en {working_dir}..."
broadcast_fn(start_msg)
creation_flags = (
subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
)
process = subprocess.Popen(
["python", "-u", script_path],
cwd=working_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
bufsize=1,
env=dict(os.environ, PYTHONIOENCODING="utf-8"),
creationflags=creation_flags,
)
while True:
line = process.stdout.readline()
if not line and process.poll() is not None:
break
if line:
cleaned_line = line.rstrip()
stdout_capture.append(cleaned_line)
if broadcast_fn:
broadcast_fn(cleaned_line)
return_code = process.wait()
end_time = datetime.now()
duration = end_time - start_time
stderr_capture = process.stderr.read()
status = "success" if return_code == 0 else "error"
completion_msg = f"[{end_time.strftime('%H:%M:%S')}] Ejecución de {script_name} finalizada ({status}). Duración: {duration}."
if stderr_capture:
if status == "error" and broadcast_fn:
broadcast_fn(f"--- ERRORES ---")
broadcast_fn(stderr_capture.strip())
broadcast_fn(f"--- FIN ERRORES ---")
completion_msg += f" Se detectaron errores (ver log)."
if broadcast_fn:
broadcast_fn(completion_msg)
# Write to script-specific log file
try:
with open(script_log_path, "w", encoding="utf-8") as log_f:
log_f.write(
f"--- Log de Ejecución: {script_name} ---\nGrupo: {group}\nDirectorio de Trabajo: {working_dir}\n"
)
log_f.write(
f"Inicio: {start_time.strftime('%Y-%m-%d %H:%M:%S')}\nFin: {end_time.strftime('%Y-%m-%d %H:%M:%S')}\nDuración: {duration}\n"
)
log_f.write(
f"Estado: {status.upper()} (Código de Salida: {return_code})\n\n--- SALIDA ESTÁNDAR (STDOUT) ---\n"
)
log_f.write("\n".join(stdout_capture))
log_f.write("\n\n--- ERRORES (STDERR) ---\n")
log_f.write(stderr_capture if stderr_capture else "Ninguno")
log_f.write("\n--- FIN DEL LOG ---\n")
if broadcast_fn:
broadcast_fn(f"Log completo guardado en: {script_log_path}")
print(f"Info: Script log saved to {script_log_path}")
except Exception as log_e:
err_msg = f"Error al guardar el log específico del script en {script_log_path}: {log_e}"
print(err_msg)
self.app_logger.append_log(f"ERROR: {err_msg}")
if broadcast_fn:
broadcast_fn(err_msg)
return {
"status": status,
"return_code": return_code,
"error": stderr_capture if stderr_capture else None,
"log_file": script_log_path,
}
except Exception as e:
end_time = datetime.now()
duration = end_time - start_time
error_msg = (
f"Error inesperado durante la ejecución de {script_name}: {str(e)}"
)
traceback_info = traceback.format_exc()
print(error_msg)
print(traceback_info)
self.app_logger.append_log(f"ERROR FATAL: {error_msg}\n{traceback_info}")
if broadcast_fn:
broadcast_fn(
f"[{end_time.strftime('%H:%M:%S')}] ERROR FATAL: {error_msg}"
)
try: # Attempt to write error to script-specific log
with open(script_log_path, "w", encoding="utf-8") as log_f:
log_f.write(
f"--- Log de Ejecución: {script_name} ---\nGrupo: {group}\nDirectorio de Trabajo: {working_dir}\n"
)
log_f.write(
f"Inicio: {start_time.strftime('%Y-%m-%d %H:%M:%S')}\nFin: {end_time.strftime('%Y-%m-%d %H:%M:%S')} (Interrumpido por error)\n"
)
log_f.write(
f"Duración: {duration}\nEstado: FATAL ERROR\n\n--- ERROR ---\n{error_msg}\n\n--- TRACEBACK ---\n{traceback_info}\n--- FIN DEL LOG ---\n"
)
except Exception as log_e:
print(f"Error adicional al intentar guardar el log de error: {log_e}")
return {"status": "error", "error": error_msg, "traceback": traceback_info}
finally:
if process and process.stderr:
process.stderr.close()
if process and process.stdout:
process.stdout.close()