234 lines
9.8 KiB
Python
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()
|