# 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 typing import ( Optional, Dict, Any, Callable, List, Tuple, ) 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, ) from .config_window import ConfigWindow from .profile_manager_window import ProfileManagerWindow logger = logging.getLogger(__name__) try: from cpp_python_debug import _version as wrapper_version WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})" WRAPPER_BUILD_INFO = f"Wrapper Built: {wrapper_version.BUILD_TIMESTAMP}" except ImportError: WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)" WRAPPER_BUILD_INFO = "Wrapper build time unknown" DEFAULT_VERSION = "0.0.0+unknown" DEFAULT_COMMIT = "Unknown" DEFAULT_BRANCH = "Unknown" class GDBGui(tk.Tk): def __init__(self): super().__init__() self.app_settings = AppSettings() self.gui_log_handler: Optional[ScrolledTextLogHandler] = None self.title( f"GDB Debug GUI - {WRAPPER_APP_VERSION_STRING} - 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 # Used by manual mode for saving 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 self.status_var: Optional[tk.StringVar] = None 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) def _create_menus(self): self.menubar = Menu(self) self.config(menu=self.menubar) options_menu = Menu(self.menubar, tearoff=0) self.menubar.add_cascade(label="Options", menu=options_menu) options_menu.add_command( label="Configure Application...", command=self._open_config_window ) options_menu.add_separator() options_menu.add_command(label="Exit", command=self._on_closing_window) profiles_menu = Menu(self.menubar, tearoff=0) self.menubar.add_cascade(label="Profiles", menu=profiles_menu) profiles_menu.add_command( label="Manage Profiles...", command=self._open_profile_manager_window ) def _open_config_window(self): 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): 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): 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"): 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 - {WRAPPER_APP_VERSION_STRING} - Settings: {os.path.basename(self.app_settings.config_filepath)}" ) def _create_widgets(self): main_frame = ttk.Frame(self, padding="10") main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) self.columnconfigure(0, weight=1) 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): 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): 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): 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: 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') # Note: Progressbar is gridded/removed by run/finish methods 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): 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): 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") # MODIFIED TAB NAME 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): 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): 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): 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 if initial_dir else None), parent=self) if path: target_var.set(path) def _browse_target_exe(self): self._browse_file("Select Target Application Executable", self.exe_path_var, [("Executable files", ("*.exe", "*")), ("All files", "*.*")]) def _update_gdb_raw_output(self, text: str, append: bool = True): 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): # This method now handles both full JSON (manual mode) and status JSON (profile mode) 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): # Check if it's a status payload from the dumper (used in profile mode) if "status" in data_to_display and ("filepath_written" in data_to_display or "message" in data_to_display): status_text = f"Status: {data_to_display.get('status', 'N/A')}\n" status_text += f"Variable: {data_to_display.get('variable_dumped', 'N/A')}\n" status_text += f"File Written: {data_to_display.get('filepath_written', 'N/A')}\n" status_text += f"Requested Format: {data_to_display.get('target_format_requested', 'N/A')}\n" status_text += f"Message: {data_to_display.get('message', 'N/A')}\n" if data_to_display.get("details"): status_text += f"Details: {data_to_display.get('details')}\n" self.parsed_json_output_text.insert("1.0", status_text) else: # Assume it's full JSON data (manual mode) 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)}") elif isinstance(data_to_display, list): # Could be full JSON that is a 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 list for GUI: {e}") self.parsed_json_output_text.insert("1.0", f"Error displaying list: {e}\nRaw data: {str(data_to_display)}") else: # Primitive or other types 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): if hasattr(self, "status_var") and self.status_var is not None: self.status_var.set(message) # Optionally change color based on is_error, but status_bar_widget needs to be stored for this # if self.status_bar_widget: # self.status_bar_widget.config(foreground="red" if is_error else "black") def _handle_gdb_operation_error(self, operation_name: str, error_details: Any): error_message = f"Error during GDB operation '{operation_name}': {error_details}" logger.error(error_message) self._update_gdb_raw_output(f"ERROR: {error_message}\n", append=True) self._update_status_bar(f"Error: {operation_name} failed.", is_error=True) if self.winfo_exists(): messagebox.showerror("GDB Operation Error", error_message, parent=self) def _start_gdb_session_action(self): if self.profile_executor_instance and self.profile_executor_instance.is_running: messagebox.showwarning("Profile Running", "An automated profile is running. Please stop it first.", parent=self) return gdb_exe = self.app_settings.get_setting("general", "gdb_executable_path") target_exe = self.exe_path_var.get() gdb_script = self.app_settings.get_setting("general", "gdb_dumper_script_path") if not gdb_exe or not os.path.isfile(gdb_exe): messagebox.showerror("Configuration Error", "GDB executable path not configured correctly.", 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 executable path is required or not found.", parent=self) return dumper_options = self.app_settings.get_category_settings("dumper_options", {}) if self.gdb_session and self.gdb_session.is_alive(): messagebox.showwarning("Session Active", "A GDB session is already active. 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) quit_timeout_on_no_symbols = self.app_settings.get_setting("timeouts", "gdb_quit", 10) self.gdb_session = GDBSession(gdb_path=gdb_exe, executable_path=target_exe, gdb_script_full_path=gdb_script, dumper_options=dumper_options) self.gdb_session.start(timeout=startup_timeout) self._update_gdb_raw_output(f"GDB session started for '{os.path.basename(target_exe)}'.\n") if not self.gdb_session.symbols_found: self._update_gdb_raw_output("ERROR: No debugging symbols found. Session terminated.\n", append=True) if self.winfo_exists(): messagebox.showwarning("No Debug Symbols", f"No debug symbols in '{os.path.basename(target_exe)}'. Session aborted.", parent=self) self._update_status_bar("GDB aborted: No debug symbols.", is_error=True) if self.gdb_session.is_alive(): self.gdb_session.quit(timeout=quit_timeout_on_no_symbols) self.gdb_session = None; self._reset_gui_to_stopped_state(); self._check_critical_configs_and_update_gui(); return if gdb_script and os.path.isfile(gdb_script): if self.gdb_session.gdb_script_sourced_successfully: self._update_gdb_raw_output(f"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)") else: self._update_gdb_raw_output(f"Warning: Dumper script '{os.path.basename(gdb_script)}' FAILED to load.\n", append=True) self._update_status_bar(f"GDB active. Dumper script load issue.", is_error=True) if self.winfo_exists(): messagebox.showwarning("Dumper Script Issue", f"Dumper '{os.path.basename(gdb_script)}' failed to load. JSON dump affected.", parent=self) self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Load Failed!)") elif gdb_script: # Path specified but not valid file self._update_gdb_raw_output(f"Warning: Dumper script path '{gdb_script}' is invalid.\n", append=True) self._update_status_bar(f"GDB active. Dumper script path invalid.", is_error=True) self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Path Invalid!)") else: self._update_gdb_raw_output("No dumper script. JSON dump via script unavailable.\n", append=True) self._update_status_bar("GDB session active. No dumper script.") 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"); 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) if hasattr(self, 'profile_selection_combo'): self.profile_selection_combo.config(state=tk.DISABLED) self.program_started_once = False; self.last_dumped_data = None; self._disable_save_buttons() except (FileNotFoundError, ConnectionError, TimeoutError) as e_specific: self._handle_gdb_operation_error("start session", e_specific); 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_WINDOW CATCH-ALL for start GDB: {type(e).__name__}: '{e}'", exc_info=True) self._handle_gdb_operation_error("start session (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): 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"BP '{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): 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_is_valid_and_loaded = 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.NORMAL if dumper_is_valid_and_loaded else tk.DISABLED 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): if not self.gdb_session or not self.gdb_session.is_alive(): messagebox.showerror("Error", "GDB session is 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() 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") # For manual mode, target_output_filepath is None, so full JSON comes to console dumped_data = self.gdb_session.dump_variable_to_json(var_expr, timeout=dump_timeout, target_output_filepath=None) self.last_dumped_data = dumped_data # Store for saving self._update_parsed_json_output(dumped_data) # This will show the full JSON if isinstance(dumped_data, dict) and ("_gdb_tool_error" in dumped_data or dumped_data.get("status") == "error"): error_msg = dumped_data.get("details", dumped_data.get("message", dumped_data.get("_gdb_tool_error", "Unknown dumper 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 for failed dump '{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): gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path") gdb_is_ok = 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 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"): can_run_profile = gdb_is_ok and not is_prof_running and 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) if hasattr(self, "save_json_button"): self._disable_save_buttons() self.program_started_once = False; self.last_dumped_data = 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 _stop_gdb_session_action(self): 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: # Try to kill inferior only if it was run kill_output = self.gdb_session.kill_program(timeout=kill_timeout) self._update_gdb_raw_output(f"Kill command output:\n{kill_output}\n", append=True) self.gdb_session.quit(timeout=quit_timeout) 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() # Re-enable profile controls else: # If no session, just ensure GUI is reset self._reset_gui_to_stopped_state() self._load_and_populate_profiles_for_automation_tab() def _enable_save_buttons_if_data(self): if self.last_dumped_data and not (isinstance(self.last_dumped_data, dict) and "_gdb_tool_error" in self.last_dumped_data): 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): 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): 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 to save.", parent=self); return file_ext = f".{format_type.lower()}"; file_types = [(f"{format_type.upper()} files", f"*{file_ext}"), ("All files", "*.*")] var_sugg = self.variable_var.get().replace(" ", "_").replace("*","ptr").replace("->","_").replace(":","_") default_fname = f"{var_sugg}_dump{file_ext}" if var_sugg 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_fname, 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: # For ProfileExecutor 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: # For ProfileExecutor self._update_gdb_raw_output(message, append=True) def _gui_json_data_update(self, data: Any) -> None: # For ProfileExecutor (receives status JSON) self._update_parsed_json_output(data) # This will now call the modified version def _gui_add_execution_log_entry(self, entry: ExecutionLogEntry) -> None: # For ProfileExecutor 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 to produced_files_tree: {e}. Entry: {entry}") def _clear_produced_files_tree(self) -> None: 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: selected_profile_name = self.profile_selection_combo.get() if not selected_profile_name: messagebox.showwarning("No Profile", "Please select a profile.", parent=self); return if self.profile_executor_instance and self.profile_executor_instance.is_running: messagebox.showwarning("Profile Running", "A profile is already running.", parent=self); return if self.gdb_session and self.gdb_session.is_alive(): messagebox.showerror("GDB Active", "Manual GDB session active. Stop it first.", parent=self); return profile_data = self.available_profiles_map.get(selected_profile_name) if not profile_data: messagebox.showerror("Error", f"Cannot find data for profile '{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 = 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) 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.") 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): 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): if not self.winfo_exists(): logger.warning("Profile finish callback but window gone."); 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) 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"Output folder invalid: {self.last_run_output_path}") else: if hasattr(self, 'open_output_folder_button'): self.open_output_folder_button.config(state=tk.DISABLED) # type: ignore # Determine a more precise final status message current_gui_status = self.profile_exec_status_var.get() if "STARTING PROFILE" in current_gui_status or "Requesting profile stop" 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") if "Error" in exec_final_status or "failed" in exec_final_status.lower() or "issues" in exec_final_status.lower(): 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. Final state: {exec_final_status}" # else keep default "Profile execution completed." if summary not available 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}" # Keep existing error else: # If no explicit error and not starting phase, use current status 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(): # type: ignore if hasattr(self, 'run_profile_button'): 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 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: logger.warning(f"TclError re-enabling menubar: {e}") self._check_critical_configs_and_update_gui() # Re-enable manual GDB start button if GDB config is OK self.profile_executor_instance = None logger.info("Profile execution GUI updates completed.") def _stop_current_profile_action(self) -> None: if self.profile_executor_instance and self.profile_executor_instance.is_running: self.profile_exec_status_var.set("Requesting profile stop...") self.profile_executor_instance.request_stop() 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: logger.info(f"Attempting to open output folder: '{self.last_run_output_path}'") if not self.last_run_output_path or not os.path.isdir(self.last_run_output_path): messagebox.showwarning("No Output Folder", "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: 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: logger.error("File manager command not found.", exc_info=True) messagebox.showerror("Error", f"Could not find file manager. Path: {self.last_run_output_path}", parent=self) except subprocess.CalledProcessError as cpe: logger.error(f"Command to open folder failed: {cpe}", exc_info=True) messagebox.showerror("Error", f"Failed to open folder (code {cpe.returncode}): {self.last_run_output_path}\nError: {cpe.stderr.decode(errors='replace') if cpe.stderr else 'Unknown'}", parent=self) except Exception as e: logger.error(f"Failed to open output folder: {e}", exc_info=True) messagebox.showerror("Error", f"Could not open folder: {self.last_run_output_path}\nError: {e}", parent=self) def _on_closing_window(self): 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", "Profile running. Stop it and 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. 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", "Manual GDB session active. Stop it and 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 window is already gone, try to stop 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.") self.destroy(); logger.info("Tkinter window destroyed.") else: logger.info("Window destruction aborted.") 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): 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 # Widget likely destroyed except Exception: self._active = False