# 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 # For pretty-printing JSON in the GUI import re import threading # For running profile executor in a separate thread import subprocess # For opening folder cross-platform import sys # To check platform from typing import ( Optional, Dict, Any, Callable, List, Tuple, ) # Ensuring typing imports are present # 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 ..core.profile_executor import ( ProfileExecutor, ExecutionLogEntry, ) # ExecutionLogEntry needs to be defined or imported if used as type hint from .config_window import ConfigWindow from .profile_manager_window import ProfileManagerWindow logger = logging.getLogger(__name__) class GDBGui(tk.Tk): def __init__(self): super().__init__() self.app_settings = AppSettings() self.gui_log_handler: Optional[ScrolledTextLogHandler] = None # Added type hint 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", "850x780") ) self.gdb_session: Optional[GDBSession] = None self.last_dumped_data: Any = 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...") self.exe_path_var = tk.StringVar( value=self.app_settings.get_setting( "general", "last_target_executable_path", "" ) ) self.breakpoint_var = tk.StringVar( value=self.app_settings.get_setting("general", "default_breakpoint", "main") ) self.variable_var = tk.StringVar( value=self.app_settings.get_setting( "general", "default_variable_to_dump", "" ) ) self.params_var = tk.StringVar( value=self.app_settings.get_setting( "general", "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.profile_progressbar: Optional[ttk.Progressbar] = None self.status_bar_widget: Optional[ttk.Label] = ( None # For status bar widget reference ) self.status_var: Optional[tk.StringVar] = None # For status bar text variable self._create_menus() self._create_widgets() # This will now correctly call the existing _create_status_bar 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) def _create_menus(self): # ... (implementation as before) 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): # ... (implementation as before) 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() def _open_profile_manager_window(self): # ... (implementation as before) 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): # ... (implementation as before) ... 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 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! Please 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).") if hasattr(self, "start_gdb_button"): # Check if widget exists if gdb_ok 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) if not gdb_ok: self._reset_gui_to_stopped_state() self.title( f"GDB Debug GUI - Settings: {os.path.basename(self.app_settings.config_filepath)}" ) def _create_widgets(self): # ... (implementation as before, with adjusted row weights) ... 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) 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) # This call should now work def _create_config_status_widgets(self, parent_frame: ttk.Frame): # ... (implementation as before) ... 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) 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=(tk.W, tk.E), 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=(tk.W, tk.E), 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): # ... (implementation as before) ... mode_notebook = ttk.Notebook(parent_frame) mode_notebook.grid(row=1, column=0, columnspan=1, sticky="nsew", pady=5, padx=0) 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) self.automated_exec_frame = ttk.Frame(mode_notebook, padding="10") # type: ignore 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): # ... (implementation as before) ... 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=(tk.W, tk.E), 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=(tk.W, tk.E), 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=(tk.W, tk.E), 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=(tk.W, tk.E), padx=5, pady=2 ) row_idx += 1 bp_help_text = "Examples: main, myfile.cpp:123, MyClass::myMethod" ttk.Label( manual_target_settings_frame, text=bp_help_text, 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=(tk.W, tk.E), 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=(tk.W, tk.E), 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 Dumped Data", padding="10" ) manual_save_data_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), 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) def _populate_automated_execution_tab(self, parent_tab_frame: ttk.Frame) -> None: # ... (implementation as in your last correct version with dynamic wraplength and progressbar in grid) ... 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: if ( 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" ) self.profile_progressbar.grid(row=1, column=0, sticky="ew", padx=5, pady=(2, 5)) 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") self.produced_files_tree.heading("timestamp", text="Time", anchor=tk.W) self.produced_files_tree.heading( "breakpoint_spec", text="Breakpoint Spec", anchor=tk.W ) self.produced_files_tree.heading("variable", text="Variable", anchor=tk.W) self.produced_files_tree.heading("file", text="File Produced", anchor=tk.W) self.produced_files_tree.heading("status", text="Status", anchor=tk.W) self.produced_files_tree.heading("details", text="Details", anchor=tk.W) self.produced_files_tree.column( "timestamp", width=130, minwidth=120, stretch=False ) self.produced_files_tree.column( "breakpoint_spec", width=150, minwidth=100, stretch=True ) self.produced_files_tree.column( "variable", width=150, minwidth=100, stretch=True ) self.produced_files_tree.column("file", width=180, minwidth=150, stretch=True) self.produced_files_tree.column("status", width=80, minwidth=60, stretch=False) self.produced_files_tree.column( "details", width=180, minwidth=150, stretch=True ) 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 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): # ... (implementation as before) ... 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") 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 if sorted_names: self.profile_selection_combo.set(sorted_names[0]) if not ( self.profile_executor_instance and self.profile_executor_instance.is_running ): 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): # ... (implementation as before, possibly adjust 'log_text_height') ... 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 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") # --- DEFINIZIONE DI _create_status_bar --- def _create_status_bar(self, parent_frame: ttk.Frame): """Creates the status bar.""" self.status_var = tk.StringVar( value="Ready. Configure GDB via Options menu if needed." ) 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=(tk.W, tk.E), pady=(5, 0), ipady=2, padx=0, ) def _setup_logging_redirect_to_gui(self): # ... (implementation as before) ... if not hasattr(self, "app_log_text") or not self.app_log_text: logger.error("app_log_text widget not available for GUI logging setup.") return self.gui_log_handler = ScrolledTextLogHandler(self.app_log_text) formatter = logging.Formatter( "%(asctime)s [%(levelname)-7s] %(name)s: %(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) def _browse_file( self, title: str, target_var: tk.StringVar, filetypes: Optional[List[Tuple[str, Any]]] = None, ): # Added types # ... (implementation as before) ... 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) def _browse_target_exe(self): # ... (implementation as before) ... self._browse_file( "Select Target Application Executable", self.exe_path_var, [("Executable files", ("*.exe", "*")), ("All files", "*.*")], ) # Adjusted for cross-platform def _update_gdb_raw_output(self, text: str, append: bool = True): # ... (implementation as before) ... 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): # ... (implementation as before) ... 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, list)): try: pretty_json = json.dumps(data_to_display, indent=2, ensure_ascii=False) self.parsed_json_output_text.insert("1.0", pretty_json) except Exception as e: logger.error(f"Error pretty-printing JSON for GUI: {e}") self.parsed_json_output_text.insert( "1.0", f"Error displaying JSON: {e}\nRaw data: {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): # ... (implementation as before) ... if hasattr(self, "status_var") and self.status_var is not None: self.status_var.set(message) # Check if status_var is None def _handle_gdb_operation_error(self, operation_name: str, error_details: Any): # ... (implementation as before) ... 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) if self.winfo_exists(): messagebox.showerror("GDB Operation Error", error_message, parent=self) def _start_gdb_session_action(self): # ... (implementation as before, including disabling profile controls) ... 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") if not gdb_exe or not os.path.isfile(gdb_exe): messagebox.showerror( "Configuration Error", "GDB executable path is not configured correctly. Please check Options > Configure.", parent=self, ) self._check_critical_configs_and_update_gui() return if not target_exe: messagebox.showerror( "Input Error", "Target executable path is required.", parent=self ) return if not os.path.exists(target_exe): messagebox.showerror( "File Not Found", f"Target executable not found: {target_exe}", parent=self, ) return if gdb_script and not os.path.isfile(gdb_script): messagebox.showwarning( "Configuration Warning", f"GDB dumper script path is set to:\n'{gdb_script}'\nbut the file was not found or is invalid.\n\nJSON dumping via script will be unavailable. You can correct this in Options > Configure.", parent=self, ) 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 self._update_status_bar("Starting GDB session...") 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) current_dumper_options = self.app_settings.get_category_settings( "dumper_options", {} ) # Ensure this is fetched self.gdb_session = GDBSession( gdb_path=gdb_exe, executable_path=target_exe, gdb_script_full_path=gdb_script, dumper_options=current_dumper_options, ) self.gdb_session.start( timeout=startup_timeout ) # This call internally sets gdb_script_sourced_successfully self._update_gdb_raw_output( f"GDB session started for '{os.path.basename(target_exe)}'.\n" ) # --- NUOVO LOGGING DIAGNOSTICO --- logger.info(f"MAIN_WINDOW_CHECK: gdb_script path = '{gdb_script}'") if self.gdb_session: # Assicurati che gdb_session esista logger.info( f"MAIN_WINDOW_CHECK: self.gdb_session.gdb_script_path (internal) = '{self.gdb_session.gdb_script_path}'" ) logger.info( f"MAIN_WINDOW_CHECK: self.gdb_session.gdb_script_sourced_successfully = {self.gdb_session.gdb_script_sourced_successfully}" ) else: logger.error( "MAIN_WINDOW_CHECK: self.gdb_session is None at the point of checking dumper status!" ) # --- FINE NUOVO LOGGING DIAGNOSTICO --- if ( gdb_script and self.gdb_session and self.gdb_session.gdb_script_sourced_successfully ): # Aggiunto check self.gdb_session self._update_gdb_raw_output( f"GDB dumper script '{os.path.basename(gdb_script)}' sourced successfully.\n", append=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)" ) elif ( gdb_script and self.gdb_session ): # Aggiunto check self.gdb_session, implica che gdb_script_sourced_successfully è False 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!)" ) # Mostra il warning solo se la finestra esiste ancora if self.winfo_exists(): messagebox.showwarning( "Dumper Script Issue", f"The GDB dumper script '{os.path.basename(gdb_script)}' may have failed to load.\nJSON dumping might be affected. Check logs.", parent=self, ) elif not gdb_script and self.gdb_session: # Aggiunto check self.gdb_session 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.") # Fallback per lo stato del dumper se non è configurato 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) self.set_bp_button.config(state=tk.NORMAL) self.run_button.config( state=tk.DISABLED, text="3. Run Program" ) # Inizialmente disabilitato fino a BP self.dump_var_button.config( state=tk.DISABLED ) # Disabilitato finché non si è a un breakpoint self.stop_gdb_button.config(state=tk.NORMAL) # Disabilita controlli profilo if hasattr(self, "run_profile_button"): self.run_profile_button.config(state=tk.DISABLED) if hasattr(self, "profile_selection_combo"): self.profile_selection_combo.config(state=tk.DISABLED) self.program_started_once = False # Resetta per la nuova sessione self.last_dumped_data = None self._disable_save_buttons() except (FileNotFoundError, ConnectionError, TimeoutError) as e_specific: self._handle_gdb_operation_error("start session", e_specific) self.gdb_session = None self._reset_gui_to_stopped_state() except Exception as e: logger.critical( f"!!! MAIN_WINDOW CATCH-ALL: Unhandled exception type: {type(e).__name__}, message: '{e}'", exc_info=True, ) self._handle_gdb_operation_error( "start session (unexpected from main_window catch-all)", e ) self.gdb_session = None self._reset_gui_to_stopped_state() def _set_gdb_breakpoint_action(self): # ... (implementation as before) if not self.gdb_session or not self.gdb_session.is_alive(): messagebox.showerror("Error", "GDB session is not active.", parent=self) return bp_location = self.breakpoint_var.get() if not bp_location: messagebox.showerror( "Input Error", "Breakpoint location cannot be empty.", parent=self ) return self._update_status_bar(f"Setting breakpoint at '{bp_location}'...") try: command_timeout = self.app_settings.get_setting("timeouts", "gdb_command") output = self.gdb_session.set_breakpoint( bp_location, timeout=command_timeout ) self._update_gdb_raw_output(output, append=True) bp_name_display = ( bp_location[:20] + "..." if len(bp_location) > 20 else bp_location ) 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_name_display} (Set)") self.run_button.config(state=tk.NORMAL) self._update_status_bar(f"Breakpoint set at '{bp_location}'.") elif "pending" in output.lower(): self.set_bp_button.config(text=f"BP: {bp_name_display} (Pend)") self.run_button.config(state=tk.NORMAL) self._update_status_bar(f"BP '{bp_location}' pending.") messagebox.showinfo( "Breakpoint Pending", f"Breakpoint at '{bp_location}' is pending.", parent=self, ) else: self._update_status_bar( f"Issue setting BP '{bp_location}'. Check GDB output.", is_error=True, ) except (ConnectionError, TimeoutError) as e: self._handle_gdb_operation_error(f"set breakpoint '{bp_location}'", e) except Exception as e: self._handle_gdb_operation_error( f"set breakpoint '{bp_location}' (unexpected)", e ) def _run_or_continue_gdb_action(self): # ... (implementation as before) if not self.gdb_session or not self.gdb_session.is_alive(): messagebox.showerror("Error", "GDB session is not active.", parent=self) return self._update_parsed_json_output(None) self._disable_save_buttons() try: 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_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() self._update_status_bar( f"Running program with params: '{params_str}'..." ) self._update_gdb_raw_output( f"Executing: run {params_str}\n", append=True ) output = self.gdb_session.run_program(params_str, timeout=run_timeout) else: self._update_status_bar("Continuing program execution...") self._update_gdb_raw_output("Executing: continue\n", append=True) output = self.gdb_session.continue_execution(timeout=run_timeout) self._update_gdb_raw_output(output, append=True) dump_button_state = tk.DISABLED 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 ): self._update_status_bar("Breakpoint hit. Ready to dump variables.") self.dump_var_button.config(state=dump_button_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 Program (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. Check GDB output.", is_error=True ) self.dump_var_button.config(state=dump_button_state) self.program_started_once = True self.run_button.config(text="3. Continue (Risky)") else: self._update_status_bar("Program running/unknown state.") self.dump_var_button.config(state=dump_button_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): # ... (implementation as before) if not self.gdb_session or not self.gdb_session.is_alive(): messagebox.showerror("Error", "GDB session is not active.", parent=self) 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 ): messagebox.showwarning( "Dumper Script Error", "GDB dumper script is not available, not found, or failed to load.\nJSON dump cannot proceed. Check configuration and logs.", parent=self, ) self._check_critical_configs_and_update_gui() return var_expr = self.variable_var.get() if not var_expr: messagebox.showerror( "Input Error", "Variable/Expression to dump cannot be empty.", parent=self, ) return self._update_status_bar(f"Dumping '{var_expr}' to JSON...") self._update_gdb_raw_output( f"Attempting JSON dump of: {var_expr}\n", append=True ) try: dump_timeout = self.app_settings.get_setting("timeouts", "dump_variable") dumped_data = self.gdb_session.dump_variable_to_json( var_expr, timeout=dump_timeout ) self.last_dumped_data = dumped_data self._update_parsed_json_output(dumped_data) if isinstance(dumped_data, dict) and "_gdb_tool_error" in dumped_data: error_msg = dumped_data.get("details", dumped_data["_gdb_tool_error"]) self._update_status_bar( f"Error dumping '{var_expr}': {error_msg}", is_error=True ) self._disable_save_buttons() if "raw_gdb_output" in dumped_data: self._update_gdb_raw_output( f"--- Raw GDB output for failed dump of '{var_expr}' ---\n{dumped_data['raw_gdb_output']}\n--- End ---\n", append=True, ) elif dumped_data is not None: self._update_status_bar(f"Successfully dumped '{var_expr}'.") self._enable_save_buttons_if_data() else: self._update_status_bar( f"Dump of '{var_expr}' returned no data.", is_error=True ) self._disable_save_buttons() except (ConnectionError, TimeoutError) as e: self._handle_gdb_operation_error(f"dump variable '{var_expr}'", e) self.last_dumped_data = None self._disable_save_buttons() self._update_parsed_json_output({"error": str(e)}) except Exception as e: self._handle_gdb_operation_error( f"dump variable '{var_expr}' (unexpected)", e ) self.last_dumped_data = None self._disable_save_buttons() self._update_parsed_json_output({"error": str(e)}) def _reset_gui_to_stopped_state(self): # ... (implementation as before) 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) is_profile_running = ( self.profile_executor_instance and self.profile_executor_instance.is_running ) if hasattr(self, "start_gdb_button"): if gdb_is_configured_correctly and not is_profile_running: self.start_gdb_button.config(state=tk.NORMAL) else: self.start_gdb_button.config(state=tk.DISABLED) if hasattr(self, "set_bp_button"): self.set_bp_button.config(state=tk.DISABLED, text="2. Set Breakpoint") 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, "run_profile_button") and hasattr( self, "profile_selection_combo" ): if gdb_is_configured_correctly and not is_profile_running: if self.profile_selection_combo.get(): 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: self.run_profile_button.config(state=tk.DISABLED) if is_profile_running: self.profile_selection_combo.config(state=tk.DISABLED) else: self.profile_selection_combo.config(state="readonly") if hasattr(self, "save_json_button"): self._disable_save_buttons() self.program_started_once = False self.last_dumped_data = None if ( not is_profile_running and hasattr(self, "status_var") and self.status_var is not None ): self._update_status_bar("GDB session stopped or not active.") def _stop_gdb_session_action(self): # ... (implementation as before) 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: 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) self._update_gdb_raw_output( "GDB session quit command sent.\n", append=True ) except Exception as e: self._handle_gdb_operation_error("stop 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 _enable_save_buttons_if_data(self): # ... (implementation as before) if self.last_dumped_data and not ( isinstance(self.last_dumped_data, dict) and "_gdb_tool_error" in self.last_dumped_data ): if hasattr(self, "save_json_button"): self.save_json_button.config(state=tk.NORMAL) if hasattr(self, "save_csv_button"): self.save_csv_button.config(state=tk.NORMAL) else: self._disable_save_buttons() def _disable_save_buttons(self): # ... (implementation as before) 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, format_type: str): # ... (implementation as before) 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("->", "_") .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, parent=self, ) if not filepath: return self._update_status_bar( f"Saving data as {format_type.upper()} to {os.path.basename(filepath)}..." ) try: if format_type == "json": save_to_json(self.last_dumped_data, filepath) elif format_type == "csv": data_for_csv = self.last_dumped_data if isinstance(data_for_csv, dict) and not isinstance( data_for_csv, list ): data_for_csv = [data_for_csv] elif not isinstance(data_for_csv, list): data_for_csv = [{"value": data_for_csv}] elif ( isinstance(data_for_csv, list) and data_for_csv and not all(isinstance(item, dict) for item in data_for_csv) ): data_for_csv = [{"value": item} for item in data_for_csv] save_to_csv(data_for_csv, filepath) messagebox.showinfo( "Save Successful", f"Data saved to:\n{filepath}", parent=self ) self._update_status_bar(f"Data saved to {os.path.basename(filepath)}.") except Exception as e: logger.error( f"Error saving data as {format_type} to {filepath}: {e}", exc_info=True ) 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: # ... (implementation as before) 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"ProfileExec Status: {message}") def _gui_gdb_output_update(self, message: str) -> None: # ... (implementation as before) self._update_gdb_raw_output(message, append=True) def _gui_json_data_update(self, data: Any) -> None: # ... (implementation as before) self._update_parsed_json_output(data) def _gui_add_execution_log_entry(self, entry: ExecutionLogEntry) -> None: # ... (implementation as before, ensure "breakpoint_spec" key is used for the treeview) 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 to add entry to produced_files_tree: {e}. Entry: {entry}" ) def _clear_produced_files_tree(self) -> None: # ... (implementation as before) 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: # ... (implementation as before) 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(): 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.profile_exec_status_var.set( f"AVVIO PROFILO '{selected_profile_name}' IN CORSO..." ) if self.profile_progressbar and hasattr( self.profile_exec_status_label_big, "master" ): parent_frame_for_progress = self.profile_exec_status_label_big.master self.profile_progressbar.grid( row=1, column=0, columnspan=1, sticky="ew", padx=5, pady=(2, 5), in_=parent_frame_for_progress, ) self.profile_progressbar.start(15) if hasattr(self, "run_profile_button"): self.run_profile_button.config(state=tk.DISABLED) if hasattr(self, "stop_profile_button"): self.stop_profile_button.config(state=tk.NORMAL) if hasattr(self, "profile_selection_combo"): 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 items during profile run.") if hasattr(self, "start_gdb_button"): self.start_gdb_button.config(state=tk.DISABLED) if hasattr(self, "set_bp_button"): self.set_bp_button.config(state=tk.DISABLED) if hasattr(self, "run_button"): self.run_button.config(state=tk.DISABLED) 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) self.last_run_output_path = None if hasattr(self, "open_output_folder_button"): self.open_output_folder_button.config(state=tk.DISABLED) 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, execution_log_callback=self._gui_add_execution_log_entry, ) self._clear_produced_files_tree() 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): # ... (implementation as before) 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): # ... (implementation as before) if not self.winfo_exists(): logger.warning( "_on_profile_execution_finished called but window no longer exists." ) return if self.profile_progressbar: self.profile_progressbar.stop() self.profile_progressbar.grid_remove() final_status_message = "Profile execution finished." 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 ): if hasattr(self, "open_output_folder_button"): self.open_output_folder_button.config(state=tk.NORMAL) else: if hasattr(self, "open_output_folder_button"): self.open_output_folder_button.config(state=tk.DISABLED) logger.warning( f"Output folder path from executor not valid or not found: {self.last_run_output_path}" ) else: if hasattr(self, "open_output_folder_button"): self.open_output_folder_button.config(state=tk.DISABLED) current_gui_status = self.profile_exec_status_var.get() if ( "AVVIO PROFILO" in current_gui_status or "Requesting profile stop" in current_gui_status ): if hasattr(self.profile_executor_instance, "profile_execution_summary"): executor_final_status = ( self.profile_executor_instance.profile_execution_summary.get( "status", "Unknown" ) ) if ( "Error" in executor_final_status or "failed" in executor_final_status.lower() or "issues" in executor_final_status.lower() ): final_status_message = ( f"Profile finished with issues: {executor_final_status}" ) elif executor_final_status not in [ "Initialized", "Pending", "Processing Dumps", ]: final_status_message = f"Profile run completed. Final state: {executor_final_status}" else: final_status_message = "Profile execution completed." else: final_status_message = "Profile execution completed." elif ( "Error:" in current_gui_status or "failed" in current_gui_status.lower() or "issues" 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) if ( hasattr(self, "profile_selection_combo") and self.profile_selection_combo.get() ): if hasattr(self, "run_profile_button"): self.run_profile_button.config(state=tk.NORMAL) else: if hasattr(self, "run_profile_button"): self.run_profile_button.config(state=tk.DISABLED) if hasattr(self, "stop_profile_button"): self.stop_profile_button.config(state=tk.DISABLED) if hasattr(self, "profile_selection_combo"): self.profile_selection_combo.config(state="readonly") 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: logger.warning(f"TclError re-enabling menubar items: {e}") self._check_critical_configs_and_update_gui() self.profile_executor_instance = None logger.info( "Profile execution GUI updates completed, executor instance cleared." ) def _stop_current_profile_action(self) -> None: # ... (implementation as before) 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 currently running to stop.") def _open_last_run_output_folder(self) -> None: # ... (implementation as before) if not self.last_run_output_path or not os.path.isdir( self.last_run_output_path ): messagebox.showwarning( "No Output Folder", "The output folder for the last run is not available or does not exist.", parent=self, ) if hasattr(self, "open_output_folder_button"): self.open_output_folder_button.config(state=tk.DISABLED) return try: logger.info(f"Opening output folder: {self.last_run_output_path}") if sys.platform == "win32": os.startfile(self.last_run_output_path) elif sys.platform == "darwin": subprocess.run(["open", self.last_run_output_path], check=True) else: subprocess.run(["xdg-open", self.last_run_output_path], check=True) except FileNotFoundError: messagebox.showerror( "Error", f"Could not find the file manager command ('xdg-open' or 'open'). Please open the folder manually:\n{self.last_run_output_path}", parent=self, ) except Exception as e: logger.error( f"Failed to open output folder '{self.last_run_output_path}': {e}" ) messagebox.showerror( "Error Opening Folder", f"Could not open the output folder:\n{self.last_run_output_path}\n\nError: {e}", parent=self, ) def _on_closing_window(self): # ... (implementation as before) logger.info("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", "An automated profile is currently running.\nDo 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 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 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() ) save_success = self.app_settings.save_settings() if not save_success: 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 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(): logger.info("User cancelled exit while manual GDB session is active.") should_destroy = False 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( "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): # ... (implementation as before) def __init__(self, text_widget: scrolledtext.ScrolledText): super().__init__() self.text_widget = text_widget self._active = True def emit(self, record): 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 except Exception: self._active = False def close(self): self._active = False super().close()