257 lines
12 KiB
Python
257 lines
12 KiB
Python
# LauncherTool/core/execution_handler.py
|
|
"""
|
|
Handles the execution of application sequences.
|
|
"""
|
|
import subprocess
|
|
import os
|
|
import shlex
|
|
import logging
|
|
import time # Per la pausa, ma verrà gestito da Tkinter per l'UI
|
|
from typing import List, Dict, Callable, Any
|
|
from .config_manager import ConfigManager
|
|
from .exceptions import (
|
|
ApplicationNotFoundError,
|
|
ExecutionError,
|
|
CommandExecutionError,
|
|
ApplicationPathNotFoundError
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ExecutionHandler:
|
|
"""
|
|
Manages the execution of a sequence of applications.
|
|
"""
|
|
|
|
def __init__(self,
|
|
config_manager: ConfigManager,
|
|
output_callback: Callable[[str], None] = None,
|
|
step_delay_callback: Callable[[float, Callable, list, int], Any] = None):
|
|
"""
|
|
Initializes the ExecutionHandler.
|
|
|
|
Args:
|
|
config_manager (ConfigManager): The configuration manager instance.
|
|
output_callback (Callable[[str], None], optional):
|
|
A callback function to send output messages to (e.g., for GUI display).
|
|
It takes a string message as an argument.
|
|
step_delay_callback (Callable[[float, Callable, list, int], Any], optional):
|
|
A callback function to handle delays between steps, typically `root.after`
|
|
for Tkinter applications.
|
|
It takes (delay_ms, next_function, *args_for_next_function).
|
|
"""
|
|
self.config_manager = config_manager
|
|
self.output_callback = output_callback if output_callback else self._default_output
|
|
# If no step_delay_callback is provided, we'll use a simple time.sleep
|
|
# This makes the class usable even outside a Tkinter event loop, e.g., for tests
|
|
# or a CLI version.
|
|
self.step_delay_callback = step_delay_callback if step_delay_callback else self._default_delay
|
|
|
|
def _default_output(self, message: str):
|
|
"""Default output method if none is provided (prints to console)."""
|
|
print(message)
|
|
logger.debug(f"ExecutionHandler (default output): {message}")
|
|
|
|
def _default_delay(self,
|
|
delay_seconds: float,
|
|
next_callable: Callable,
|
|
*args):
|
|
"""Default delay method if none is provided (uses time.sleep)."""
|
|
time.sleep(delay_seconds)
|
|
return next_callable(*args)
|
|
|
|
def _log_and_output(self, message: str, level: int = logging.INFO):
|
|
"""Helper to log and send output via callback."""
|
|
if level == logging.ERROR:
|
|
logger.error(message)
|
|
elif level == logging.WARNING:
|
|
logger.warning(message)
|
|
else:
|
|
logger.info(message)
|
|
self.output_callback(message + "\n")
|
|
|
|
|
|
def run_sequence_async(self, sequence_name: str):
|
|
"""
|
|
Starts the execution of a named sequence.
|
|
This method is designed to be called, and then the execution proceeds
|
|
step-by-step using the step_delay_callback.
|
|
|
|
Args:
|
|
sequence_name (str): The name of the sequence to run.
|
|
|
|
Raises:
|
|
SequenceNotFoundError: If the sequence is not found.
|
|
"""
|
|
try:
|
|
sequence = self.config_manager.get_sequence_by_name(sequence_name)
|
|
except NameNotFoundError as e: # Catching the specific error from config_manager
|
|
self._log_and_output(f"Error: {e}", level=logging.ERROR)
|
|
raise # Re-raise to be handled by the caller (e.g., GUI)
|
|
|
|
self._log_and_output(f"Starting sequence: {sequence_name}")
|
|
steps = sequence.get("steps", [])
|
|
if not steps:
|
|
self._log_and_output(f"Sequence '{sequence_name}' has no steps to execute.", level=logging.WARNING)
|
|
self._log_and_output("Sequence completed (no steps).")
|
|
return
|
|
|
|
# Start the first step using the delay callback mechanism
|
|
# The first step doesn't have a preceding delay from here,
|
|
# but we use the callback structure for consistency.
|
|
self.step_delay_callback(0, self._execute_step, steps, 0)
|
|
|
|
|
|
def _execute_step(self, steps: List[Dict], step_index: int):
|
|
"""
|
|
Executes a single step of the sequence and schedules the next one.
|
|
This method is intended to be called by the step_delay_callback.
|
|
|
|
Args:
|
|
steps (List[Dict]): The list of all steps in the sequence.
|
|
step_index (int): The index of the current step to execute.
|
|
"""
|
|
if step_index >= len(steps):
|
|
self._log_and_output("Sequence completed.")
|
|
return # Sequence finished
|
|
|
|
current_step = steps[step_index]
|
|
app_name = current_step.get("application")
|
|
wait_time_seconds = float(current_step.get("wait_time", 0.0))
|
|
|
|
if not app_name:
|
|
self._log_and_output(
|
|
f"Error in step {step_index + 1}: Application name is missing.",
|
|
level=logging.ERROR
|
|
)
|
|
# Optionally, decide whether to stop or skip to the next step
|
|
# For now, we stop if a step is malformed.
|
|
return
|
|
|
|
try:
|
|
application_config = self.config_manager.get_application_by_name(app_name)
|
|
except ApplicationNotFoundError:
|
|
self._log_and_output(
|
|
f"Error: Application '{app_name}' in step {step_index + 1} not found in configuration.",
|
|
level=logging.ERROR
|
|
)
|
|
# Decide: stop sequence or try next step? For now, stop.
|
|
return
|
|
|
|
app_path = application_config.get("path")
|
|
if not app_path or not os.path.exists(app_path):
|
|
self._log_and_output(
|
|
f"Error: Executable path for application '{app_name}' ('{app_path}') not found or invalid.",
|
|
level=logging.ERROR
|
|
)
|
|
# This is a critical error for this step.
|
|
# Consider raising ApplicationPathNotFoundError(app_name, app_path) if the GUI should handle it.
|
|
return
|
|
|
|
|
|
# Build the command
|
|
command = [app_path]
|
|
# Parameters defined in the sequence step override defaults from application config
|
|
step_parameters = current_step.get("parameters", {})
|
|
# Parameters defined in the application config (defaults)
|
|
app_config_parameters = application_config.get("parameters", [])
|
|
|
|
# Logic for assembling parameters:
|
|
# 1. Use value from step_parameters if defined for a parameter name.
|
|
# 2. Else, use default_value from app_config_parameters.
|
|
# Parameter names in step_parameters should match names in app_config_parameters.
|
|
|
|
# Create a dictionary of default values from app_config for quick lookup
|
|
default_params_map = {p["name"]: p["default_value"] for p in app_config_parameters if "name" in p and "default_value" in p}
|
|
|
|
# Add parameters defined in app_config_parameters, potentially overridden by step_parameters
|
|
for app_param_config in app_config_parameters:
|
|
param_name = app_param_config["name"]
|
|
param_value_to_use = None
|
|
|
|
if param_name in step_parameters:
|
|
# Value is directly provided in the sequence step's parameters
|
|
param_value_to_use = step_parameters[param_name]
|
|
elif app_param_config.get("default_value"):
|
|
# Use the default value from the application's configuration
|
|
param_value_to_use = app_param_config["default_value"]
|
|
|
|
if param_value_to_use is not None:
|
|
# The original code appended the value directly.
|
|
# If parameters need to be in the form "--name=value" or "/name:value",
|
|
# this logic needs adjustment based on how your EXEs expect them.
|
|
# Assuming the value itself is the full argument string (e.g., "/--NoRis", "/r:p=127.0.0.1")
|
|
command.append(str(param_value_to_use))
|
|
|
|
self._log_and_output(f"Executing step {step_index + 1}: {' '.join(map(shlex.quote, command))}")
|
|
|
|
try:
|
|
app_dir = os.path.dirname(app_path)
|
|
|
|
# Ensure current working directory is valid before changing
|
|
original_cwd = os.getcwd()
|
|
if os.path.isdir(app_dir):
|
|
os.chdir(app_dir)
|
|
logger.debug(f"Changed CWD to: {app_dir}")
|
|
else:
|
|
logger.warning(f"Application directory '{app_dir}' not found. Executing from CWD: {original_cwd}")
|
|
|
|
|
|
# For Windows, DETACHED_PROCESS and CREATE_NEW_PROCESS_GROUP are useful
|
|
# For Unix, subprocess.DEVNULL for stdout/stderr to detach cleanly if no output is needed
|
|
# shell=True can be a security risk if command parts come from untrusted input.
|
|
# Here, app_path comes from config, parameters too. shlex.quote helps.
|
|
# If shell=False, command must be a list, and path resolution is stricter.
|
|
# Given the original code used shell=True, we'll stick to it for now, but be mindful.
|
|
|
|
# The original code joined command into a string then passed to Popen with shell=True.
|
|
# It's generally safer to pass a list of args if shell=False.
|
|
# If shell=True, command_string is better.
|
|
command_string = ' '.join(map(shlex.quote, command))
|
|
|
|
if os.name == 'nt': # Windows
|
|
process = subprocess.Popen(
|
|
command_string, # or command list if shell=False
|
|
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
|
|
shell=True, # Consistent with original
|
|
cwd=app_dir if os.path.isdir(app_dir) else None # Popen can handle cwd
|
|
)
|
|
else: # Unix-like (Linux, macOS)
|
|
process = subprocess.Popen(
|
|
command_string, # or command list if shell=False
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
shell=True, # Consistent with original
|
|
start_new_session=True, # Good for detaching on Unix
|
|
cwd=app_dir if os.path.isdir(app_dir) else None
|
|
)
|
|
|
|
self._log_and_output(f"Application '{app_name}' launched (PID: {process.pid if process else 'Unknown'}).")
|
|
|
|
except FileNotFoundError: # Specifically for the executable not being found by Popen
|
|
err_msg = f"Execution Error: Application executable '{app_path}' for '{app_name}' not found by the system."
|
|
self._log_and_output(err_msg, level=logging.ERROR)
|
|
# raise ApplicationPathNotFoundError(app_name, app_path) from fnfe # Let GUI handle
|
|
# For now, stopping sequence
|
|
return
|
|
except Exception as e:
|
|
err_msg = f"Execution Error for '{app_name}': {e}"
|
|
self._log_and_output(err_msg, level=logging.ERROR)
|
|
# raise CommandExecutionError(command, e) from e # Let GUI handle
|
|
# For now, stopping sequence
|
|
return
|
|
finally:
|
|
if 'original_cwd' in locals() and os.getcwd() != original_cwd : # Check if original_cwd was set
|
|
os.chdir(original_cwd) # Change back to original CWD
|
|
logger.debug(f"Restored CWD to: {original_cwd}")
|
|
|
|
|
|
self._log_and_output(f"Waiting {wait_time_seconds} seconds before next step...")
|
|
|
|
# Schedule the next step
|
|
self.step_delay_callback(
|
|
wait_time_seconds * 1000, # Convert to milliseconds for Tkinter's 'after'
|
|
self._execute_step,
|
|
steps,
|
|
step_index + 1
|
|
) |