# File: cpp_python_debug/gui/main_window.py # 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 import logging import os import json import re import threading import subprocess import sys from datetime import datetime from typing import ( Optional, Dict, Any, Callable, List, Tuple, ) from cpp_python_debug.core.config_manager import AppSettings from cpp_python_debug.core.gdb_controller import GDBSession from cpp_python_debug.core.output_formatter import save_to_json, save_to_csv from cpp_python_debug.core.profile_executor import ProfileExecutor, ExecutionLogEntry from cpp_python_debug.core.file_utils import sanitize_filename_component from cpp_python_debug.gui.config_window import ConfigWindow from cpp_python_debug.gui.profile_manager_window import ProfileManagerWindow from cpp_python_debug.gui.dump_analysis_tab import DumpAnalysisTab logger = logging.getLogger(__name__) APP_TITLE_NAME = "Cpp-Python GDB Debug Helper" try: from cpp_python_debug import _version as app_version_module APP_VERSION_INFO_STRING = f"v{app_version_module.__version__}" if ( hasattr(app_version_module, "GIT_COMMIT_HASH") and app_version_module.GIT_COMMIT_HASH != "Unknown" ): APP_VERSION_INFO_STRING += f" (commit {app_version_module.GIT_COMMIT_HASH[:7]})" except ImportError: logger.warning( "cpp_python_debug._version.py not found, using default version display." ) APP_VERSION_INFO_STRING = "(Development Version)" MANUAL_DUMP_PROFILE_NAME_PLACEHOLDER = "ManualDump" MANUAL_DUMP_FILENAME_PATTERN = ( "{profile_name}_{app_name}_{breakpoint}_{variable}_{timestamp}{extension}" ) MANUAL_DUMPS_SUBFOLDER = ( "manual_gdb_dumps" # Sottocartella dentro la directory dei log dell'app ) GDB_DUMP_EXTENSION = ".gdbdump.json" class GDBGui(tk.Tk): # --- MODIFICA: Aggiornato costruttore per accettare percorsi --- def __init__(self, app_base_path: str, log_directory_path: str): super().__init__() self.app_base_path: str = app_base_path self.log_directory_path: str = log_directory_path logger.info( f"GDBGui initialized with base_path: {self.app_base_path}, log_dir: {self.log_directory_path}" ) # AppSettings ora potrebbe usare app_base_path se modificato per non usare più appdirs per il file principale # Se AppSettings è già stato modificato per usare get_app_base_path(), non serve passarlo. # Per ora, assumiamo che AppSettings sappia come trovare il suo file di config. self.app_settings = AppSettings( app_base_path_override=self.app_base_path ) # Passa la base se AppSettings la usa # --- FINE MODIFICA --- self.gui_log_handler: Optional[ScrolledTextLogHandler] = None self.title(f"{APP_TITLE_NAME} - {APP_VERSION_INFO_STRING}") logger.info(f"Settings file in use: {self.app_settings.config_filepath}") self.geometry( self.app_settings.get_setting("gui", "main_window_geometry", "850x800") ) self.gdb_session: Optional[GDBSession] = None self.last_manual_gdb_dump_filepath: Optional[str] = None self.program_started_once: bool = False self.gdb_exe_status_var = tk.StringVar(value="GDB: Checking...") self.gdb_dumper_status_var = tk.StringVar(value="Dumper Script: Checking...") default_general_settings = self.app_settings._get_default_settings().get( "general", {} ) self.exe_path_var = tk.StringVar( value=self.app_settings.get_setting( "general", "last_target_executable_path", default_general_settings.get("last_target_executable_path", ""), ) ) self.breakpoint_var = tk.StringVar( value=self.app_settings.get_setting( "general", "default_breakpoint", default_general_settings.get("default_breakpoint", "main"), ) ) self.variable_var = tk.StringVar( value=self.app_settings.get_setting( "general", "default_variable_to_dump", default_general_settings.get("default_variable_to_dump", ""), ) ) self.params_var = tk.StringVar( value=self.app_settings.get_setting( "general", "default_program_parameters", default_general_settings.get("default_program_parameters", ""), ) ) self.profile_executor_instance: Optional[ProfileExecutor] = None self.available_profiles_map: Dict[str, Dict[str, Any]] = {} self.profile_exec_status_var = tk.StringVar(value="Select a profile to run.") self.produced_files_tree: Optional[ttk.Treeview] = None self.last_run_output_path: Optional[str] = None self.manual_dumps_output_path: Optional[str] = None self.profile_progressbar: Optional[ttk.Progressbar] = None self.status_bar_widget: Optional[ttk.Label] = None self.status_var = tk.StringVar(value="Ready.") self._prepare_manual_dumps_directory() 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) logger.info( f"{APP_TITLE_NAME} GUI initialized. Version: {APP_VERSION_INFO_STRING}" ) def _prepare_manual_dumps_directory(self): # --- MODIFICA: Usa self.log_directory_path come base per MANUAL_DUMPS_SUBFOLDER --- if not self.log_directory_path: # Dovrebbe essere sempre impostato da __init__ logger.error( "Log directory path not set in GDBGui, cannot prepare manual dumps directory." ) self.manual_dumps_output_path = None if ( hasattr(self, "open_manual_dumps_folder_button") and self.open_manual_dumps_folder_button ): self.open_manual_dumps_folder_button.config(state=tk.DISABLED) return self.manual_dumps_output_path = os.path.join( self.log_directory_path, MANUAL_DUMPS_SUBFOLDER ) # --- FINE MODIFICA --- try: os.makedirs(self.manual_dumps_output_path, exist_ok=True) logger.info( f"Manual dumps directory ensured/created: {self.manual_dumps_output_path}" ) if ( hasattr(self, "open_manual_dumps_folder_button") and self.open_manual_dumps_folder_button ): self.open_manual_dumps_folder_button.config(state=tk.NORMAL) except OSError as e: logger.error( f"Could not create manual dumps directory '{self.manual_dumps_output_path}': {e}" ) self.manual_dumps_output_path = None if ( hasattr(self, "open_manual_dumps_folder_button") and self.open_manual_dumps_folder_button ): self.open_manual_dumps_folder_button.config(state=tk.DISABLED) def _create_menus(self): # (Invariato) 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_config_window(self): # (Invariato) logger.debug("Opening configuration window.") config_win = ConfigWindow(self, self.app_settings) self.wait_window(config_win) logger.debug("Configuration window closed.") self._check_critical_configs_and_update_gui() self._prepare_manual_dumps_directory() def _open_profile_manager_window(self): # (Invariato) 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() def _check_critical_configs_and_update_gui(self): # (Invariato) logger.info("Checking critical configurations and updating GUI status...") 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 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: self.gdb_exe_status_var.set(f"GDB: '{gdb_exe_path}' (Not Found/Invalid!)") else: self.gdb_exe_status_var.set( "GDB: Not Configured! Set in Options > Configure." ) 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)" ) elif dumper_script_path: self.gdb_dumper_status_var.set( f"Dumper: '{dumper_script_path}' (Not Found/Invalid!)" ) else: self.gdb_dumper_status_var.set( "Dumper: Not Configured (Optional for JSON dump)." ) is_profile_currently_running = bool( self.profile_executor_instance and self.profile_executor_instance.is_running ) if hasattr(self, "start_gdb_button"): can_start_gdb = gdb_ok and not is_profile_currently_running self.start_gdb_button.config( state=tk.NORMAL if can_start_gdb else tk.DISABLED ) if not gdb_ok and not is_profile_currently_running: self._reset_gui_to_stopped_state() def _create_widgets(self): # (Invariato) main_frame = ttk.Frame(self, padding="10") main_frame.grid(row=0, column=0, sticky="nsew") self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) main_frame.rowconfigure(0, weight=0) main_frame.rowconfigure(1, weight=1) main_frame.rowconfigure(2, weight=8) main_frame.rowconfigure(3, weight=0) main_frame.columnconfigure(0, weight=1) self._create_config_status_widgets(main_frame) self._create_mode_notebook_widgets(main_frame) self._create_output_log_widgets(main_frame) self._create_status_bar(main_frame) def _create_config_status_widgets(self, parent_frame: ttk.Frame): # (Invariato) 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="ew", pady=5, padx=0) config_status_frame.columnconfigure(1, weight=1) config_status_frame.columnconfigure(3, weight=1) 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="ew", padx=(0, 10), pady=5 ) ttk.Label(config_status_frame, text="Dumper:").grid( row=0, column=2, sticky=tk.W, padx=(5, 0), pady=5 ) self.gdb_dumper_status_label = ttk.Label( config_status_frame, textvariable=self.gdb_dumper_status_var, relief="sunken", padding=(5, 2), anchor=tk.W, ) self.gdb_dumper_status_label.grid( row=0, column=3, sticky="ew", padx=(0, 10), pady=5 ) ttk.Button( config_status_frame, text="Configure...", command=self._open_config_window ).grid(row=0, column=4, padx=(5, 5), pady=5, sticky=tk.E) def _create_mode_notebook_widgets(self, parent_frame: ttk.Frame): # (Invariato) mode_notebook = ttk.Notebook(parent_frame) mode_notebook.grid(row=1, column=0, columnspan=1, sticky="nsew", pady=5, padx=0) 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) 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) # Add the Dump Analysis Tab dump_analysis_frame = DumpAnalysisTab(mode_notebook, app_settings=self.app_settings, log_directory_path=self.log_directory_path) mode_notebook.add(dump_analysis_frame, text="Dump Analysis") def _populate_manual_debug_tab(self, parent_tab_frame: ttk.Frame): # (Invariato) parent_tab_frame.columnconfigure(0, weight=1) 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="ew", pady=5) manual_target_settings_frame.columnconfigure(1, weight=1) row_idx = 0 ttk.Label(manual_target_settings_frame, text="Target Executable:").grid( row=row_idx, column=0, sticky=tk.W, padx=5, pady=2 ) ttk.Entry( manual_target_settings_frame, textvariable=self.exe_path_var, width=70 ).grid(row=row_idx, column=1, sticky="ew", padx=5, pady=2) ttk.Button( manual_target_settings_frame, text="Browse...", command=self._browse_target_exe, ).grid(row=row_idx, column=2, padx=5, pady=2) row_idx += 1 ttk.Label(manual_target_settings_frame, text="Program Parameters:").grid( row=row_idx, column=0, sticky=tk.W, padx=5, pady=2 ) ttk.Entry(manual_target_settings_frame, textvariable=self.params_var).grid( row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=2 ) row_idx += 1 ttk.Label(manual_target_settings_frame, text="Breakpoint Location:").grid( row=row_idx, column=0, sticky=tk.W, padx=5, pady=2 ) ttk.Entry(manual_target_settings_frame, textvariable=self.breakpoint_var).grid( row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=2 ) row_idx += 1 ttk.Label( manual_target_settings_frame, text="Examples: main, file.cpp:123, MyClass::foo", foreground="gray", font=("TkDefaultFont", 8), ).grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0, 5)) row_idx += 1 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="ew", padx=5, pady=2 ) manual_session_control_frame = ttk.LabelFrame( parent_tab_frame, text="Session Control", padding="10" ) manual_session_control_frame.grid(row=1, column=0, sticky="ew", pady=(10, 5)) 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) self.set_bp_button = ttk.Button( button_flow_frame, text="2. Set BP", command=self._set_gdb_breakpoint_action, state=tk.DISABLED, ) self.set_bp_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True) self.run_button = ttk.Button( button_flow_frame, text="3. Run", command=self._run_or_continue_gdb_action, state=tk.DISABLED, ) self.run_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True) self.dump_var_button = ttk.Button( button_flow_frame, text="4. Dump Var", command=self._dump_gdb_variable_action, state=tk.DISABLED, ) self.dump_var_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True) 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) manual_save_data_frame = ttk.LabelFrame( parent_tab_frame, text="Save/Manage Dumped Data", padding="10" ) manual_save_data_frame.grid(row=2, column=0, sticky="ew", pady=5) self.save_json_button = ttk.Button( manual_save_data_frame, text="Save as JSON", command=lambda: self._save_dumped_data("json"), state=tk.DISABLED, ) self.save_json_button.pack(side=tk.LEFT, padx=5, pady=5) 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) self.open_manual_dumps_folder_button = ttk.Button( manual_save_data_frame, text="Open Dumps Folder", command=self._open_manual_dumps_folder, state=( tk.NORMAL if self.manual_dumps_output_path and os.path.isdir(self.manual_dumps_output_path) else tk.DISABLED ), ) self.open_manual_dumps_folder_button.pack(side=tk.LEFT, padx=5, pady=5) def _populate_automated_execution_tab( self, parent_tab_frame: ttk.Frame ) -> None: # (Invariato) parent_tab_frame.columnconfigure(0, weight=1) parent_tab_frame.rowconfigure(0, weight=0) parent_tab_frame.rowconfigure(1, weight=0) parent_tab_frame.rowconfigure(2, weight=1) parent_tab_frame.rowconfigure(3, weight=0) 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) auto_control_frame.columnconfigure(1, weight=1) 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, 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") progress_status_frame = ttk.Frame(parent_tab_frame) progress_status_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0)) progress_status_frame.columnconfigure(0, weight=1) progress_status_frame.rowconfigure(0, weight=1) progress_status_frame.rowconfigure(1, weight=0) self.profile_exec_status_label_big = ttk.Label( progress_status_frame, textvariable=self.profile_exec_status_var, font=("TkDefaultFont", 10, "bold"), anchor=tk.NW, justify=tk.LEFT, ) self.profile_exec_status_label_big.grid( row=0, column=0, sticky="new", padx=5, pady=(0, 2) ) def _configure_wraplength_for_status_label(event): new_width = event.width - 15 if ( new_width > 20 and hasattr(self, "profile_exec_status_label_big") and self.profile_exec_status_label_big.winfo_exists() ): self.profile_exec_status_label_big.config(wraplength=new_width) progress_status_frame.bind( "", _configure_wraplength_for_status_label ) self.profile_progressbar = ttk.Progressbar( progress_status_frame, orient=tk.HORIZONTAL, mode="indeterminate" ) produced_files_frame = ttk.LabelFrame( parent_tab_frame, text="Produced Files Log", padding="10" ) produced_files_frame.grid(row=2, column=0, sticky="nsew", pady=(5, 0)) produced_files_frame.columnconfigure(0, weight=1) produced_files_frame.rowconfigure(0, weight=1) self.produced_files_tree = ttk.Treeview( produced_files_frame, columns=( "timestamp", "breakpoint_spec", "variable", "file", "status", "details", ), show="headings", selectmode="browse", ) self.produced_files_tree.grid(row=0, column=0, sticky="nsew") headings = { "timestamp": "Time", "breakpoint_spec": "Breakpoint Spec", "variable": "Variable", "file": "File Produced", "status": "Status", "details": "Details", } widths = { "timestamp": 130, "breakpoint_spec": 150, "variable": 150, "file": 180, "status": 100, "details": 600, } minwidths = { "timestamp": 120, "breakpoint_spec": 100, "variable": 100, "file": 150, "status": 60, "details": 150, } stretches = { "timestamp": False, "breakpoint_spec": True, "variable": True, "file": True, "status": False, "details": True, } for col, text in headings.items(): self.produced_files_tree.heading(col, text=text, anchor=tk.W) self.produced_files_tree.column( col, width=widths[col], minwidth=minwidths[col], stretch=stretches[col] ) tree_scrollbar_y = ttk.Scrollbar( produced_files_frame, orient=tk.VERTICAL, command=self.produced_files_tree.yview, ) tree_scrollbar_y.grid(row=0, column=1, sticky="ns") tree_scrollbar_x = ttk.Scrollbar( produced_files_frame, orient=tk.HORIZONTAL, command=self.produced_files_tree.xview, ) tree_scrollbar_x.grid(row=1, column=0, sticky="ew") self.produced_files_tree.configure( yscrollcommand=tree_scrollbar_y.set, xscrollcommand=tree_scrollbar_x.set ) folder_button_frame = ttk.Frame(parent_tab_frame) folder_button_frame.grid(row=3, column=0, sticky="e", pady=(5, 0)) self.open_output_folder_button = ttk.Button( folder_button_frame, text="Open Profile Output Folder", command=self._open_last_run_output_folder, state=tk.DISABLED, ) self.open_output_folder_button.pack(side=tk.RIGHT, padx=5, pady=0) def _load_and_populate_profiles_for_automation_tab(self): # (Invariato) self.available_profiles_map.clear() profiles_list = self.app_settings.get_profiles() profile_display_names = [] for profile_item in profiles_list: name = profile_item.get("profile_name") self.available_profiles_map[name] = profile_item if name else {} profile_display_names.append(name if name else "Unnamed Profile") # type: ignore sorted_names = sorted(profile_display_names) self.profile_selection_combo["values"] = sorted_names is_profile_currently_running = bool( self.profile_executor_instance and self.profile_executor_instance.is_running ) gdb_is_ok = self.app_settings.get_setting( "general", "gdb_executable_path" ) and os.path.isfile( self.app_settings.get_setting("general", "gdb_executable_path") ) if sorted_names: self.profile_selection_combo.set(sorted_names[0]) self.run_profile_button.config( state=( tk.NORMAL if not is_profile_currently_running and gdb_is_ok else tk.DISABLED ) ) 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 defined. Create/manage via 'Profiles > Manage Profiles...'." ) def _create_output_log_widgets(self, parent_frame: ttk.Frame): # (Invariato) output_log_notebook = ttk.Notebook(parent_frame) output_log_notebook.grid( row=2, column=0, columnspan=1, sticky="nsew", pady=(5, 0), padx=0 ) log_text_height = 12 self.gdb_raw_output_text = scrolledtext.ScrolledText( output_log_notebook, wrap=tk.WORD, height=log_text_height, state=tk.DISABLED, font=("Consolas", 9), ) output_log_notebook.add(self.gdb_raw_output_text, text="GDB Raw Output") self.parsed_json_output_text = scrolledtext.ScrolledText( output_log_notebook, wrap=tk.WORD, height=log_text_height, state=tk.DISABLED, font=("Consolas", 9), ) output_log_notebook.add( self.parsed_json_output_text, text="Parsed JSON/Status Output" ) self.app_log_text = scrolledtext.ScrolledText( output_log_notebook, wrap=tk.WORD, height=log_text_height, state=tk.DISABLED, font=("Consolas", 9), ) output_log_notebook.add(self.app_log_text, text="Application Log") def _create_status_bar(self, parent_frame: ttk.Frame): # (Invariato) self.status_bar_widget = ttk.Label( parent_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W ) self.status_bar_widget.grid( row=3, column=0, columnspan=1, sticky="ew", pady=(5, 0), ipady=2, padx=0 ) def _setup_logging_redirect_to_gui(self): # (Invariato) if not hasattr(self, "app_log_text") or not self.app_log_text: logger.error("app_log_text not found.") return self.gui_log_handler = ScrolledTextLogHandler(self.app_log_text) formatter = logging.Formatter( "%(asctime)s - %(name)-30s - [%(levelname)-7s] %(message)s", datefmt="%H:%M:%S", ) self.gui_log_handler.setFormatter(formatter) self.gui_log_handler.setLevel(logging.INFO) logging.getLogger().addHandler(self.gui_log_handler) logger.info("GUI logging handler initialized and added to root logger.") def _browse_file( self, title: str, target_var: tk.StringVar, filetypes: Optional[List[Tuple[str, Any]]] = None, ): # (Invariato) current_path = target_var.get() initial_dir = ( os.path.dirname(current_path) if current_path and os.path.exists(os.path.dirname(current_path)) else None ) path = filedialog.askopenfilename( title=title, filetypes=filetypes or [("All files", "*.*")], initialdir=initial_dir, parent=self, ) if path: target_var.set(path) if target_var == self.exe_path_var: self.app_settings.set_setting( "general", "last_target_executable_path", path ) logger.info(f"Target executable path updated: {path}") def _browse_target_exe(self): self._browse_file( "Select Target Executable", self.exe_path_var, [("Executable files", ("*.exe", "*")), ("All files", "*.*")], ) # (Invariato) def _update_gdb_raw_output(self, text: str, append: bool = True): # (Invariato) 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: self.gdb_raw_output_text.delete("1.0", tk.END) self.gdb_raw_output_text.insert("1.0", str(text)) 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): # (Invariato) 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", "") elif isinstance(data_to_display, dict): if "status" in data_to_display and ( "filepath_written" in data_to_display or "message" in data_to_display or "variable_dumped" in data_to_display ): status_text = f"Status: {data_to_display.get('status', 'N/A')}\nVar: {data_to_display.get('variable_dumped', 'N/A')}\nFile Written: {data_to_display.get('filepath_written', 'N/A')}\nFmt Req: {data_to_display.get('target_format_requested', 'N/A')}\nMsg: {data_to_display.get('message', 'N/A')}\n" if data_to_display.get("details"): status_text += f"Details: {data_to_display.get('details')}\n" if data_to_display.get("raw_gdb_output"): status_text += f"Raw GDB Output Snippet: {str(data_to_display.get('raw_gdb_output'))[:200]}...\n" self.parsed_json_output_text.insert("1.0", status_text) else: try: self.parsed_json_output_text.insert( "1.0", json.dumps(data_to_display, indent=2, ensure_ascii=False) ) except Exception as e: self.parsed_json_output_text.insert( "1.0", f"Err JSON dict: {e}\nRaw: {str(data_to_display)}" ) elif isinstance(data_to_display, list): try: self.parsed_json_output_text.insert( "1.0", json.dumps(data_to_display, indent=2, ensure_ascii=False) ) except Exception as e: self.parsed_json_output_text.insert( "1.0", f"Err JSON list: {e}\nRaw: {str(data_to_display)}" ) else: self.parsed_json_output_text.insert("1.0", str(data_to_display)) 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): # (Invariato) if hasattr(self, "status_var") and self.status_var is not None: self.status_var.set(message) def _handle_gdb_operation_error( self, op_name: str, err_details: Any ): # (Invariato) msg = f"Error GDB op '{op_name}': {err_details}" logger.error(msg, exc_info=isinstance(err_details, Exception)) self._update_gdb_raw_output(f"GDB_OPERATION_ERROR: {msg}\n", True) self._update_status_bar(f"Error: GDB op '{op_name}' failed.", True) if self.winfo_exists(): messagebox.showerror("GDB Operation Error", msg, parent=self) def _start_gdb_session_action(self): if self.profile_executor_instance and self.profile_executor_instance.is_running: messagebox.showwarning( "Profile Running", "Profile running. Stop 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") if not gdb_exe or not os.path.isfile(gdb_exe): messagebox.showerror("Config Error", "GDB path incorrect.", parent=self) self._check_critical_configs_and_update_gui() return if not target_exe or not os.path.exists(target_exe): messagebox.showerror( "Input Error", "Target exe required/not found.", parent=self ) return dumper_opts = self.app_settings.get_category_settings("dumper_options", {}) if self.gdb_session and self.gdb_session.is_alive(): messagebox.showwarning( "Session Active", "GDB active. Stop first.", parent=self ) return self._update_status_bar("Starting GDB...") self._update_gdb_raw_output("Starting GDB session...\n", False) self._update_parsed_json_output(None) default_timeouts = self.app_settings._get_default_settings().get("timeouts", {}) startup_timeout = self.app_settings.get_setting( "timeouts", "gdb_start", default_timeouts.get("gdb_start", 30) ) quit_timeout_no_sym = self.app_settings.get_setting( "timeouts", "gdb_quit", default_timeouts.get("gdb_quit", 10) ) try: self.gdb_session = GDBSession( gdb_path=gdb_exe, executable_path=target_exe, gdb_script_full_path=gdb_script, dumper_options=dumper_opts, app_log_directory_path=self.log_directory_path, ) self.gdb_session.start(timeout=startup_timeout) self._update_gdb_raw_output( f"GDB started for '{os.path.basename(target_exe)}'.\n" ) if not self.gdb_session.symbols_found: self._update_gdb_raw_output( "ERROR: No debug symbols. Session terminated.\n", True ) if self.winfo_exists(): messagebox.showwarning( "No Debug Symbols", f"No symbols in '{os.path.basename(target_exe)}'. Aborted.", parent=self, ) self._update_status_bar("GDB aborted: No symbols.", True) if self.gdb_session.is_alive(): self.gdb_session.quit(timeout=quit_timeout_no_sym) self.gdb_session = None self._reset_gui_to_stopped_state() self._check_critical_configs_and_update_gui() return # --- BLOCCO MODIFICATO --- if gdb_script and os.path.isfile(gdb_script): if self.gdb_session.gdb_script_sourced_successfully: self._update_gdb_raw_output( f"Dumper '{os.path.basename(gdb_script)}' sourced.\n", True ) self._update_status_bar( f"GDB active. Dumper '{os.path.basename(gdb_script)}' loaded." ) self.gdb_dumper_status_var.set( f"Dumper: {os.path.basename(gdb_script)} (Loaded)" ) else: # gdb_script_sourced_successfully è False self._update_gdb_raw_output( f"Warn: Dumper '{os.path.basename(gdb_script)}' FAILED load.\n", True, ) self._update_status_bar( f"GDB active. Dumper load issue.", is_error=True ) # Sposta il messagebox e l'aggiornamento dello status var QUI DENTRO L'ELSE if self.winfo_exists(): messagebox.showwarning( "Dumper Issue", f"Dumper '{os.path.basename(gdb_script)}' failed load.", parent=self, ) self.gdb_dumper_status_var.set( f"Dumper: {os.path.basename(gdb_script)} (Load Failed!)" ) # --- FINE BLOCCO MODIFICATO --- elif gdb_script: # gdb_script specificato ma non è un file valido self._update_gdb_raw_output( f"Warn: Dumper path '{gdb_script}' invalid.\n", True ) self._update_status_bar( f"GDB active. Dumper path invalid.", is_error=True ) self.gdb_dumper_status_var.set( f"Dumper: {os.path.basename(gdb_script) if gdb_script else 'N/A'} (Path Invalid!)" ) else: # Nessun gdb_script specificato self._update_gdb_raw_output( "No dumper script configured. JSON dump via 'dump_json' command unavailable.\n", True ) self._update_status_bar("GDB active. No dumper script.") self.gdb_dumper_status_var.set("Dumper: Not Configured.") self.start_gdb_button.config(state=tk.DISABLED) self.set_bp_button.config(state=tk.NORMAL) 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) if hasattr(self, "run_profile_button"): self.run_profile_button.config(state=tk.DISABLED) # type: ignore if hasattr(self, "profile_selection_combo"): self.profile_selection_combo.config(state=tk.DISABLED) # type: ignore self.program_started_once = False self.last_manual_gdb_dump_filepath = None self._disable_save_buttons() except (FileNotFoundError, ConnectionError, TimeoutError) as e: self._handle_gdb_operation_error("start GDB session", e) self.gdb_session = None self._reset_gui_to_stopped_state() self._check_critical_configs_and_update_gui() except Exception as e: logger.critical( f"MAIN CATCH start GDB: {type(e).__name__}: '{e}'", exc_info=True ) self._handle_gdb_operation_error("start (unexpected)", e) self.gdb_session = None self._reset_gui_to_stopped_state() self._check_critical_configs_and_update_gui() def _set_gdb_breakpoint_action(self): # (Come prima, usa i default corretti per i timeout) if not self.gdb_session or not self.gdb_session.is_alive(): messagebox.showerror("Error", "GDB not active.", parent=self) return bp_loc = self.breakpoint_var.get().strip() if not bp_loc: messagebox.showerror("Input Error", "BP location empty.", parent=self) return self._update_status_bar(f"Setting BP at '{bp_loc}'...") try: default_timeouts = self.app_settings._get_default_settings().get( "timeouts", {} ) cmd_timeout = self.app_settings.get_setting( "timeouts", "gdb_command", default_timeouts.get("gdb_command", 30) ) output = self.gdb_session.set_breakpoint(bp_loc, timeout=cmd_timeout) self._update_gdb_raw_output(output, True) bp_disp = bp_loc[:20] + "..." if len(bp_loc) > 20 else bp_loc if ( "Breakpoint" in output and "not defined" not in output.lower() and "pending" not in output.lower() ): self.set_bp_button.config(text=f"BP: {bp_disp} (Set)") self.run_button.config(state=tk.NORMAL) self._update_status_bar(f"BP set at '{bp_loc}'.") elif "pending" in output.lower(): self.set_bp_button.config(text=f"BP: {bp_disp} (Pend)") self.run_button.config(state=tk.NORMAL) self._update_status_bar(f"BP '{bp_loc}' pending.") messagebox.showinfo( "BP Pending", f"BP '{bp_loc}' pending.", parent=self ) else: self._update_status_bar(f"Issue setting BP '{bp_loc}'.", True) if self.winfo_exists(): messagebox.showwarning( "BP Error", f"Failed to set BP at '{bp_loc}'. Check GDB Output.", parent=self, ) except (ConnectionError, TimeoutError) as e: self._handle_gdb_operation_error(f"set BP '{bp_loc}'", e) except Exception as e: self._handle_gdb_operation_error(f"set BP '{bp_loc}' (unexpected)", e) def _run_or_continue_gdb_action(self): # (Come prima, usa i default corretti per i timeout) if not self.gdb_session or not self.gdb_session.is_alive(): messagebox.showerror("Error", "GDB not active.", parent=self) return self._update_parsed_json_output(None) self._disable_save_buttons() try: output = "" default_timeouts = self.app_settings._get_default_settings().get( "timeouts", {} ) run_timeout = self.app_settings.get_setting( "timeouts", "program_run_continue", default_timeouts.get("program_run_continue", 120), ) dumper_ok = self.gdb_session.gdb_script_sourced_successfully if not self.program_started_once: params = self.params_var.get() self._update_status_bar(f"Running with params: '{params}'...") self._update_gdb_raw_output(f"run {params}\n", True) output = self.gdb_session.run_program(params, timeout=run_timeout) else: self._update_status_bar("Continuing...") self._update_gdb_raw_output("continue\n", True) output = self.gdb_session.continue_execution(timeout=run_timeout) self._update_gdb_raw_output(output, True) dump_btn_state = tk.NORMAL if dumper_ok else tk.DISABLED if "Breakpoint" in output or re.search( r"Hit Breakpoint \d+", output, re.IGNORECASE ): self._update_status_bar("BP hit. Ready to dump.") self.dump_var_button.config(state=dump_btn_state) self.program_started_once = True self.run_button.config(text="3. Continue") elif "Program exited normally" in output or "exited with code" in output: self._update_status_bar("Program exited.") self.dump_var_button.config(state=tk.DISABLED) self.run_button.config(text="3. Run (Restart)") self.program_started_once = False elif ( "received signal" in output.lower() or "segmentation fault" in output.lower() ): self._update_status_bar("Program signal/crash.", True) self.dump_var_button.config(state=dump_btn_state) self.program_started_once = True self.run_button.config(text="3. Continue (Risky)") else: self._update_status_bar("Program running/unknown.") self.dump_var_button.config(state=dump_btn_state) self.program_started_once = True self.run_button.config(text="3. Continue") except (ConnectionError, TimeoutError) as e: self._handle_gdb_operation_error("run/continue", e) except Exception as e: self._handle_gdb_operation_error("run/continue (unexpected)", e) def _dump_gdb_variable_action(self): # (Come prima, usa i default corretti per i timeout e sanitize_filename_component importato) if not self.gdb_session or not self.gdb_session.is_alive(): messagebox.showerror("Error", "GDB session not active.", parent=self) return if not self.gdb_session.gdb_script_sourced_successfully: messagebox.showwarning( "Dumper Script Error", "GDB dumper script not loaded. JSON dump unavailable.", parent=self, ) self._check_critical_configs_and_update_gui() return var_expr = self.variable_var.get().strip() if not var_expr: messagebox.showerror( "Input Error", "Variable/Expression to dump empty.", parent=self ) return if not self.manual_dumps_output_path or not os.path.isdir( self.manual_dumps_output_path ): messagebox.showerror( "Directory Error", "Manual dumps output directory not set.", parent=self ) self._prepare_manual_dumps_directory() if not self.manual_dumps_output_path or not os.path.isdir( self.manual_dumps_output_path ): return self._update_status_bar(f"Dumping '{var_expr}' to file...") self._update_gdb_raw_output(f"Attempting file dump of: {var_expr}\n", True) self.last_manual_gdb_dump_filepath = None self._disable_save_buttons() timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] app_name_manual = sanitize_filename_component( os.path.basename( self.exe_path_var.get() if self.exe_path_var.get() else "manual_app" ) ) breakpoint_manual = ( sanitize_filename_component(self.breakpoint_var.get()) if self.breakpoint_var.get() else "manual_bp_not_set" ) variable_manual = sanitize_filename_component(var_expr) gdb_dump_ext_name_part = GDB_DUMP_EXTENSION.lstrip(".") manual_placeholders = { "{profile_name}": MANUAL_DUMP_PROFILE_NAME_PLACEHOLDER, "{app_name}": app_name_manual, "{breakpoint}": breakpoint_manual, "{variable}": variable_manual, "{timestamp}": timestamp_str, "{extension}": gdb_dump_ext_name_part, } gdb_target_dump_filename = MANUAL_DUMP_FILENAME_PATTERN for ph, val in manual_placeholders.items(): gdb_target_dump_filename = gdb_target_dump_filename.replace(ph, val) if not gdb_target_dump_filename.endswith(GDB_DUMP_EXTENSION): base_n, _ = os.path.splitext(gdb_target_dump_filename) gdb_target_dump_filename = base_n + GDB_DUMP_EXTENSION gdb_target_dump_filepath = os.path.join( self.manual_dumps_output_path, gdb_target_dump_filename ) try: default_timeouts = self.app_settings._get_default_settings().get( "timeouts", {} ) dump_timeout = self.app_settings.get_setting( "timeouts", "dump_variable", default_timeouts.get("dump_variable", 60) ) status_payload = self.gdb_session.dump_variable_to_json( var_expr, timeout=dump_timeout, target_output_filepath=gdb_target_dump_filepath, target_output_format="json", ) self._update_parsed_json_output(status_payload) if status_payload.get("status") == "success": filepath_written = status_payload.get("filepath_written") if filepath_written and os.path.exists(filepath_written): self.last_manual_gdb_dump_filepath = filepath_written self._update_status_bar( f"Dumped '{var_expr}' to temp: {os.path.basename(filepath_written)}." ) self._enable_save_buttons_if_data() else: self._update_status_bar( f"Dumper success, but temp file for '{var_expr}' missing.", True ) else: error_msg = status_payload.get( "details", status_payload.get("message", "Unknown dumper error") ) self._update_status_bar( f"Error dumping '{var_expr}': {error_msg}", True ) except (ConnectionError, TimeoutError) as e: self._handle_gdb_operation_error(f"dump var '{var_expr}'", e) except Exception as e: self._handle_gdb_operation_error(f"dump var '{var_expr}' (unexpected)", e) def _stop_gdb_session_action(self): # (Come prima, usa i default corretti per i timeout) if self.gdb_session and self.gdb_session.is_alive(): self._update_status_bar("Stopping GDB session...") try: default_timeouts = self.app_settings._get_default_settings().get( "timeouts", {} ) kill_timeout = self.app_settings.get_setting( "timeouts", "kill_program", default_timeouts.get("kill_program", 20) ) quit_timeout = self.app_settings.get_setting( "timeouts", "gdb_quit", default_timeouts.get("gdb_quit", 10) ) if self.program_started_once: kill_output = self.gdb_session.kill_program(timeout=kill_timeout) self._update_gdb_raw_output(f"Kill output:\n{kill_output}\n", True) self.gdb_session.quit(timeout=quit_timeout) self._update_gdb_raw_output("GDB quit sent.\n", True) except Exception as e: self._handle_gdb_operation_error("stop GDB session", e) finally: self.gdb_session = None self._reset_gui_to_stopped_state() self._load_and_populate_profiles_for_automation_tab() else: self._reset_gui_to_stopped_state() self._load_and_populate_profiles_for_automation_tab() def _reset_gui_to_stopped_state(self): # (Invariato) gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path") gdb_is_ok = bool(gdb_exe_path and os.path.isfile(gdb_exe_path)) is_prof_running = ( self.profile_executor_instance and self.profile_executor_instance.is_running ) if hasattr(self, "start_gdb_button"): self.start_gdb_button.config( state=tk.NORMAL if gdb_is_ok and not is_prof_running else tk.DISABLED ) if hasattr(self, "set_bp_button"): self.set_bp_button.config(state=tk.DISABLED, text="2. Set BP") if hasattr(self, "run_button"): self.run_button.config(state=tk.DISABLED, text="3. Run Program") if hasattr(self, "dump_var_button"): self.dump_var_button.config(state=tk.DISABLED) if hasattr(self, "stop_gdb_button"): self.stop_gdb_button.config(state=tk.DISABLED) if hasattr(self, "save_json_button"): self._disable_save_buttons() if hasattr(self, "run_profile_button") and hasattr( self, "profile_selection_combo" ): can_run_profile = ( gdb_is_ok and not is_prof_running and bool(self.profile_selection_combo.get()) ) self.run_profile_button.config( state=tk.NORMAL if can_run_profile else tk.DISABLED ) self.profile_selection_combo.config(state="readonly" if not is_prof_running else tk.DISABLED) # type: ignore self.program_started_once = False self.last_manual_gdb_dump_filepath = None if ( not is_prof_running and hasattr(self, "status_var") and self.status_var is not None ): self._update_status_bar("GDB session stopped or not active.") def _enable_save_buttons_if_data(self): # (Invariato) can_save = bool( self.last_manual_gdb_dump_filepath and os.path.exists(self.last_manual_gdb_dump_filepath) ) if hasattr(self, "save_json_button"): self.save_json_button.config(state=tk.NORMAL if can_save else tk.DISABLED) if hasattr(self, "save_csv_button"): self.save_csv_button.config(state=tk.NORMAL if can_save else tk.DISABLED) def _disable_save_buttons(self): # (Invariato) if hasattr(self, "save_json_button"): self.save_json_button.config(state=tk.DISABLED) if hasattr(self, "save_csv_button"): self.save_csv_button.config(state=tk.DISABLED) def _save_dumped_data(self, final_format_type: str): # (Invariato) if not self.last_manual_gdb_dump_filepath or not os.path.exists( self.last_manual_gdb_dump_filepath ): messagebox.showwarning( "No Data", "No temp dump file to save from.", parent=self ) return file_ext = f".{final_format_type.lower()}" file_types = [ (f"{final_format_type.upper()} files", f"*{file_ext}"), ("All files", "*.*"), ] var_sugg = ( sanitize_filename_component(self.variable_var.get()) if self.variable_var.get() else "manual_dump_var" ) timestamp_from_temp_file = "data" try: temp_basename = os.path.basename(self.last_manual_gdb_dump_filepath) match_ts = re.search(r"(\d{8}_\d{6}_\d{3})", temp_basename) if match_ts: timestamp_from_temp_file = match_ts.group(1) except Exception: pass default_fname_base = f"{var_sugg}_{timestamp_from_temp_file}" final_save_filepath = filedialog.asksaveasfilename( defaultextension=file_ext, filetypes=file_types, title=f"Save Manual Dump as {final_format_type.upper()}", initialfile=f"{default_fname_base}{file_ext}", parent=self, ) if not final_save_filepath: return self._update_status_bar( f"Saving data as {final_format_type.upper()} to {os.path.basename(final_save_filepath)}..." ) try: with open( self.last_manual_gdb_dump_filepath, "r", encoding="utf-8" ) as f_temp: data_from_gdb_dump = json.load(f_temp) if final_format_type == "json": save_to_json(data_from_gdb_dump, final_save_filepath) elif final_format_type == "csv": data_csv = data_from_gdb_dump if isinstance(data_csv, dict) and not isinstance(data_csv, list): data_csv = [data_csv] elif not isinstance(data_csv, list): data_csv = [{"value": data_csv}] elif ( isinstance(data_csv, list) and data_csv and not all(isinstance(i, dict) for i in data_csv) ): data_csv = [{"value": i} for i in data_csv] save_to_csv(data_csv, final_save_filepath) messagebox.showinfo( "Save Successful", f"Data saved to:\n{final_save_filepath}", parent=self ) self._update_status_bar( f"Data saved to {os.path.basename(final_save_filepath)}." ) try: os.remove(self.last_manual_gdb_dump_filepath) logger.info( f"Deleted temp manual dump: {self.last_manual_gdb_dump_filepath}" ) self.last_manual_gdb_dump_filepath = None self._disable_save_buttons() except Exception as e_del: logger.error( f"Could not delete temp manual dump '{self.last_manual_gdb_dump_filepath}': {e_del}" ) except Exception as e: logger.error(f"Error saving manual dump: {e}", exc_info=True) messagebox.showerror("Save Error", f"Failed to save: {e}", parent=self) self._update_status_bar(f"Error saving.", True) def _gui_status_update(self, message: str) -> None: # (Invariato) if ( hasattr(self, "profile_exec_status_var") and self.profile_exec_status_var is not None ): self.profile_exec_status_var.set(message) logger.info(f"ProfileExecutor Status Update (via callback): {message}") def _gui_gdb_output_update(self, message: str) -> None: self._update_gdb_raw_output(message, append=True) # (Invariato) def _gui_json_data_update(self, data: Any) -> None: self._update_parsed_json_output(data) # (Invariato) def _gui_add_execution_log_entry( self, entry: ExecutionLogEntry ) -> None: # (Invariato) if self.produced_files_tree and self.winfo_exists(): try: values = ( entry.get("timestamp", ""), entry.get("breakpoint_spec", "N/A"), entry.get("variable", "N/A"), entry.get("file_produced", "N/A"), entry.get("status", "N/A"), entry.get("details", ""), ) item_id = self.produced_files_tree.insert("", tk.END, values=values) self.produced_files_tree.see(item_id) except Exception as e: logger.error(f"Failed add to tree: {e}. Entry: {entry}") def _clear_produced_files_tree(self) -> None: # (Invariato) if self.produced_files_tree: for item in self.produced_files_tree.get_children(): self.produced_files_tree.delete(item) def _run_selected_profile_action(self) -> None: # (Come prima, ma con il passaggio di log_directory_path a ProfileExecutor) 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 Already Running", "A profile is already in execution.", parent=self, ) return if self.gdb_session and self.gdb_session.is_alive(): messagebox.showerror( "GDB Session Active", "A manual GDB session is active. Stop it first.", parent=self, ) return profile_data_to_run = self.available_profiles_map.get(selected_profile_name) if not profile_data_to_run: messagebox.showerror( "Profile Error", f"Could not retrieve data for '{selected_profile_name}'.", parent=self, ) return self.profile_exec_status_var.set( f"STARTING PROFILE '{selected_profile_name}'..." ) if self.profile_progressbar and hasattr( self.profile_exec_status_label_big, "master" ): parent_frame_for_pb = self.profile_exec_status_label_big.master # type: ignore self.profile_progressbar.grid( row=1, column=0, columnspan=1, sticky="ew", padx=5, pady=(2, 5), in_=parent_frame_for_pb, ) self.profile_progressbar.start(15) 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: if self.menubar.winfo_exists(): self.menubar.entryconfig("Profiles", state=tk.DISABLED) self.menubar.entryconfig("Options", state=tk.DISABLED) except tk.TclError: logger.warning("TclError disabling menubar.") 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.last_run_output_path = None self.open_output_folder_button.config(state=tk.DISABLED) # type: ignore # --- MODIFICA: Passa self.log_directory_path --- self.profile_executor_instance = ProfileExecutor( profile_data_to_run, self.app_settings, app_log_directory_path=self.log_directory_path, status_update_callback=self._gui_status_update, gdb_output_callback=self._gui_gdb_output_update, json_output_callback=self._gui_json_data_update, execution_log_callback=self._gui_add_execution_log_entry, ) # --- FINE MODIFICA --- self._clear_produced_files_tree() self._update_gdb_raw_output("", 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): # (Invariato) if self.profile_executor_instance: try: self.profile_executor_instance.run() finally: if self.winfo_exists(): self.after(0, self._on_profile_execution_finished) def _on_profile_execution_finished(self): # (Invariato) if not self.winfo_exists(): logger.warning("Profile exec finished callback, but window gone.") return if self.profile_progressbar: self.profile_progressbar.stop() self.profile_progressbar.grid_remove() final_status_message = ( "Profile execution finished (status unknown if executor missing)." ) if self.profile_executor_instance: if hasattr(self.profile_executor_instance, "current_run_output_path"): self.last_run_output_path = ( self.profile_executor_instance.current_run_output_path ) if ( self.last_run_output_path and os.path.isdir(self.last_run_output_path) and hasattr(self, "open_output_folder_button") ): self.open_output_folder_button.config(state=tk.NORMAL) # type: ignore else: if hasattr(self, "open_output_folder_button"): self.open_output_folder_button.config(state=tk.DISABLED) # type: ignore logger.warning( f"Profile output folder invalid: {self.last_run_output_path}" ) current_gui_status = self.profile_exec_status_var.get() if ( "STARTING PROFILE" in current_gui_status or "Requesting profile stop" in current_gui_status or "Status:" in current_gui_status ): if hasattr(self.profile_executor_instance, "profile_execution_summary"): exec_final_status = ( self.profile_executor_instance.profile_execution_summary.get( "status", "Unknown status from executor" ) ) if ( "Error" in exec_final_status or "failed" in exec_final_status.lower() or "Interrupted" in exec_final_status ): final_status_message = ( f"Profile finished with issues: {exec_final_status}" ) elif exec_final_status not in [ "Initialized", "Pending", "Processing Dumps", ]: final_status_message = ( f"Profile run completed. State: {exec_final_status}" ) elif ( "Error:" in current_gui_status or "failed" in current_gui_status.lower() ): final_status_message = ( f"Profile finished with issues: {current_gui_status}" ) else: final_status_message = ( f"Profile run completed. Last status: {current_gui_status}" ) self.profile_exec_status_var.set(final_status_message) gdb_is_ok = self.app_settings.get_setting( "general", "gdb_executable_path" ) and os.path.isfile( self.app_settings.get_setting("general", "gdb_executable_path") ) if hasattr(self, "profile_selection_combo") and self.profile_selection_combo.get(): # type: ignore if hasattr(self, "run_profile_button") and gdb_is_ok: self.run_profile_button.config(state=tk.NORMAL) # type: ignore else: if hasattr(self, "run_profile_button"): self.run_profile_button.config(state=tk.DISABLED) # type: ignore else: if hasattr(self, "run_profile_button"): self.run_profile_button.config(state=tk.DISABLED) # type: ignore if hasattr(self, "stop_profile_button"): self.stop_profile_button.config(state=tk.DISABLED) # type: ignore if hasattr(self, "profile_selection_combo"): self.profile_selection_combo.config(state="readonly") # type: ignore try: if self.menubar.winfo_exists(): self.menubar.entryconfig("Profiles", state=tk.NORMAL) self.menubar.entryconfig("Options", state=tk.NORMAL) except tk.TclError as e_menu_enable: logger.warning(f"TclError re-enabling menubar: {e_menu_enable}") self._check_critical_configs_and_update_gui() self.profile_executor_instance = None logger.info("Profile execution GUI updates completed.") def _stop_current_profile_action(self) -> None: # (Invariato) 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() if hasattr(self, "stop_profile_button"): self.stop_profile_button.config(state=tk.DISABLED) else: self.profile_exec_status_var.set("No profile running to stop.") def _open_last_run_output_folder(self) -> None: self._open_folder_path( self.last_run_output_path, "Profile Output Folder" ) # (Invariato) def _open_manual_dumps_folder(self) -> None: self._open_folder_path( self.manual_dumps_output_path, "Manual Dumps Folder" ) # (Invariato) def _open_folder_path( self, folder_path: Optional[str], folder_desc: str ) -> None: # (Invariato) logger.info(f"Attempting to open {folder_desc}: '{folder_path}'") if not folder_path or not os.path.isdir(folder_path): messagebox.showwarning( f"No {folder_desc}", f"Path for '{folder_desc}' not set/exist: {folder_path}", parent=self, ) else: try: if sys.platform == "win32": os.startfile(os.path.normpath(folder_path)) elif sys.platform == "darwin": subprocess.run(["open", folder_path], check=True) else: subprocess.run(["xdg-open", folder_path], check=True) except Exception as e: logger.error( f"Failed to open {folder_desc} at '{folder_path}': {e}", exc_info=True, ) messagebox.showerror( "Error Opening Folder", f"Could not open {folder_desc.lower()}: {folder_path}\nError: {e}", parent=self, ) def _on_closing_window(self): # (Invariato) logger.info("Main window closing sequence initiated.") active_profile_stop_requested = False if self.profile_executor_instance and self.profile_executor_instance.is_running: response = messagebox.askyesnocancel( "Profile Running", "Profile running. Stop & exit?", default=messagebox.CANCEL, parent=self, ) if response is True: self._stop_current_profile_action() active_profile_stop_requested = True elif response is None: logger.info("User cancelled exit.") return self.app_settings.set_setting("gui", "main_window_geometry", self.geometry()) 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() ) self.app_settings.set_setting( "general", "default_program_parameters", self.params_var.get() ) if not self.app_settings.save_settings() and self.winfo_exists(): messagebox.showwarning( "Settings Error", "Could not save settings.", parent=self ) should_destroy = True if self.gdb_session and self.gdb_session.is_alive(): if self.winfo_exists() and messagebox.askokcancel( "GDB Session Active", "Manual GDB active. Stop & exit?", parent=self ): self._stop_gdb_session_action() elif self.winfo_exists(): should_destroy = False logger.info("User cancelled exit (manual GDB active).") elif not self.winfo_exists(): self._stop_gdb_session_action() if should_destroy: logger.info("Proceeding with window destruction.") if self.gui_log_handler: logging.getLogger().removeHandler(self.gui_log_handler) self.gui_log_handler.close() self.gui_log_handler = None if active_profile_stop_requested: logger.debug("Main window closing. Profile executor asked to stop.") self.destroy() logger.info("Main Tkinter window destroyed. App will exit.") else: logger.info("Window destruction aborted by user.") class ScrolledTextLogHandler(logging.Handler): def __init__(self, text_widget: scrolledtext.ScrolledText): super().__init__() self.text_widget = text_widget self._active = True def emit(self, record: logging.LogRecord): if ( not self._active or not hasattr(self.text_widget, "winfo_exists") or not self.text_widget.winfo_exists() ): return try: log_entry = self.format(record) self.text_widget.config(state=tk.NORMAL) 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: self._active = False logger.debug("ScrolledTextLogHandler TclError, disabling.") except Exception as e: self._active = False print(f"FATAL ScrolledTextLogHandler ERROR: {e}", file=sys.stderr) if __name__ == "__main__": if not logging.getLogger().hasHandlers(): logging.basicConfig( level=logging.DEBUG, format="%(asctime)s - %(name)-25s - %(levelname)-8s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) logger.info("Running GDBGui directly for testing (main_window.py).") # Per testare GDBGui standalone, dobbiamo simulare i percorsi passati da __main__ test_app_base_path = os.path.dirname( os.path.dirname(os.path.abspath(__file__)) ) # Simula project_root test_log_dir = os.path.join(test_app_base_path, "logs_test_gui") try: os.makedirs(test_log_dir, exist_ok=True) except: pass app = GDBGui(app_base_path=test_app_base_path, log_directory_path=test_log_dir) app.mainloop()