From ec89134d2ce4835e34d249ae225adb7f85c3957a Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Fri, 23 May 2025 09:23:10 +0200 Subject: [PATCH] fix gui --- cpp_python_debug/core/profile_executor.py | 243 ++++++++++ cpp_python_debug/gui/main_window.py | 523 ++++++++++++++-------- 2 files changed, 585 insertions(+), 181 deletions(-) diff --git a/cpp_python_debug/core/profile_executor.py b/cpp_python_debug/core/profile_executor.py index e69de29..c7d7a2b 100644 --- a/cpp_python_debug/core/profile_executor.py +++ b/cpp_python_debug/core/profile_executor.py @@ -0,0 +1,243 @@ +# File: cpp_python_debug/core/profile_executor.py +# Manages the automated execution of a debug profile. + +import logging +import os +import time # For timestamping if we log data directly for now +from typing import Dict, Any, Optional, Callable + +from .gdb_controller import GDBSession +from .config_manager import AppSettings +# from .output_formatter import save_to_json, save_to_csv # For later + +logger = logging.getLogger(__name__) + +class ProfileExecutor: + """ + Orchestrates the execution of a debug profile, interacting with GDBSession. + """ + + def __init__(self, + profile_data: Dict[str, Any], + app_settings: AppSettings, + status_update_callback: Optional[Callable[[str], None]] = None, + gdb_output_callback: Optional[Callable[[str], None]] = None, + json_output_callback: Optional[Callable[[Any], None]] = None + ): + """ + Initializes the ProfileExecutor. + + Args: + profile_data: The dictionary containing the profile configuration. + app_settings: The application settings instance. + status_update_callback: Callback to update status in GUI. + gdb_output_callback: Callback to send GDB raw output to GUI. + json_output_callback: Callback to send parsed JSON data to GUI. + """ + self.profile = profile_data + self.app_settings = app_settings + self.gdb_session: Optional[GDBSession] = None + self.is_running: bool = False + self._stop_requested: bool = False + + self.status_updater = status_update_callback if status_update_callback else lambda msg: logger.info(f"Status: {msg}") + self.gdb_output_writer = gdb_output_callback if gdb_output_callback else lambda msg: logger.debug(f"GDB Output: {msg}") + self.json_data_handler = json_output_callback if json_output_callback else lambda data: logger.debug(f"JSON Data: {str(data)[:200]}") + + logger.info(f"ProfileExecutor initialized for profile: '{self.profile.get('profile_name', 'Unnamed Profile')}'") + + def _get_setting(self, category: str, key: str, default: Optional[Any] = None) -> Any: + """Helper to get settings via app_settings.""" + return self.app_settings.get_setting(category, key, default) + + def _get_dumper_options(self) -> Dict[str, Any]: + """Helper to get dumper options.""" + return self.app_settings.get_category_settings("dumper_options", {}) + + + def run(self) -> None: + """ + Starts the automated execution of the debug profile. + This method will run synchronously for now for simplicity in this first step. + Later, it might need to run in a separate thread to keep the GUI responsive. + """ + profile_name = self.profile.get("profile_name", "Unnamed Profile") + self.status_updater(f"Starting profile: '{profile_name}'...") + self.is_running = True + self._stop_requested = False + + gdb_exe = self._get_setting("general", "gdb_executable_path") + target_exe = self.profile.get("target_executable") + gdb_script_path = self._get_setting("general", "gdb_dumper_script_path") + + if not target_exe or not os.path.exists(target_exe): + self.status_updater(f"Error: Target executable '{target_exe}' not found for profile '{profile_name}'.") + logger.error(f"Target executable '{target_exe}' not found for profile '{profile_name}'.") + self.is_running = False + return + + try: + self.gdb_session = GDBSession( + gdb_path=gdb_exe, + executable_path=target_exe, + gdb_script_full_path=gdb_script_path, + dumper_options=self._get_dumper_options() + ) + + startup_timeout = self._get_setting("timeouts", "gdb_start", 30) + self.status_updater(f"Profile '{profile_name}': Spawning GDB for '{os.path.basename(target_exe)}'...") + self.gdb_session.start(timeout=startup_timeout) + self.gdb_output_writer(f"GDB session started for profile '{profile_name}'.\n") + if gdb_script_path and self.gdb_session.gdb_script_sourced_successfully: + self.gdb_output_writer(f"GDB dumper script '{os.path.basename(gdb_script_path)}' sourced successfully.\n") + elif gdb_script_path: + self.gdb_output_writer(f"Warning: GDB dumper script '{os.path.basename(gdb_script_path)}' failed to load.\n") + + + # --- Simplified Execution Logic for the first action and first variable --- + actions = self.profile.get("actions", []) + if not actions: + self.status_updater(f"Profile '{profile_name}' has no actions defined. Stopping.") + logger.warning(f"Profile '{profile_name}' has no actions.") + self._cleanup_session() + return + + # For now, just process the first action + first_action = actions[0] + bp_location = first_action.get("breakpoint_location") + vars_to_dump = first_action.get("variables_to_dump", []) + continue_after = first_action.get("continue_after_dump", True) + + if not bp_location: + self.status_updater(f"Profile '{profile_name}': First action has no breakpoint. Stopping.") + logger.error(f"Profile '{profile_name}': First action missing breakpoint.") + self._cleanup_session() + return + + if not vars_to_dump: + self.status_updater(f"Profile '{profile_name}': First action at '{bp_location}' has no variables to dump. Setting BP and running.") + # We might still want to run to the breakpoint even if no vars are dumped. + # For now, let's proceed if there's a BP. + + # Set breakpoint + self.status_updater(f"Profile '{profile_name}': Setting breakpoint at '{bp_location}'...") + cmd_timeout = self._get_setting("timeouts", "gdb_command", 30) + bp_output = self.gdb_session.set_breakpoint(bp_location, timeout=cmd_timeout) + self.gdb_output_writer(bp_output) + if "Breakpoint" not in bp_output and "pending" not in bp_output.lower(): + self.status_updater(f"Error: Failed to set breakpoint '{bp_location}'. Check GDB output.") + logger.error(f"Failed to set breakpoint '{bp_location}' for profile '{profile_name}'. Output: {bp_output}") + self._cleanup_session() + return + + # Run program + program_params = self.profile.get("program_parameters", "") + self.status_updater(f"Profile '{profile_name}': Running program '{os.path.basename(target_exe)} {program_params}'...") + run_timeout = self._get_setting("timeouts", "program_run_continue", 120) + run_output = self.gdb_session.run_program(program_params, timeout=run_timeout) + self.gdb_output_writer(run_output) + + if self._stop_requested: + self.status_updater(f"Profile '{profile_name}' execution stopped by user request.") + self._cleanup_session() + return + + # Check if breakpoint was hit + if "Breakpoint" in run_output or \ + (hasattr(self.gdb_session.child, 'before') and "Breakpoint" in self.gdb_session.child.before): # Check 'before' as well + self.status_updater(f"Profile '{profile_name}': Hit breakpoint at '{bp_location}'.") + + if vars_to_dump: + var_to_dump = vars_to_dump[0] # Just the first one for now + self.status_updater(f"Profile '{profile_name}': Dumping variable '{var_to_dump}'...") + dump_timeout = self._get_setting("timeouts", "dump_variable", 60) + + if not self.gdb_session.gdb_script_sourced_successfully: + msg = f"Profile '{profile_name}': GDB Dumper script not available/loaded. Cannot dump '{var_to_dump}'." + self.status_updater(msg) + logger.warning(msg) + self.json_data_handler({"_profile_executor_error": msg}) + else: + dumped_json = self.gdb_session.dump_variable_to_json(var_to_dump, timeout=dump_timeout) + self.json_data_handler(dumped_json) # Send to GUI/logger + self.gdb_output_writer(f"Dumped '{var_to_dump}': {str(dumped_json)[:200]}...\n") # Also to GDB raw output for now + + if isinstance(dumped_json, dict) and "_gdb_tool_error" in dumped_json: + self.status_updater(f"Error dumping '{var_to_dump}': {dumped_json.get('details', '')}") + else: + self.status_updater(f"Successfully dumped '{var_to_dump}'.") + # Placeholder for saving to file later + logger.info(f"Profile '{profile_name}' - Dumped data for '{var_to_dump}': {str(dumped_json)[:100]}...") + + + if continue_after: + if self._stop_requested: + self.status_updater(f"Profile '{profile_name}' execution stopped by user request before continue.") + self._cleanup_session() + return + self.status_updater(f"Profile '{profile_name}': Continuing execution...") + continue_output = self.gdb_session.continue_execution(timeout=run_timeout) + self.gdb_output_writer(continue_output) + # Rudimentary: check for program exit after continue + if "Program exited normally" in continue_output or "exited with code" in continue_output: + self.status_updater(f"Profile '{profile_name}': Program exited after continue.") + else: + self.status_updater(f"Profile '{profile_name}': Program continued. Further automatic steps not yet implemented.") + else: + self.status_updater(f"Profile '{profile_name}': Execution paused at '{bp_location}' as per profile action (continue_after_dump is false).") + + elif "Program exited normally" in run_output or "exited with code" in run_output: + self.status_updater(f"Profile '{profile_name}': Program exited before hitting breakpoint '{bp_location}'.") + else: + self.status_updater(f"Profile '{profile_name}': Program did not hit breakpoint '{bp_location}' as expected. Output: {run_output[:100]}...") + + + except FileNotFoundError as fnf_e: + err_msg = f"Error running profile '{profile_name}': File not found - {fnf_e}" + self.status_updater(err_msg) + logger.error(err_msg, exc_info=True) + except (ConnectionError, TimeoutError) as session_e: + err_msg = f"Session error running profile '{profile_name}': {type(session_e).__name__} - {session_e}" + self.status_updater(err_msg) + logger.error(err_msg, exc_info=True) + except Exception as e: + err_msg = f"Unexpected error running profile '{profile_name}': {type(e).__name__} - {e}" + self.status_updater(err_msg) + logger.critical(err_msg, exc_info=True) + finally: + self._cleanup_session() + self.status_updater(f"Profile '{profile_name}' execution finished.") + self.is_running = False + + def request_stop(self) -> None: + """Requests the profile execution to stop gracefully.""" + self.status_updater("Stop requested for current profile execution...") + self._stop_requested = True + # If GDB session is in a blocking expect, this won't have immediate effect + # until GDB returns control. For true async stop, gdb_session would need interrupt. + + def _cleanup_session(self) -> None: + """Cleans up the GDB session.""" + if self.gdb_session and self.gdb_session.is_alive(): + self.status_updater("Cleaning up GDB session...") + quit_timeout = self._get_setting("timeouts", "gdb_quit", 10) + # If program might be running, try to kill it first, then quit + if self.gdb_session.child and self.gdb_session.child.isalive(): # Check if child process exists + # A more robust check would be to see if GDB thinks a program is loaded/running + # For now, assume we might need to kill if a breakpoint was hit or run was issued. + try: + kill_timeout = self._get_setting("timeouts", "kill_program", 20) + # Only kill if it's likely the inferior is running. + # This is tricky. GDB might not have an inferior if it exited. + # For simplicity now, just attempt quit. A more robust kill would be needed + # if the program is left running and blocks quit. + # kill_output = self.gdb_session.kill_program(timeout=kill_timeout) + # self.gdb_output_writer(f"Kill attempt during cleanup: {kill_output}\n") + pass # Skip kill for now to avoid complexity if program already exited + except Exception as e_kill: + logger.warning(f"Exception during kill in cleanup: {e_kill}") + + self.gdb_session.quit(timeout=quit_timeout) + self.gdb_output_writer("GDB session quit during cleanup.\n") + self.gdb_session = None + logger.info("ProfileExecutor GDB session cleaned up.") \ No newline at end of file diff --git a/cpp_python_debug/gui/main_window.py b/cpp_python_debug/gui/main_window.py index f1cf781..51e3634 100644 --- a/cpp_python_debug/gui/main_window.py +++ b/cpp_python_debug/gui/main_window.py @@ -1,5 +1,5 @@ # File: cpp_python_debug/gui/main_window.py -# Provides the Tkinter GUI for interacting with the GDB session. +# Provides the Tkinter GUI for interacting with the GDB session and automated profiles. import tkinter as tk from tkinter import filedialog, messagebox, ttk, scrolledtext, Menu @@ -7,13 +7,17 @@ import logging import os import json # For pretty-printing JSON in the GUI import re +import threading # For running profile executor in a separate thread +from typing import Optional, Dict, Any + # Relative imports for modules within the same package from ..core.gdb_controller import GDBSession from ..core.output_formatter import save_to_json, save_to_csv from ..core.config_manager import AppSettings -from .config_window import ConfigWindow -from .profile_manager_window import ProfileManagerWindow +from ..core.profile_executor import ProfileExecutor # NEW IMPORT +from .config_window import ConfigWindow +from .profile_manager_window import ProfileManagerWindow logger = logging.getLogger(__name__) @@ -22,21 +26,19 @@ class GDBGui(tk.Tk): super().__init__() self.app_settings = AppSettings() - self.gui_log_handler = None self.title(f"GDB Debug GUI - Settings: {os.path.basename(self.app_settings.config_filepath)}") - self.geometry(self.app_settings.get_setting("gui", "main_window_geometry", "850x650")) # Adjusted height slightly + self.geometry(self.app_settings.get_setting("gui", "main_window_geometry", "850x650")) - self.gdb_session = None - self.last_dumped_data = None - self.program_started_once = False + self.gdb_session: Optional[GDBSession] = None # For manual GDB session + self.last_dumped_data: Any = None # For manual dump + self.program_started_once: bool = False # For manual run/continue logic - # MODIFIED: StringVars for status display of critical paths self.gdb_exe_status_var = tk.StringVar(value="GDB: Checking...") self.gdb_dumper_status_var = tk.StringVar(value="Dumper Script: Checking...") - # Tkinter StringVars for other input fields remain + # StringVars for manual debug input fields self.exe_path_var = tk.StringVar( value=self.app_settings.get_setting("general", "last_target_executable_path", "") ) @@ -50,153 +52,104 @@ class GDBGui(tk.Tk): value=self.app_settings.get_setting("general", "default_program_parameters", "") ) - self._create_menus() - self._create_widgets() # Widgets creation first - self._setup_logging_redirect_to_gui() + # For Automated Profile Execution Tab + self.profile_executor_instance: Optional[ProfileExecutor] = None + self.available_profiles_map: Dict[str, Dict[str, Any]] = {} # Maps display name to profile data + self.profile_exec_status_var = tk.StringVar(value="Select a profile to run.") # Status for auto execution - # MODIFIED: Initial check of critical configurations - self._check_critical_configs_and_update_gui() + self._create_menus() + self._create_widgets() + self._setup_logging_redirect_to_gui() + self._check_critical_configs_and_update_gui() + self._load_and_populate_profiles_for_automation_tab() self.protocol("WM_DELETE_WINDOW", self._on_closing_window) - # REMOVED: _initialize_gdb_dumper_script_path (handled by AppSettings and ConfigWindow) - def _create_menus(self): self.menubar = Menu(self) self.config(menu=self.menubar) options_menu = Menu(self.menubar, tearoff=0) self.menubar.add_cascade(label="Options", menu=options_menu) - options_menu.add_command(label="Configure Application...", command=self._open_config_window) options_menu.add_separator() options_menu.add_command(label="Exit", command=self._on_closing_window) - + profiles_menu = Menu(self.menubar, tearoff=0) self.menubar.add_cascade(label="Profiles", menu=profiles_menu) profiles_menu.add_command(label="Manage Profiles...", command=self._open_profile_manager_window) - - def _open_profile_manager_window(self): - logger.info("Opening Profile Manager window.") - # Pass 'self' as parent and self.app_settings - profile_win = ProfileManagerWindow(self, self.app_settings) - self.wait_window(profile_win) # Makes it modal - logger.info("Profile Manager window closed.") + # Dynamic list of profiles for quick run could be added here later def _open_config_window(self): logger.debug("Opening configuration window.") config_win = ConfigWindow(self, self.app_settings) - self.wait_window(config_win) # Makes the config window modal + self.wait_window(config_win) logger.debug("Configuration window closed.") - self._check_critical_configs_and_update_gui() # Refresh status and button states + self._check_critical_configs_and_update_gui() + + def _open_profile_manager_window(self): + logger.info("Opening Profile Manager window.") + profile_win = ProfileManagerWindow(self, self.app_settings) + self.wait_window(profile_win) + logger.info("Profile Manager window closed.") + self._load_and_populate_profiles_for_automation_tab() # Refresh profiles list def _check_critical_configs_and_update_gui(self): - """ - Checks critical GDB and Dumper script configurations and updates GUI elements accordingly. - Enables/disables session control buttons based on configuration validity. - """ logger.info("Checking critical configurations (GDB executable and Dumper script).") gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path") dumper_script_path = self.app_settings.get_setting("general", "gdb_dumper_script_path") gdb_ok = False - dumper_ok = False # Dumper is optional, but its status is still relevant - - # Check GDB executable - if gdb_exe_path and os.path.isfile(gdb_exe_path): # Check if it's a file, more robust than just exists + if gdb_exe_path and os.path.isfile(gdb_exe_path): self.gdb_exe_status_var.set(f"GDB: {os.path.basename(gdb_exe_path)} (OK)") gdb_ok = True - elif gdb_exe_path: # Path is set but not a valid file + elif gdb_exe_path: self.gdb_exe_status_var.set(f"GDB: '{gdb_exe_path}' (Not Found/Invalid!)") - else: # Path not set + else: self.gdb_exe_status_var.set("GDB: Not Configured! Please set in Options > Configure.") - # Check GDB Dumper script (optional) if dumper_script_path and os.path.isfile(dumper_script_path): self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(dumper_script_path)} (OK)") - dumper_ok = True # Not strictly necessary for dumper_ok to be true for session start - elif dumper_script_path: # Path is set but not a valid file + elif dumper_script_path: self.gdb_dumper_status_var.set(f"Dumper: '{dumper_script_path}' (Not Found/Invalid!)") - else: # Path not set + else: self.gdb_dumper_status_var.set("Dumper: Not Configured (Optional).") - dumper_ok = True # Considered "OK" in the sense that it's not misconfigured if empty - # Update button states if gdb_ok: - self.start_gdb_button.config(state=tk.NORMAL) - # Other buttons remain disabled until GDB session starts - if self.gdb_session and self.gdb_session.is_alive(): - # This case should ideally not happen here as _reset_gui_to_stopped_state - # would have been called if GDB session was active and then config changed. - # But as a safeguard: - self.set_bp_button.config(state=tk.NORMAL) - # self.run_button state depends on breakpoint - # self.dump_var_button state depends on hitting breakpoint & dumper script - self.stop_gdb_button.config(state=tk.NORMAL) - else: # No active session, reset dependent buttons - self.set_bp_button.config(state=tk.DISABLED) - self.run_button.config(state=tk.DISABLED, text="3. Run Program") - self.dump_var_button.config(state=tk.DISABLED) - self.stop_gdb_button.config(state=tk.DISABLED) - else: # GDB not configured or invalid - self._reset_gui_to_stopped_state() # This will disable all session buttons - self.start_gdb_button.config(state=tk.DISABLED) # Explicitly ensure start is disabled - - # The dump_var_button's state also depends on whether the dumper script is OK - # and if a breakpoint is hit. This is handled in _run_or_continue_gdb_action. - # Here, we primarily care about enabling the GDB start. + # Only enable manual start if no profile is running + if not (self.profile_executor_instance and self.profile_executor_instance.is_running): + self.start_gdb_button.config(state=tk.NORMAL) + # Other manual buttons depend on GDB session state + else: + self._reset_gui_to_stopped_state() # This will disable all manual session buttons + self.start_gdb_button.config(state=tk.DISABLED) - # Update main window title to reflect config file self.title(f"GDB Debug GUI - Settings: {os.path.basename(self.app_settings.config_filepath)}") - def _create_widgets(self): main_frame = ttk.Frame(self, padding="10") main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) - self.columnconfigure(0, weight=1) # Permette a main_frame di espandersi orizzontalmente - self.rowconfigure(0, weight=1) # Permette a main_frame di espandersi verticalmente + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) - # Configura le righe di main_frame per l'espansione - # row 0: config_status_frame (non espandibile verticalmente) - # row 1: mode_notebook (moderatamente espandibile o fisso) - # row 2: output_log_notebook (massima espansione verticale) - # row 3: status_bar (non espandibile verticalmente) - main_frame.rowconfigure(0, weight=0) - main_frame.rowconfigure(1, weight=1) # Diamo un po' di peso al notebook delle modalità - main_frame.rowconfigure(2, weight=3) # Diamo più peso al notebook dei log per espansione - main_frame.rowconfigure(3, weight=0) - main_frame.columnconfigure(0, weight=1) # Permette espansione orizzontale dei figli + main_frame.rowconfigure(0, weight=0) # Config Status + main_frame.rowconfigure(1, weight=1) # Mode Notebook + main_frame.rowconfigure(2, weight=3) # Output/Log Notebook + main_frame.rowconfigure(3, weight=0) # Status Bar + main_frame.columnconfigure(0, weight=1) - # --- 1. Frame Stato Configurazione Critica (IN ALTO) --- self._create_config_status_widgets(main_frame) - - # --- 2. Notebook per Modalità di Debug (Manuale / Automatico) --- self._create_mode_notebook_widgets(main_frame) - - # --- 3. Notebook per Output e Log (IN BASSO, SOPRA LA STATUS BAR) --- self._create_output_log_widgets(main_frame) - - # --- 4. Barra di Stato (IN FONDO) --- self._create_status_bar(main_frame) def _create_config_status_widgets(self, parent_frame: ttk.Frame): - """Crea i widget per lo stato della configurazione critica, tutti su una riga.""" - config_status_frame = ttk.LabelFrame(parent_frame, text="Critical Configuration Status", padding=(10, 5, 10, 10)) # Aggiustato padding + config_status_frame = ttk.LabelFrame(parent_frame, text="Critical Configuration Status", padding=(10, 5, 10, 10)) config_status_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5, padx=0) - # Configura le colonne per distribuire lo spazio - # Colonna 0: Label "GDB Executable:" - # Colonna 1: Status GDB (espandibile) - # Colonna 2: Label "GDB Dumper Script:" - # Colonna 3: Status Dumper (espandibile) - # Colonna 4: Bottone "Open Configuration..." - config_status_frame.columnconfigure(0, weight=0) # Label fissa - config_status_frame.columnconfigure(1, weight=1) # Status espandibile - config_status_frame.columnconfigure(2, weight=0) # Label fissa - config_status_frame.columnconfigure(3, weight=1) # Status espandibile - config_status_frame.columnconfigure(4, weight=0) # Bottone fisso + config_status_frame.columnconfigure(1, weight=1) + config_status_frame.columnconfigure(3, weight=1) - # Riga 0 ttk.Label(config_status_frame, text="GDB:").grid(row=0, column=0, sticky=tk.W, padx=(5,0), pady=5) self.gdb_exe_status_label = ttk.Label(config_status_frame, textvariable=self.gdb_exe_status_var, relief="sunken", padding=(5,2), anchor=tk.W) self.gdb_exe_status_label.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0,10), pady=5) @@ -210,27 +163,21 @@ class GDBGui(tk.Tk): ) def _create_mode_notebook_widgets(self, parent_frame: ttk.Frame): - """Crea il Notebook per le modalità di debug (Manuale, Automatico).""" mode_notebook = ttk.Notebook(parent_frame) - # MODIFIED: sticky per espandere in tutte le direzioni, padx per allineare mode_notebook.grid(row=1, column=0, columnspan=1, sticky='nsew', pady=5, padx=0) - # Tab: Manual Debug manual_debug_frame = ttk.Frame(mode_notebook, padding="5") mode_notebook.add(manual_debug_frame, text="Manual Debug") self._populate_manual_debug_tab(manual_debug_frame) - # Tab: Automatic Debug (Placeholder) - automatic_debug_frame = ttk.Frame(mode_notebook, padding="10") - mode_notebook.add(automatic_debug_frame, text="Automatic Debug") - ttk.Label(automatic_debug_frame, text="Automated profile execution controls will be here.").pack(padx=5, pady=5) + self.automated_exec_frame = ttk.Frame(mode_notebook, padding="10") + mode_notebook.add(self.automated_exec_frame, text="Automated Profile Execution") + self._populate_automated_execution_tab(self.automated_exec_frame) def _populate_manual_debug_tab(self, parent_tab_frame: ttk.Frame): - """Popola la scheda "Manual Debug" con i controlli necessari.""" - parent_tab_frame.columnconfigure(0, weight=1) # Permetti al contenuto di espandersi + parent_tab_frame.columnconfigure(0, weight=1) - # Frame per Target & Debug Session Settings (precedentemente runtime_config_frame) manual_target_settings_frame = ttk.LabelFrame(parent_tab_frame, text="Target & Debug Session Settings", padding="10") manual_target_settings_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5) manual_target_settings_frame.columnconfigure(1, weight=1) @@ -257,12 +204,11 @@ class GDBGui(tk.Tk): ttk.Label(manual_target_settings_frame, text="Variable/Expression:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2) ttk.Entry(manual_target_settings_frame, textvariable=self.variable_var).grid(row=row_idx, column=1, columnspan=2, sticky=(tk.W, tk.E), padx=5, pady=2) - # Frame per Session Control (precedentemente control_frame) manual_session_control_frame = ttk.LabelFrame(parent_tab_frame, text="Session Control", padding="10") manual_session_control_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(10,5)) - button_flow_frame = ttk.Frame(manual_session_control_frame) # Contenitore per i bottoni - button_flow_frame.pack(fill=tk.X, expand=True) # Permetti ai bottoni di distribuirsi + button_flow_frame = ttk.Frame(manual_session_control_frame) + button_flow_frame.pack(fill=tk.X, expand=True) self.start_gdb_button = ttk.Button(button_flow_frame, text="1. Start GDB", command=self._start_gdb_session_action, state=tk.DISABLED) self.start_gdb_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True) @@ -279,7 +225,6 @@ class GDBGui(tk.Tk): self.stop_gdb_button = ttk.Button(button_flow_frame, text="Stop GDB", command=self._stop_gdb_session_action, state=tk.DISABLED) self.stop_gdb_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True) - # Frame per Save Dumped Data (precedentemente save_frame) manual_save_data_frame = ttk.LabelFrame(parent_tab_frame, text="Save Dumped Data", padding="10") manual_save_data_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=5) @@ -289,13 +234,76 @@ class GDBGui(tk.Tk): self.save_csv_button = ttk.Button(manual_save_data_frame, text="Save as CSV", command=lambda: self._save_dumped_data("csv"), state=tk.DISABLED) self.save_csv_button.pack(side=tk.LEFT, padx=5, pady=5) + def _populate_automated_execution_tab(self, parent_tab_frame: ttk.Frame) -> None: + """Populates the tab for automated profile execution.""" + parent_tab_frame.columnconfigure(0, weight=1) # Consenti al frame principale del tab di espandersi + + # Frame per Profile Selection e Control, ora con una griglia più complessa + auto_control_frame = ttk.LabelFrame(parent_tab_frame, text="Profile Execution Control", padding="10") + auto_control_frame.grid(row=0, column=0, sticky="ew", pady=5) + + # Configura le colonne del auto_control_frame per il layout desiderato: + # Colonna 0: Label "Select Profile:" + # Colonna 1: Combobox (espandibile) + # Colonna 2: Bottone Run + # Colonna 3: Bottone Stop + auto_control_frame.columnconfigure(0, weight=0) # Label fissa + auto_control_frame.columnconfigure(1, weight=1) # Combobox espandibile + auto_control_frame.columnconfigure(2, weight=0) # Bottone Run fisso + auto_control_frame.columnconfigure(3, weight=0) # Bottone Stop fisso + + # Riga 0: Label, Combobox, Run Button, Stop Button + ttk.Label(auto_control_frame, text="Select Profile:").grid(row=0, column=0, padx=(5,2), pady=5, sticky="w") + + self.profile_selection_combo = ttk.Combobox(auto_control_frame, state="readonly", width=35, # Larghezza leggermente ridotta + textvariable=tk.StringVar()) + self.profile_selection_combo.grid(row=0, column=1, padx=(0,5), pady=5, sticky="ew") + + self.run_profile_button = ttk.Button(auto_control_frame, text="Run Profile", command=self._run_selected_profile_action, state=tk.DISABLED) + self.run_profile_button.grid(row=0, column=2, padx=(0,2), pady=5, sticky="ew") + + self.stop_profile_button = ttk.Button(auto_control_frame, text="Stop Profile", command=self._stop_current_profile_action, state=tk.DISABLED) + self.stop_profile_button.grid(row=0, column=3, padx=(0,5), pady=5, sticky="ew") + + # Riga 1: Status label (sotto i controlli) + ttk.Label(auto_control_frame, textvariable=self.profile_exec_status_var, relief=tk.SUNKEN, anchor=tk.W, padding=3).grid( + row=1, column=0, columnspan=4, sticky="ew", padx=5, pady=(10,5), ipady=2 # columnspan=4 per coprire tutte le colonne + ) + + def _load_and_populate_profiles_for_automation_tab(self) -> None: + self.available_profiles_map.clear() + profiles = self.app_settings.get_profiles() + profile_display_names = [] + for profile_item in profiles: # Renamed to avoid conflict with menubar 'profiles_menu' + name = profile_item.get("profile_name") + if name: + self.available_profiles_map[name] = profile_item + profile_display_names.append(name) + + sorted_names = sorted(profile_display_names) + self.profile_selection_combo['values'] = sorted_names + + # Safely get the textvariable and set its value + combo_text_var = self.profile_selection_combo.cget("textvariable") + if isinstance(combo_text_var, str): # If it's a string name of a tk variable + # This case is less common if we assign a tk.StringVar() directly + # For safety, one might re-fetch or ensure it's a StringVar instance. + # For now, assuming direct StringVar assignment or it works. + pass + + if sorted_names: + self.profile_selection_combo.set(sorted_names[0]) # Set current value using set() + self.run_profile_button.config(state=tk.NORMAL) + self.profile_exec_status_var.set(f"Ready to run profile: {self.profile_selection_combo.get()}") + else: + self.profile_selection_combo.set("") + self.run_profile_button.config(state=tk.DISABLED) + self.profile_exec_status_var.set("No profiles. Create one via 'Profiles > Manage Profiles'.") + def _create_output_log_widgets(self, parent_frame: ttk.Frame): - """Crea il Notebook per i log e l'output JSON.""" output_log_notebook = ttk.Notebook(parent_frame) - # MODIFIED: sticky per espandere, padx per allineare output_log_notebook.grid(row=2, column=0, columnspan=1, sticky='nsew', pady=(5,0), padx=0) - # Le ScrolledText ora vengono create qui e aggiunte al notebook self.gdb_raw_output_text = scrolledtext.ScrolledText(output_log_notebook, wrap=tk.WORD, height=10, state=tk.DISABLED, font=("Consolas", 9)) output_log_notebook.add(self.gdb_raw_output_text, text="GDB Raw Output") @@ -306,10 +314,8 @@ class GDBGui(tk.Tk): output_log_notebook.add(self.app_log_text, text="Application Log") def _create_status_bar(self, parent_frame: ttk.Frame): - """Crea la barra di stato.""" self.status_var = tk.StringVar(value="Ready. Configure GDB via Options menu if needed.") status_bar = ttk.Label(parent_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) - # MODIFIED: sticky per espandere orizzontalmente, padx per allineare status_bar.grid(row=3, column=0, columnspan=1, sticky=(tk.W, tk.E), pady=(5,0), ipady=2, padx=0) def _setup_logging_redirect_to_gui(self): @@ -328,16 +334,17 @@ class GDBGui(tk.Tk): path = filedialog.askopenfilename( title=title, filetypes=filetypes or [("All files", "*.*")], - initialdir=initial_dir + initialdir=initial_dir, + parent=self ) if path: target_var.set(path) - # REMOVED: _browse_gdb_exe and _browse_gdb_script (handled in ConfigWindow) - def _browse_target_exe(self): # This one remains as target exe is configured in main GUI - self._browse_file("Select Target Application Executable", self.exe_path_var, [("Executable files", "*.exe"), ("All files", "*.*")]) + def _browse_target_exe(self): + self._browse_file("Select Target Application Executable", self.exe_path_var, [("Executable files", ("*.exe","*")), ("All files", "*.*")]) - def _update_gdb_raw_output(self, text: str, append: bool = True): # No changes + def _update_gdb_raw_output(self, text: str, append: bool = True): + if not hasattr(self, 'gdb_raw_output_text') or not self.gdb_raw_output_text.winfo_exists(): return self.gdb_raw_output_text.config(state=tk.NORMAL) if append: self.gdb_raw_output_text.insert(tk.END, str(text) + "\n") else: @@ -346,7 +353,8 @@ class GDBGui(tk.Tk): self.gdb_raw_output_text.see(tk.END) self.gdb_raw_output_text.config(state=tk.DISABLED) - def _update_parsed_json_output(self, data_to_display: any): # No changes + def _update_parsed_json_output(self, data_to_display: any): + if not hasattr(self, 'parsed_json_output_text') or not self.parsed_json_output_text.winfo_exists(): return self.parsed_json_output_text.config(state=tk.NORMAL) self.parsed_json_output_text.delete("1.0", tk.END) if data_to_display is None: self.parsed_json_output_text.insert("1.0", "") @@ -361,17 +369,22 @@ class GDBGui(tk.Tk): self.parsed_json_output_text.see("1.0") self.parsed_json_output_text.config(state=tk.DISABLED) - def _update_status_bar(self, message: str, is_error: bool = False): # No changes - self.status_var.set(message) + def _update_status_bar(self, message: str, is_error: bool = False): + if hasattr(self, 'status_var'): self.status_var.set(message) - def _handle_gdb_operation_error(self, operation_name: str, error_details: any): # No changes + def _handle_gdb_operation_error(self, operation_name: str, error_details: any): error_message = f"Error during GDB operation '{operation_name}': {error_details}" logger.error(error_message) self._update_gdb_raw_output(f"ERROR: {error_message}\n", append=True) self._update_status_bar(f"Error: {operation_name} failed.", is_error=True) - messagebox.showerror("GDB Operation Error", error_message) + if self.winfo_exists(): # Check if window still exists before showing messagebox + messagebox.showerror("GDB Operation Error", error_message, parent=self) def _start_gdb_session_action(self): + if self.profile_executor_instance and self.profile_executor_instance.is_running: + messagebox.showwarning("Profile Running", "An automated profile is running. Please stop it first.", parent=self) + return + gdb_exe = self.app_settings.get_setting("general", "gdb_executable_path") target_exe = self.exe_path_var.get() gdb_script = self.app_settings.get_setting("general", "gdb_dumper_script_path") @@ -395,7 +408,6 @@ class GDBGui(tk.Tk): gdb_script = None self.gdb_dumper_status_var.set(f"Dumper: '{self.app_settings.get_setting('general', 'gdb_dumper_script_path')}' (Not Found!)") - if self.gdb_session and self.gdb_session.is_alive(): messagebox.showwarning("Session Active", "A GDB session is already active. Please stop it first.", parent=self) return @@ -404,23 +416,16 @@ class GDBGui(tk.Tk): self._update_gdb_raw_output("Attempting to start GDB session...\n", append=False) self._update_parsed_json_output(None) try: - startup_timeout = self.app_settings.get_setting("timeouts", "gdb_start", 30) # Added default for safety - - # MODIFIED: Correctly get the dumper_options dictionary + startup_timeout = self.app_settings.get_setting("timeouts", "gdb_start", 30) current_dumper_options = self.app_settings.get_category_settings("dumper_options", {}) - logger.debug(f"Attempting to create GDBSession with dumper_options: type={type(current_dumper_options)}, value='{str(current_dumper_options)[:200]}...'") - self.gdb_session = GDBSession( gdb_path=gdb_exe, executable_path=target_exe, gdb_script_full_path=gdb_script, dumper_options=current_dumper_options ) - - logger.debug("GDBSession instance created. Calling start()...") self.gdb_session.start(timeout=startup_timeout) - logger.debug("GDBSession start() returned.") self._update_gdb_raw_output(f"GDB session started for '{os.path.basename(target_exe)}'.\n") if gdb_script and self.gdb_session.gdb_script_sourced_successfully: @@ -431,13 +436,14 @@ class GDBGui(tk.Tk): self._update_gdb_raw_output(f"Warning: GDB dumper script '{os.path.basename(gdb_script)}' specified but failed to load.\n", append=True) self._update_status_bar(f"GDB active. Dumper script issues (check logs).", is_error=True) self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Load Failed!)") - messagebox.showwarning("Dumper Script Issue", + if self.winfo_exists(): + messagebox.showwarning("Dumper Script Issue", f"The GDB dumper script '{os.path.basename(gdb_script)}' may have failed to load.\n" "JSON dumping might be affected. Check logs.", parent=self) else: self._update_gdb_raw_output("No GDB dumper script. JSON dump via script unavailable.\n", append=True) self._update_status_bar("GDB session active. No dumper script.") - if not self.app_settings.get_setting("general", "gdb_dumper_script_path"): # Check specific setting for this message + if not self.app_settings.get_setting("general", "gdb_dumper_script_path"): self.gdb_dumper_status_var.set("Dumper: Not Configured (Optional).") self.start_gdb_button.config(state=tk.DISABLED) @@ -445,12 +451,15 @@ class GDBGui(tk.Tk): self.run_button.config(state=tk.DISABLED, text="3. Run Program") self.dump_var_button.config(state=tk.DISABLED) self.stop_gdb_button.config(state=tk.NORMAL) + self.run_profile_button.config(state=tk.DISABLED) # Disable profile run when manual session starts + self.profile_selection_combo.config(state=tk.DISABLED) + + self.program_started_once = False self.last_dumped_data = None self._disable_save_buttons() except (FileNotFoundError, ConnectionError, TimeoutError) as e_specific: - logger.error(f"Specific error during GDB start: {type(e_specific).__name__}: {e_specific}", exc_info=False) self._handle_gdb_operation_error("start session", e_specific) self.gdb_session = None self._reset_gui_to_stopped_state() @@ -459,7 +468,6 @@ class GDBGui(tk.Tk): self._handle_gdb_operation_error("start session (unexpected from main_window catch-all)", e) self.gdb_session = None self._reset_gui_to_stopped_state() - # _check_critical_configs_and_update_gui() def _set_gdb_breakpoint_action(self): if not self.gdb_session or not self.gdb_session.is_alive(): @@ -502,7 +510,9 @@ class GDBGui(tk.Tk): output = "" run_timeout = self.app_settings.get_setting("timeouts", "program_run_continue") dumper_script_path = self.app_settings.get_setting("general", "gdb_dumper_script_path") - dumper_is_valid = dumper_script_path and os.path.isfile(dumper_script_path) and self.gdb_session.gdb_script_sourced_successfully + dumper_is_valid_and_loaded = dumper_script_path and os.path.isfile(dumper_script_path) and \ + self.gdb_session and self.gdb_session.gdb_script_sourced_successfully + if not self.program_started_once: params_str = self.params_var.get() @@ -516,7 +526,7 @@ class GDBGui(tk.Tk): self._update_gdb_raw_output(output, append=True) dump_button_state = tk.DISABLED - if dumper_is_valid: # Only enable dump if dumper is OK + if dumper_is_valid_and_loaded: dump_button_state = tk.NORMAL if "Breakpoint" in output or re.search(r"Hit Breakpoint \d+", output, re.IGNORECASE): @@ -531,7 +541,7 @@ class GDBGui(tk.Tk): self.program_started_once = False elif "received signal" in output.lower() or "segmentation fault" in output.lower(): self._update_status_bar("Program signal/crash. Check GDB output.", is_error=True) - self.dump_var_button.config(state=dump_button_state) # Still might want to dump + self.dump_var_button.config(state=dump_button_state) self.program_started_once = True self.run_button.config(text="3. Continue (Risky)") else: @@ -550,11 +560,12 @@ class GDBGui(tk.Tk): return dumper_script_path = self.app_settings.get_setting("general", "gdb_dumper_script_path") - if not dumper_script_path or not os.path.isfile(dumper_script_path) or not self.gdb_session.gdb_script_sourced_successfully: + if not dumper_script_path or not os.path.isfile(dumper_script_path) or \ + not self.gdb_session.gdb_script_sourced_successfully: messagebox.showwarning("Dumper Script Error", "GDB dumper script is not available, not found, or failed to load.\n" "JSON dump cannot proceed. Check configuration and logs.", parent=self) - self._check_critical_configs_and_update_gui() # Re-check to update dumper status display + self._check_critical_configs_and_update_gui() return var_expr = self.variable_var.get() @@ -590,11 +601,10 @@ class GDBGui(tk.Tk): self.last_dumped_data = None; self._disable_save_buttons(); self._update_parsed_json_output({"error": str(e)}) def _reset_gui_to_stopped_state(self): - # Determine if GDB path is currently valid to correctly set start_gdb_button state gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path") gdb_is_configured_correctly = gdb_exe_path and os.path.isfile(gdb_exe_path) - if gdb_is_configured_correctly: + if gdb_is_configured_correctly and not (self.profile_executor_instance and self.profile_executor_instance.is_running): self.start_gdb_button.config(state=tk.NORMAL) else: self.start_gdb_button.config(state=tk.DISABLED) @@ -604,20 +614,36 @@ class GDBGui(tk.Tk): self.dump_var_button.config(state=tk.DISABLED) self.stop_gdb_button.config(state=tk.DISABLED) + # Enable profile controls if GDB is ok and no profile is running + if gdb_is_configured_correctly and not (self.profile_executor_instance and self.profile_executor_instance.is_running): + if self.profile_selection_combo.get(): # If a profile is selected in combobox + self.run_profile_button.config(state=tk.NORMAL) + else: + self.run_profile_button.config(state=tk.DISABLED) + self.profile_selection_combo.config(state="readonly") + else: # If GDB not ok, or profile is running + self.run_profile_button.config(state=tk.DISABLED) + if self.profile_executor_instance and self.profile_executor_instance.is_running: + self.profile_selection_combo.config(state=tk.DISABLED) + else: + self.profile_selection_combo.config(state="readonly") + + self._disable_save_buttons() self.program_started_once = False self.last_dumped_data = None - self._update_status_bar("GDB session stopped or not active.") - # REMOVED: self._check_critical_configs_and_update_gui() # This was the source of recursion + if not (self.profile_executor_instance and self.profile_executor_instance.is_running): + self._update_status_bar("GDB session stopped or not active.") + # No need to call _check_critical_configs_and_update_gui() here as it caused recursion. + # It's called at specific points like startup or after config changes. - - def _stop_gdb_session_action(self): + def _stop_gdb_session_action(self): # For manual session if self.gdb_session and self.gdb_session.is_alive(): self._update_status_bar("Stopping GDB session...") try: kill_timeout = self.app_settings.get_setting("timeouts", "kill_program") quit_timeout = self.app_settings.get_setting("timeouts", "gdb_quit") - if self.program_started_once: + if self.program_started_once: # Only kill if program was actually run kill_output = self.gdb_session.kill_program(timeout=kill_timeout) self._update_gdb_raw_output(f"Kill command output:\n{kill_output}\n", append=True) self.gdb_session.quit(timeout=quit_timeout) @@ -626,30 +652,33 @@ class GDBGui(tk.Tk): self._handle_gdb_operation_error("stop session", e) finally: self.gdb_session = None - self._reset_gui_to_stopped_state() # This will also call _check_critical_configs + self._reset_gui_to_stopped_state() + self._load_and_populate_profiles_for_automation_tab() # Re-enable profile UI potentially else: - messagebox.showinfo("Info", "GDB session is not active or already stopped.", parent=self) - self._reset_gui_to_stopped_state() # Ensure GUI is reset and buttons updated + # messagebox.showinfo("Info", "GDB session is not active or already stopped.", parent=self) + self._reset_gui_to_stopped_state() + self._load_and_populate_profiles_for_automation_tab() - def _enable_save_buttons_if_data(self): # No changes + + def _enable_save_buttons_if_data(self): if self.last_dumped_data and not (isinstance(self.last_dumped_data, dict) and "_gdb_tool_error" in self.last_dumped_data): self.save_json_button.config(state=tk.NORMAL) self.save_csv_button.config(state=tk.NORMAL) else: self._disable_save_buttons() - def _disable_save_buttons(self): # No changes + def _disable_save_buttons(self): self.save_json_button.config(state=tk.DISABLED) self.save_csv_button.config(state=tk.DISABLED) - def _save_dumped_data(self, format_type: str): # No changes beyond parent=self for messageboxes + def _save_dumped_data(self, format_type: str): if self.last_dumped_data is None or \ (isinstance(self.last_dumped_data, dict) and "_gdb_tool_error" in self.last_dumped_data): messagebox.showwarning("No Data", "No valid data has been dumped to save.", parent=self) return file_ext = f".{format_type.lower()}"; file_types = [(f"{format_type.upper()} files", f"*{file_ext}"), ("All files", "*.*")] - var_name_suggestion = self.variable_var.get().replace(" ", "_").replace("*", "ptr").replace("->", "_") + var_name_suggestion = self.variable_var.get().replace(" ", "_").replace("*", "ptr").replace("->", "_").replace(":", "_") default_filename = f"{var_name_suggestion}_dump{file_ext}" if var_name_suggestion else f"gdb_dump{file_ext}" - filepath = filedialog.asksaveasfilename(defaultextension=file_ext, filetypes=file_types, title=f"Save Dumped Data as {format_type.upper()}", initialfile=default_filename) + filepath = filedialog.asksaveasfilename(defaultextension=file_ext, filetypes=file_types, title=f"Save Dumped Data as {format_type.upper()}", initialfile=default_filename, parent=self) if not filepath: return self._update_status_bar(f"Saving data as {format_type.upper()} to {os.path.basename(filepath)}...") try: @@ -668,13 +697,136 @@ class GDBGui(tk.Tk): messagebox.showerror("Save Error", f"Failed to save data to {filepath}:\n{e}", parent=self) self._update_status_bar(f"Error saving data as {format_type}.", is_error=True) + def _gui_status_update(self, message: str) -> None: + if hasattr(self, 'profile_exec_status_var') and self.profile_exec_status_var.get() : # Check if widget exists + self.profile_exec_status_var.set(message) + logger.info(f"ProfileExec Status: {message}") + + def _gui_gdb_output_update(self, message: str) -> None: + self._update_gdb_raw_output(message, append=True) + + def _gui_json_data_update(self, data: Any) -> None: + self._update_parsed_json_output(data) + + def _run_selected_profile_action(self) -> None: + selected_profile_name = self.profile_selection_combo.get() + if not selected_profile_name: + messagebox.showwarning("No Profile Selected", "Please select a profile to run.", parent=self) + return + + if self.profile_executor_instance and self.profile_executor_instance.is_running: + messagebox.showwarning("Profile Running", "A profile is already running. Please stop it first.", parent=self) + return + + if self.gdb_session and self.gdb_session.is_alive(): # Manual session active + messagebox.showerror("GDB Session Active", "A manual GDB session is active. Please stop it first via 'Manual Debug' tab.", parent=self) + return + + profile_data = self.available_profiles_map.get(selected_profile_name) + if not profile_data: + messagebox.showerror("Error", f"Could not find data for profile '{selected_profile_name}'.", parent=self) + return + + self.start_gdb_button.config(state=tk.DISABLED) + self.set_bp_button.config(state=tk.DISABLED) + self.run_button.config(state=tk.DISABLED) + self.dump_var_button.config(state=tk.DISABLED) + self.stop_gdb_button.config(state=tk.DISABLED) + + self.run_profile_button.config(state=tk.DISABLED) + self.stop_profile_button.config(state=tk.NORMAL) + self.profile_selection_combo.config(state=tk.DISABLED) + try: # Disable menu items + self.menubar.entryconfig("Profiles", state=tk.DISABLED) + self.menubar.entryconfig("Options", state=tk.DISABLED) + except tk.TclError: pass # In case menubar or entries are not yet fully available + + self.profile_executor_instance = ProfileExecutor( + profile_data, + self.app_settings, + status_update_callback=self._gui_status_update, + gdb_output_callback=self._gui_gdb_output_update, + json_output_callback=self._gui_json_data_update + ) + + self.profile_exec_status_var.set(f"Starting profile '{selected_profile_name}' in background...") + # Clear previous outputs before starting a new profile run + self._update_gdb_raw_output("", append=False) + self._update_parsed_json_output(None) + + executor_thread = threading.Thread(target=self._profile_executor_thread_target, daemon=True) + executor_thread.start() + + def _profile_executor_thread_target(self): + if self.profile_executor_instance: + try: + self.profile_executor_instance.run() + finally: + if self.winfo_exists(): # Check if main window still exists + self.after(0, self._on_profile_execution_finished) + + def _on_profile_execution_finished(self): + if not self.winfo_exists(): return # Window might have been destroyed + + # Get final status from executor instance if it's still around and set it. + # The instance might have already updated profile_exec_status_var directly. + final_status_message = "Profile execution finished." + if self.profile_executor_instance and hasattr(self.profile_executor_instance, 'status_updater'): + # This is a bit indirect; ideally, status_updater already set the final message + # or executor.run() returns a final status. For now, we take the current var value. + current_status = self.profile_exec_status_var.get() + if "Error:" in current_status or "failed" in current_status.lower(): + final_status_message = f"Profile finished with issues: {current_status}" + elif current_status and not current_status.startswith("Starting profile"): # If not default starting msg + final_status_message = f"Profile run completed. Last status: {current_status}" + self.profile_exec_status_var.set(final_status_message) + + # Re-enable UI components + self.run_profile_button.config(state=tk.NORMAL if self.profile_selection_combo.get() else tk.DISABLED) + self.stop_profile_button.config(state=tk.DISABLED) + self.profile_selection_combo.config(state="readonly") + try: + self.menubar.entryconfig("Profiles", state=tk.NORMAL) + self.menubar.entryconfig("Options", state=tk.NORMAL) + except tk.TclError: pass + + self._check_critical_configs_and_update_gui() # This will re-evaluate manual GDB buttons state + + self.profile_executor_instance = None + + + def _stop_current_profile_action(self) -> None: + if self.profile_executor_instance and self.profile_executor_instance.is_running: + self.profile_exec_status_var.set("Requesting profile stop...") + self.profile_executor_instance.request_stop() + self.stop_profile_button.config(state=tk.DISABLED) + else: + self.profile_exec_status_var.set("No profile currently running to stop.") + def _on_closing_window(self): logger.info("Window closing sequence initiated.") - # MODIFIED: Only save settings that are still managed by main_window's UI + active_profile_stop_requested = False + if self.profile_executor_instance and self.profile_executor_instance.is_running: + response = messagebox.askyesnocancel("Profile Running", + "An automated profile is currently running.\n" + "Do you want to stop it and exit?", + default=messagebox.CANCEL, parent=self) + if response is True: + self._stop_current_profile_action() + active_profile_stop_requested = True + # Give a small grace period for the thread to notice the stop request. + # This is not a perfect solution for immediate shutdown. + # A more robust solution would involve joining the thread with a timeout, + # or a more direct way to interrupt gdb_session. + # For now, we'll proceed after requesting stop. + logger.info("Requested stop for active profile. Proceeding with shutdown.") + elif response is None: + logger.info("User cancelled exit while automated profile is running.") + return + # If False (No), user wants to exit without stopping. + self.app_settings.set_setting("gui", "main_window_geometry", self.geometry()) - # GDB exe and dumper script paths are no longer in main_window StringVars directly tied to AppSettings here. - # They are managed via ConfigWindow. self.app_settings.set_setting("general", "last_target_executable_path", self.exe_path_var.get()) self.app_settings.set_setting("general", "default_breakpoint", self.breakpoint_var.get()) self.app_settings.set_setting("general", "default_variable_to_dump", self.variable_var.get()) @@ -682,31 +834,40 @@ class GDBGui(tk.Tk): save_success = self.app_settings.save_settings() if not save_success: - messagebox.showwarning("Settings Error", "Could not save application settings. Check logs.", parent=self) + if self.winfo_exists(): messagebox.showwarning("Settings Error", "Could not save application settings. Check logs.", parent=self) should_destroy = True - if self.gdb_session and self.gdb_session.is_alive(): - if messagebox.askokcancel("Quit GDB Session", "A GDB session is active. Stop it and exit?", parent=self): - logger.info("User chose to stop active GDB session and exit.") - self._stop_gdb_session_action() - else: - logger.info("User cancelled exit while GDB session is active.") + if self.gdb_session and self.gdb_session.is_alive(): + if self.winfo_exists() and messagebox.askokcancel("Quit GDB Session", "A manual GDB session is active. Stop it and exit?", parent=self): + logger.info("User chose to stop active manual GDB session and exit.") + self._stop_gdb_session_action() + elif self.winfo_exists(): # User chose not to stop manual GDB + logger.info("User cancelled exit while manual GDB session is active.") should_destroy = False + elif not self.winfo_exists(): # Window already gone + self._stop_gdb_session_action() # Try to stop anyway if should_destroy: logger.info("Proceeding with window destruction.") if self.gui_log_handler: - logger.debug("Removing GUI log handler.") logging.getLogger().removeHandler(self.gui_log_handler) self.gui_log_handler.close() - self.gui_log_handler = None + self.gui_log_handler = None # Important to break reference + + # Ensure the executor thread has a chance to finish if stop was requested + # This is still a bit tricky if the executor is deep in a blocking GDB call. + if active_profile_stop_requested: + # A join with timeout could be attempted on the thread here if we stored the thread. + # For now, rely on daemon=True and the request_stop flag. + logger.debug("Assuming profile executor thread will terminate due to stop request or daemon nature.") + self.destroy() logger.info("Tkinter window destroyed.") else: logger.info("Window destruction aborted by user.") -class ScrolledTextLogHandler(logging.Handler): # No changes +class ScrolledTextLogHandler(logging.Handler): def __init__(self, text_widget: scrolledtext.ScrolledText): super().__init__() self.text_widget = text_widget @@ -719,12 +880,12 @@ class ScrolledTextLogHandler(logging.Handler): # No changes self.text_widget.insert(tk.END, log_entry + "\n") self.text_widget.see(tk.END) self.text_widget.config(state=tk.DISABLED) - except tk.TclError as e: - print(f"ScrolledTextLogHandler TclError (widget likely destroyed): {e} - Record: {self.format(record)}") - self._active = False + except tk.TclError as e: # Handle widget destroyed error + # print(f"ScrolledTextLogHandler TclError (widget likely destroyed): {e} - Record: {self.format(record)}") + self._active = False # Stop trying to use a dead widget except Exception as e: - print(f"ScrolledTextLogHandler unexpected error: {e} - Record: {self.format(record)}") - self._active = False + # print(f"ScrolledTextLogHandler unexpected error: {e} - Record: {self.format(record)}") + self._active = False def close(self): self._active = False super().close() \ No newline at end of file