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()