# 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 import shutil from datetime import datetime 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, # Still needed for type hints and its constants if used ExecutionLogEntry, GDB_DUMP_EXTENSION, sanitize_filename_component ) 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" MANUAL_DUMP_PROFILE_NAME_PLACEHOLDER = "ManualDump" MANUAL_DUMP_FILENAME_PATTERN = "{profile_name}_{app_name}_{breakpoint}_{variable}_{timestamp}{extension}" # Added app_name MANUAL_DUMPS_SUBFOLDER = "manual_gdb_dumps" 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", "850x800") ) self.gdb_session: Optional[GDBSession] = None self.last_manual_gdb_dump_filepath: Optional[str] = None self.program_started_once: bool = False self.gdb_exe_status_var = tk.StringVar(value="GDB: Checking...") self.gdb_dumper_status_var = tk.StringVar(value="Dumper Script: Checking...") 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.manual_dumps_output_path: Optional[str] = None self.profile_progressbar: Optional[ttk.Progressbar] = None self.status_bar_widget: Optional[ttk.Label] = None self.status_var: Optional[tk.StringVar] = None # MODIFICA: Chiamare _prepare_manual_dumps_directory PRIMA di _create_widgets self._prepare_manual_dumps_directory() self._create_menus() self._create_widgets() # Ora _populate_manual_debug_tab avrà self.manual_dumps_output_path già impostato self._setup_logging_redirect_to_gui() self._check_critical_configs_and_update_gui() self._load_and_populate_profiles_for_automation_tab() # self._prepare_manual_dumps_directory() # Rimosso da qui self.protocol("WM_DELETE_WINDOW", self._on_closing_window) def _prepare_manual_dumps_directory(self): # ... (implementazione come prima, ma ora viene chiamata prima) ... base_dir = self.app_settings.config_dir self.manual_dumps_output_path = os.path.join(base_dir, MANUAL_DUMPS_SUBFOLDER) try: os.makedirs(self.manual_dumps_output_path, exist_ok=True) logger.info(f"Manual dumps directory ensured: {self.manual_dumps_output_path}") # Se il pulsante esiste già e la directory è stata creata/confermata, abilitalo if hasattr(self, 'open_manual_dumps_folder_button') and self.open_manual_dumps_folder_button: self.open_manual_dumps_folder_button.config(state=tk.NORMAL) except OSError as e: logger.error(f"Could not create manual dumps directory '{self.manual_dumps_output_path}': {e}") self.manual_dumps_output_path = None if hasattr(self, 'open_manual_dumps_folder_button') and self.open_manual_dumps_folder_button: self.open_manual_dumps_folder_button.config(state=tk.DISABLED) # Non mostrare messagebox qui perché potrebbe essere chiamato prima che la GUI principale sia pronta # Lo stato del pulsante rifletterà il successo o fallimento. def _create_menus(self): # Unchanged 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): # Unchanged logger.debug("Opening configuration window.") config_win = ConfigWindow(self, self.app_settings); self.wait_window(config_win) logger.debug("Configuration window closed."); self._check_critical_configs_and_update_gui() self._prepare_manual_dumps_directory() def _open_profile_manager_window(self): # Unchanged 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): # Unchanged logger.info("Checking critical configurations..."); gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path") dumper_script_path = self.app_settings.get_setting("general", "gdb_dumper_script_path"); gdb_ok = False if gdb_exe_path and os.path.isfile(gdb_exe_path): self.gdb_exe_status_var.set(f"GDB: {os.path.basename(gdb_exe_path)} (OK)"); gdb_ok = True elif gdb_exe_path: self.gdb_exe_status_var.set(f"GDB: '{gdb_exe_path}' (Not Found/Invalid!)") else: self.gdb_exe_status_var.set("GDB: Not Configured! Set in Options > Configure.") if dumper_script_path and os.path.isfile(dumper_script_path): self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(dumper_script_path)} (OK)") elif dumper_script_path: self.gdb_dumper_status_var.set(f"Dumper: '{dumper_script_path}' (Not Found/Invalid!)") else: self.gdb_dumper_status_var.set("Dumper: Not Configured (Optional).") 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): # Unchanged main_frame = ttk.Frame(self, padding="10"); main_frame.grid(row=0, column=0, sticky="nsew") self.columnconfigure(0, weight=1); self.rowconfigure(0, weight=1) main_frame.rowconfigure(0, weight=0); main_frame.rowconfigure(1, weight=1); main_frame.rowconfigure(2, weight=8); main_frame.rowconfigure(3, weight=0); main_frame.columnconfigure(0, weight=1) self._create_config_status_widgets(main_frame); self._create_mode_notebook_widgets(main_frame); self._create_output_log_widgets(main_frame); self._create_status_bar(main_frame) def _create_config_status_widgets(self, parent_frame: ttk.Frame): # Unchanged config_status_frame = ttk.LabelFrame(parent_frame, text="Critical Configuration Status", padding=(10,5,10,10)); config_status_frame.grid(row=0, column=0, sticky="ew", pady=5, padx=0) config_status_frame.columnconfigure(1, weight=1); config_status_frame.columnconfigure(3, weight=1) ttk.Label(config_status_frame, text="GDB:").grid(row=0, column=0, sticky=tk.W, padx=(5,0), pady=5) self.gdb_exe_status_label = ttk.Label(config_status_frame, textvariable=self.gdb_exe_status_var, relief="sunken", padding=(5,2), anchor=tk.W); self.gdb_exe_status_label.grid(row=0, column=1, sticky="ew", padx=(0,10), pady=5) ttk.Label(config_status_frame, text="Dumper:").grid(row=0, column=2, sticky=tk.W, padx=(5,0), pady=5) self.gdb_dumper_status_label = ttk.Label(config_status_frame, textvariable=self.gdb_dumper_status_var, relief="sunken", padding=(5,2), anchor=tk.W); self.gdb_dumper_status_label.grid(row=0, column=3, sticky="ew", padx=(0,10), pady=5) ttk.Button(config_status_frame, text="Configure...", command=self._open_config_window).grid(row=0, column=4, padx=(5,5), pady=5, sticky=tk.E) def _create_mode_notebook_widgets(self, parent_frame: ttk.Frame): # Unchanged 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"); mode_notebook.add(self.automated_exec_frame, text="Automated Profile Execution"); self._populate_automated_execution_tab(self.automated_exec_frame) # type: ignore 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="ew", pady=5) manual_target_settings_frame.columnconfigure(1, weight=1) row_idx = 0 # ... (campi Target, Params, Breakpoint, Variable come prima) ... ttk.Label(manual_target_settings_frame, text="Target Executable:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2) ttk.Entry(manual_target_settings_frame, textvariable=self.exe_path_var, width=70).grid(row=row_idx, column=1, sticky="ew", padx=5, pady=2) ttk.Button(manual_target_settings_frame, text="Browse...", command=self._browse_target_exe).grid(row=row_idx, column=2, padx=5, pady=2); row_idx += 1 ttk.Label(manual_target_settings_frame, text="Program Parameters:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2) ttk.Entry(manual_target_settings_frame, textvariable=self.params_var).grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=2); row_idx += 1 ttk.Label(manual_target_settings_frame, text="Breakpoint Location:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2) ttk.Entry(manual_target_settings_frame, textvariable=self.breakpoint_var).grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=2); row_idx += 1 ttk.Label(manual_target_settings_frame, text="Examples: main, file.cpp:123", foreground="gray", font=("TkDefaultFont", 8)).grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0,5)); row_idx += 1 ttk.Label(manual_target_settings_frame, text="Variable/Expression:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2) ttk.Entry(manual_target_settings_frame, textvariable=self.variable_var).grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=2) manual_session_control_frame = ttk.LabelFrame(parent_tab_frame, text="Session Control", padding="10") manual_session_control_frame.grid(row=1, column=0, sticky="ew", pady=(10,5)) button_flow_frame = ttk.Frame(manual_session_control_frame); button_flow_frame.pack(fill=tk.X, expand=True) # ... (pulsanti Start GDB, Set BP, Run, Dump Var, Stop GDB come prima) ... self.start_gdb_button = ttk.Button(button_flow_frame, text="1. Start GDB", command=self._start_gdb_session_action, state=tk.DISABLED); self.start_gdb_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True) self.set_bp_button = ttk.Button(button_flow_frame, text="2. Set BP", command=self._set_gdb_breakpoint_action, state=tk.DISABLED); self.set_bp_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True) self.run_button = ttk.Button(button_flow_frame, text="3. Run", command=self._run_or_continue_gdb_action, state=tk.DISABLED); self.run_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True) self.dump_var_button = ttk.Button(button_flow_frame, text="4. Dump Var", command=self._dump_gdb_variable_action, state=tk.DISABLED); self.dump_var_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True) self.stop_gdb_button = ttk.Button(button_flow_frame, text="Stop GDB", command=self._stop_gdb_session_action, state=tk.DISABLED); self.stop_gdb_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True) manual_save_data_frame = ttk.LabelFrame(parent_tab_frame, text="Save/Manage Dumped Data", padding="10") manual_save_data_frame.grid(row=2, column=0, sticky="ew", pady=5) self.save_json_button = ttk.Button(manual_save_data_frame, text="Save as JSON", command=lambda: self._save_dumped_data("json"), state=tk.DISABLED) self.save_json_button.pack(side=tk.LEFT, padx=5, pady=5) self.save_csv_button = ttk.Button(manual_save_data_frame, text="Save as CSV", command=lambda: self._save_dumped_data("csv"), state=tk.DISABLED) self.save_csv_button.pack(side=tk.LEFT, padx=5, pady=5) # MODIFICA: Lo stato iniziale del pulsante dipende da self.manual_dumps_output_path # che ora dovrebbe essere impostato correttamente prima che questo metodo venga chiamato. self.open_manual_dumps_folder_button = ttk.Button( manual_save_data_frame, text="Open Dumps Folder", command=self._open_manual_dumps_folder, state=(tk.NORMAL if self.manual_dumps_output_path else tk.DISABLED) ) self.open_manual_dumps_folder_button.pack(side=tk.LEFT, padx=5, pady=5) def _populate_automated_execution_tab(self, parent_tab_frame: ttk.Frame) -> None: # Unchanged parent_tab_frame.columnconfigure(0, weight=1); parent_tab_frame.rowconfigure(0, weight=0); parent_tab_frame.rowconfigure(1, weight=0); parent_tab_frame.rowconfigure(2, weight=1); parent_tab_frame.rowconfigure(3, weight=0) auto_control_frame = ttk.LabelFrame(parent_tab_frame, text="Profile Execution Control", padding="10"); auto_control_frame.grid(row=0, column=0, sticky="ew", pady=5); auto_control_frame.columnconfigure(1, weight=1) ttk.Label(auto_control_frame, text="Select Profile:").grid(row=0, column=0, padx=(5,2), pady=5, sticky="w") self.profile_selection_combo = ttk.Combobox(auto_control_frame, state="readonly", width=35, textvariable=tk.StringVar()); self.profile_selection_combo.grid(row=0, column=1, padx=(0,5), pady=5, sticky="ew") self.run_profile_button = ttk.Button(auto_control_frame, text="Run Profile", command=self._run_selected_profile_action, state=tk.DISABLED); self.run_profile_button.grid(row=0, column=2, padx=(0,2), pady=5, sticky="ew") self.stop_profile_button = ttk.Button(auto_control_frame, text="Stop Profile", command=self._stop_current_profile_action, state=tk.DISABLED); self.stop_profile_button.grid(row=0, column=3, padx=(0,5), pady=5, sticky="ew") progress_status_frame = ttk.Frame(parent_tab_frame); progress_status_frame.grid(row=1, column=0, sticky="ew", pady=(5,0)); progress_status_frame.columnconfigure(0, weight=1); progress_status_frame.rowconfigure(0, weight=1); progress_status_frame.rowconfigure(1, weight=0) self.profile_exec_status_label_big = ttk.Label(progress_status_frame, textvariable=self.profile_exec_status_var, font=("TkDefaultFont", 10, "bold"), anchor=tk.NW, justify=tk.LEFT); self.profile_exec_status_label_big.grid(row=0, column=0, sticky="new", padx=5, pady=(0,2)) def _configure_wraplength_for_status_label(event): new_width = event.width - 15 if new_width > 20 and hasattr(self, 'profile_exec_status_label_big') and self.profile_exec_status_label_big.winfo_exists(): self.profile_exec_status_label_big.config(wraplength=new_width) progress_status_frame.bind("", _configure_wraplength_for_status_label) self.profile_progressbar = ttk.Progressbar(progress_status_frame, orient=tk.HORIZONTAL, mode='indeterminate') produced_files_frame = ttk.LabelFrame(parent_tab_frame, text="Produced Files Log", padding="10"); produced_files_frame.grid(row=2, column=0, sticky="nsew", pady=(5,0)); produced_files_frame.columnconfigure(0, weight=1); produced_files_frame.rowconfigure(0, weight=1) self.produced_files_tree = ttk.Treeview(produced_files_frame, columns=("timestamp", "breakpoint_spec", "variable", "file", "status", "details"), show="headings", selectmode="browse"); self.produced_files_tree.grid(row=0, column=0, sticky="nsew") headings = {"timestamp": "Time", "breakpoint_spec": "Breakpoint Spec", "variable": "Variable", "file": "File Produced", "status": "Status", "details": "Details"} widths = {"timestamp": 130, "breakpoint_spec": 150, "variable": 150, "file": 180, "status": 80, "details": 180} minwidths = {"timestamp": 120, "breakpoint_spec": 100, "variable": 100, "file": 150, "status": 60, "details": 150} stretches = {"timestamp": False, "breakpoint_spec": True, "variable": True, "file": True, "status": False, "details": True} for col, text in headings.items(): self.produced_files_tree.heading(col, text=text, anchor=tk.W); self.produced_files_tree.column(col, width=widths[col], minwidth=minwidths[col], stretch=stretches[col]) tree_scrollbar_y = ttk.Scrollbar(produced_files_frame, orient=tk.VERTICAL, command=self.produced_files_tree.yview); tree_scrollbar_y.grid(row=0, column=1, sticky="ns") tree_scrollbar_x = ttk.Scrollbar(produced_files_frame, orient=tk.HORIZONTAL, command=self.produced_files_tree.xview); tree_scrollbar_x.grid(row=1, column=0, sticky="ew") self.produced_files_tree.configure(yscrollcommand=tree_scrollbar_y.set, xscrollcommand=tree_scrollbar_x.set) folder_button_frame = ttk.Frame(parent_tab_frame); folder_button_frame.grid(row=3, column=0, sticky="e", pady=(5,0)) self.open_output_folder_button = ttk.Button(folder_button_frame, text="Open Profile Output Folder", command=self._open_last_run_output_folder, state=tk.DISABLED); self.open_output_folder_button.pack(side=tk.RIGHT, padx=5, pady=0) def _load_and_populate_profiles_for_automation_tab(self): # Unchanged 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 via 'Profiles > Manage'.") def _create_output_log_widgets(self, parent_frame: ttk.Frame): # Unchanged output_log_notebook = ttk.Notebook(parent_frame); output_log_notebook.grid(row=2, column=0, columnspan=1, sticky="nsew", pady=(5,0), padx=0) log_text_height = 12 self.gdb_raw_output_text = scrolledtext.ScrolledText(output_log_notebook, wrap=tk.WORD, height=log_text_height, state=tk.DISABLED, font=("Consolas", 9)); output_log_notebook.add(self.gdb_raw_output_text, text="GDB Raw Output") self.parsed_json_output_text = scrolledtext.ScrolledText(output_log_notebook, wrap=tk.WORD, height=log_text_height, state=tk.DISABLED, font=("Consolas", 9)); output_log_notebook.add(self.parsed_json_output_text, text="Parsed JSON/Status Output") self.app_log_text = scrolledtext.ScrolledText(output_log_notebook, wrap=tk.WORD, height=log_text_height, state=tk.DISABLED, font=("Consolas", 9)); output_log_notebook.add(self.app_log_text, text="Application Log") def _create_status_bar(self, parent_frame: ttk.Frame): # Unchanged self.status_var = tk.StringVar(value="Ready."); self.status_bar_widget = ttk.Label(parent_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) self.status_bar_widget.grid(row=3, column=0, columnspan=1, sticky="ew", pady=(5,0), ipady=2, padx=0) def _setup_logging_redirect_to_gui(self): # Unchanged if not hasattr(self, "app_log_text") or not self.app_log_text: logger.error("app_log_text not found."); return self.gui_log_handler = ScrolledTextLogHandler(self.app_log_text); formatter = logging.Formatter('%(asctime)s [%(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): # Unchanged 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): self._browse_file("Select Target Executable", self.exe_path_var, [("Executable files", ("*.exe", "*")), ("All files", "*.*")]) # Unchanged def _update_gdb_raw_output(self, text: str, append: bool = True): # Unchanged 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): # Unchanged (handles status payload) if not hasattr(self, "parsed_json_output_text") or not self.parsed_json_output_text.winfo_exists(): return self.parsed_json_output_text.config(state=tk.NORMAL); self.parsed_json_output_text.delete("1.0", tk.END) if data_to_display is None: self.parsed_json_output_text.insert("1.0", "") elif isinstance(data_to_display, dict): if "status" in data_to_display and ("filepath_written" in data_to_display or "message" in data_to_display): status_text = f"Status: {data_to_display.get('status', 'N/A')}\nVar: {data_to_display.get('variable_dumped', 'N/A')}\n" \ f"File: {data_to_display.get('filepath_written', 'N/A')}\nFmt Req: {data_to_display.get('target_format_requested', 'N/A')}\n" \ f"Msg: {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: try: self.parsed_json_output_text.insert("1.0", json.dumps(data_to_display, indent=2, ensure_ascii=False)) except Exception as e: self.parsed_json_output_text.insert("1.0", f"Err JSON: {e}\nRaw: {str(data_to_display)}") elif isinstance(data_to_display, list): try: self.parsed_json_output_text.insert("1.0", json.dumps(data_to_display, indent=2, ensure_ascii=False)) except Exception as e: self.parsed_json_output_text.insert("1.0", f"Err list: {e}\nRaw: {str(data_to_display)}") else: self.parsed_json_output_text.insert("1.0", str(data_to_display)) self.parsed_json_output_text.see("1.0"); self.parsed_json_output_text.config(state=tk.DISABLED) def _update_status_bar(self, message: str, is_error: bool = False): # Unchanged if hasattr(self, "status_var") and self.status_var is not None: self.status_var.set(message) def _handle_gdb_operation_error(self, op_name: str, err_details: Any): # Unchanged msg = f"Error GDB op '{op_name}': {err_details}"; logger.error(msg) self._update_gdb_raw_output(f"ERROR: {msg}\n", True); self._update_status_bar(f"Error: {op_name} failed.", True) if self.winfo_exists(): messagebox.showerror("GDB Error", msg, parent=self) def _start_gdb_session_action(self): # Unchanged if self.profile_executor_instance and self.profile_executor_instance.is_running: messagebox.showwarning("Profile Running", "Profile running. Stop first.", parent=self); return gdb_exe = self.app_settings.get_setting("general", "gdb_executable_path"); target_exe = self.exe_path_var.get(); gdb_script = self.app_settings.get_setting("general", "gdb_dumper_script_path") if not gdb_exe or not os.path.isfile(gdb_exe): messagebox.showerror("Config Error", "GDB path incorrect.", parent=self); self._check_critical_configs_and_update_gui(); return if not target_exe or not os.path.exists(target_exe): messagebox.showerror("Input Error", "Target exe required/not found.", parent=self); return dumper_opts = self.app_settings.get_category_settings("dumper_options", {}) if self.gdb_session and self.gdb_session.is_alive(): messagebox.showwarning("Session Active", "GDB active. Stop first.", parent=self); return self._update_status_bar("Starting GDB..."); self._update_gdb_raw_output("Starting GDB session...\n", False); self._update_parsed_json_output(None) try: startup_timeout = self.app_settings.get_setting("timeouts", "gdb_start", 30); quit_to_no_sym = 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_opts) self.gdb_session.start(timeout=startup_timeout); self._update_gdb_raw_output(f"GDB started for '{os.path.basename(target_exe)}'.\n") if not self.gdb_session.symbols_found: self._update_gdb_raw_output("ERROR: No debug symbols. Session terminated.\n", True) if self.winfo_exists(): messagebox.showwarning("No Debug Symbols", f"No symbols in '{os.path.basename(target_exe)}'. Aborted.", parent=self) self._update_status_bar("GDB aborted: No symbols.", True); if self.gdb_session.is_alive(): self.gdb_session.quit(timeout=quit_to_no_sym) 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 '{os.path.basename(gdb_script)}' sourced.\n",True); self._update_status_bar(f"GDB active. Dumper '{os.path.basename(gdb_script)}' loaded."); self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Loaded)") else: self._update_gdb_raw_output(f"Warn: Dumper '{os.path.basename(gdb_script)}' FAILED load.\n",True); self._update_status_bar(f"GDB active. Dumper load issue.",is_error=True); if self.winfo_exists(): messagebox.showwarning("Dumper Issue", f"Dumper '{os.path.basename(gdb_script)}' failed load.", parent=self); self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Load Failed!)") elif gdb_script: self._update_gdb_raw_output(f"Warn: Dumper path '{gdb_script}' invalid.\n",True); self._update_status_bar(f"GDB active. Dumper path invalid.",is_error=True); self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Path Invalid!)") else: self._update_gdb_raw_output("No dumper. JSON dump via script unavailable.\n",True); self._update_status_bar("GDB active. No dumper."); self.gdb_dumper_status_var.set("Dumper: Not Configured.") self.start_gdb_button.config(state=tk.DISABLED); self.set_bp_button.config(state=tk.NORMAL); self.run_button.config(state=tk.DISABLED, text="3. Run Program"); self.dump_var_button.config(state=tk.DISABLED); self.stop_gdb_button.config(state=tk.NORMAL) if hasattr(self, 'run_profile_button'): self.run_profile_button.config(state=tk.DISABLED) # type: ignore if hasattr(self, 'profile_selection_combo'): self.profile_selection_combo.config(state=tk.DISABLED) # type: ignore self.program_started_once = False; self.last_manual_gdb_dump_filepath = None; self._disable_save_buttons() except (FileNotFoundError, ConnectionError, TimeoutError) as e: self._handle_gdb_operation_error("start session", e); self.gdb_session = None; self._reset_gui_to_stopped_state(); self._check_critical_configs_and_update_gui() except Exception as e: logger.critical(f"MAIN CATCH start GDB: {type(e).__name__}: '{e}'", exc_info=True); self._handle_gdb_operation_error("start (unexpected)", e); self.gdb_session = None; self._reset_gui_to_stopped_state(); self._check_critical_configs_and_update_gui() def _set_gdb_breakpoint_action(self): # Unchanged if not self.gdb_session or not self.gdb_session.is_alive(): messagebox.showerror("Error", "GDB not active.", parent=self); return bp_loc = self.breakpoint_var.get(); if not bp_loc: messagebox.showerror("Input Error", "BP location empty.", parent=self); return self._update_status_bar(f"Setting BP at '{bp_loc}'...") try: cmd_to = self.app_settings.get_setting("timeouts", "gdb_command"); output = self.gdb_session.set_breakpoint(bp_loc, timeout=cmd_to); self._update_gdb_raw_output(output, True) bp_disp = bp_loc[:20] + "..." if len(bp_loc) > 20 else bp_loc if "Breakpoint" in output and "not defined" not in output.lower() and "pending" not in output.lower(): self.set_bp_button.config(text=f"BP: {bp_disp} (Set)"); self.run_button.config(state=tk.NORMAL); self._update_status_bar(f"BP set at '{bp_loc}'.") elif "pending" in output.lower(): self.set_bp_button.config(text=f"BP: {bp_disp} (Pend)"); self.run_button.config(state=tk.NORMAL); self._update_status_bar(f"BP '{bp_loc}' pending."); messagebox.showinfo("BP Pending", f"BP '{bp_loc}' pending.", parent=self) else: self._update_status_bar(f"Issue setting BP '{bp_loc}'.", True) except (ConnectionError, TimeoutError) as e: self._handle_gdb_operation_error(f"set BP '{bp_loc}'", e) except Exception as e: self._handle_gdb_operation_error(f"set BP '{bp_loc}' (unexpected)", e) def _run_or_continue_gdb_action(self): # Unchanged if not self.gdb_session or not self.gdb_session.is_alive(): messagebox.showerror("Error", "GDB not active.", parent=self); return self._update_parsed_json_output(None); self._disable_save_buttons() try: output = ""; run_to = self.app_settings.get_setting("timeouts", "program_run_continue"); dumper_ok = self.gdb_session.gdb_script_sourced_successfully if not self.program_started_once: params = self.params_var.get(); self._update_status_bar(f"Running with params: '{params}'..."); self._update_gdb_raw_output(f"run {params}\n",True); output = self.gdb_session.run_program(params, timeout=run_to) else: self._update_status_bar("Continuing..."); self._update_gdb_raw_output("continue\n",True); output = self.gdb_session.continue_execution(timeout=run_to) self._update_gdb_raw_output(output, True); dump_btn_state = tk.NORMAL if dumper_ok else tk.DISABLED if "Breakpoint" in output or re.search(r"Hit Breakpoint \d+", output, re.IGNORECASE): self._update_status_bar("BP hit. Ready to dump."); self.dump_var_button.config(state=dump_btn_state); self.program_started_once = True; self.run_button.config(text="3. Continue") elif "Program exited normally" in output or "exited with code" in output: self._update_status_bar("Program exited."); self.dump_var_button.config(state=tk.DISABLED); self.run_button.config(text="3. Run (Restart)"); self.program_started_once = False elif "received signal" in output.lower() or "segmentation fault" in output.lower(): self._update_status_bar("Program signal/crash.", True); self.dump_var_button.config(state=dump_btn_state); self.program_started_once = True; self.run_button.config(text="3. Continue (Risky)") else: self._update_status_bar("Program running/unknown."); self.dump_var_button.config(state=dump_btn_state); self.program_started_once = True; self.run_button.config(text="3. Continue") except (ConnectionError, TimeoutError) as e: self._handle_gdb_operation_error("run/continue", e) except Exception as e: self._handle_gdb_operation_error("run/continue (unexpected)", e) def _dump_gdb_variable_action(self): # MODIFIED if not self.gdb_session or not self.gdb_session.is_alive(): messagebox.showerror("Error", "GDB session not active.", parent=self); return if not self.gdb_session.gdb_script_sourced_successfully: messagebox.showwarning("Dumper Script Error", "GDB dumper script not loaded. JSON dump unavailable.", parent=self); self._check_critical_configs_and_update_gui(); return var_expr = self.variable_var.get() if not var_expr: messagebox.showerror("Input Error", "Variable/Expression to dump empty.", parent=self); return if not self.manual_dumps_output_path: messagebox.showerror("Directory Error", "Manual dumps output directory not set.", parent=self); return self._update_status_bar(f"Dumping '{var_expr}' to file..."); self._update_gdb_raw_output(f"Attempting file dump of: {var_expr}\n", True) self.last_manual_gdb_dump_filepath = None; self._disable_save_buttons() timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] app_name_manual = sanitize_filename_component(os.path.basename(self.exe_path_var.get() if self.exe_path_var.get() else "manual_app")) breakpoint_manual = sanitize_filename_component(self.breakpoint_var.get() if self.breakpoint_var.get() else "manual_bp") variable_manual = sanitize_filename_component(var_expr) gdb_dump_ext_name_part = GDB_DUMP_EXTENSION.lstrip('.') manual_placeholders = { "{profile_name}": MANUAL_DUMP_PROFILE_NAME_PLACEHOLDER, "{app_name}": app_name_manual, "{breakpoint}": breakpoint_manual, "{variable}": variable_manual, "{timestamp}": timestamp_str, "{extension}": gdb_dump_ext_name_part } gdb_target_dump_filename = MANUAL_DUMP_FILENAME_PATTERN for ph, val in manual_placeholders.items(): gdb_target_dump_filename = gdb_target_dump_filename.replace(ph, val) if not gdb_target_dump_filename.endswith(GDB_DUMP_EXTENSION): base_n, _ = os.path.splitext(gdb_target_dump_filename); gdb_target_dump_filename = base_n + GDB_DUMP_EXTENSION gdb_target_dump_filepath = os.path.join(self.manual_dumps_output_path, gdb_target_dump_filename) try: dump_timeout = self.app_settings.get_setting("timeouts", "dump_variable") status_payload = self.gdb_session.dump_variable_to_json(var_expr, timeout=dump_timeout, target_output_filepath=gdb_target_dump_filepath, target_output_format="json") self._update_parsed_json_output(status_payload) if status_payload.get("status") == "success": filepath_written = status_payload.get("filepath_written") if filepath_written and os.path.exists(filepath_written): self.last_manual_gdb_dump_filepath = filepath_written self._update_status_bar(f"Dumped '{var_expr}' to temp: {os.path.basename(filepath_written)}.") self._enable_save_buttons_if_data() else: self._update_status_bar(f"Dumper success, but temp file for '{var_expr}' missing.", True) else: error_msg = status_payload.get("details", status_payload.get("message", "Unknown dumper error")); self._update_status_bar(f"Error dumping '{var_expr}': {error_msg}", True) except (ConnectionError, TimeoutError) as e: self._handle_gdb_operation_error(f"dump var '{var_expr}'", e) except Exception as e: self._handle_gdb_operation_error(f"dump var '{var_expr}' (unexpected)", e) def _reset_gui_to_stopped_state(self): # Unchanged 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 BP") if hasattr(self, "run_button"): self.run_button.config(state=tk.DISABLED, text="3. Run Program") if hasattr(self, "dump_var_button"): self.dump_var_button.config(state=tk.DISABLED) if hasattr(self, "stop_gdb_button"): self.stop_gdb_button.config(state=tk.DISABLED) if hasattr(self, "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() # type: ignore self.run_profile_button.config(state=tk.NORMAL if can_run_profile else tk.DISABLED); self.profile_selection_combo.config(state="readonly" if not is_prof_running else tk.DISABLED) # type: ignore if hasattr(self, "save_json_button"): self._disable_save_buttons() self.program_started_once = False; self.last_manual_gdb_dump_filepath = None if not is_prof_running and hasattr(self, "status_var") and self.status_var is not None: self._update_status_bar("GDB session stopped or not active.") def _stop_gdb_session_action(self): # Unchanged if self.gdb_session and self.gdb_session.is_alive(): self._update_status_bar("Stopping GDB session...") try: kill_to = self.app_settings.get_setting("timeouts", "kill_program"); quit_to = self.app_settings.get_setting("timeouts", "gdb_quit") if self.program_started_once: self._update_gdb_raw_output(f"Kill output:\n{self.gdb_session.kill_program(timeout=kill_to)}\n", True) self.gdb_session.quit(timeout=quit_to); self._update_gdb_raw_output("GDB quit sent.\n", 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): # MODIFIED can_save = bool(self.last_manual_gdb_dump_filepath and os.path.exists(self.last_manual_gdb_dump_filepath)) if hasattr(self, "save_json_button"): self.save_json_button.config(state=tk.NORMAL if can_save else tk.DISABLED) if hasattr(self, "save_csv_button"): self.save_csv_button.config(state=tk.NORMAL if can_save else tk.DISABLED) def _disable_save_buttons(self): # Unchanged if hasattr(self, "save_json_button"): self.save_json_button.config(state=tk.DISABLED) if hasattr(self, "save_csv_button"): self.save_csv_button.config(state=tk.DISABLED) def _save_dumped_data(self, final_format_type: str): # MODIFIED if not self.last_manual_gdb_dump_filepath or not os.path.exists(self.last_manual_gdb_dump_filepath): messagebox.showwarning("No Data", "No temp dump file to save from.", parent=self); return file_ext = f".{final_format_type.lower()}"; file_types = [(f"{final_format_type.upper()} files", f"*{file_ext}"), ("All files", "*.*")] var_sugg = self.variable_var.get().replace(" ", "_").replace("*","ptr").replace("->","_").replace(":","_") default_fname_base = f"{var_sugg}_manual_dump" if var_sugg else "gdb_manual_dump" try: # Suggest name based on temp file's timestamp if possible temp_basename = os.path.basename(self.last_manual_gdb_dump_filepath) match_ts = re.search(r"(\d{8}_\d{6}_\d{3})", temp_basename) if match_ts: default_fname_base = f"{var_sugg}_{match_ts.group(1)}" except Exception: pass final_save_filepath = filedialog.asksaveasfilename(defaultextension=file_ext, filetypes=file_types, title=f"Save Manual Dump as {final_format_type.upper()}", initialfile=f"{default_fname_base}{file_ext}", parent=self) if not final_save_filepath: return self._update_status_bar(f"Saving data as {final_format_type.upper()} to {os.path.basename(final_save_filepath)}...") try: with open(self.last_manual_gdb_dump_filepath, 'r', encoding='utf-8') as f_temp: data_from_gdb_dump = json.load(f_temp) if final_format_type == "json": save_to_json(data_from_gdb_dump, final_save_filepath) # Re-save for pretty print elif final_format_type == "csv": data_csv = data_from_gdb_dump if isinstance(data_csv, dict) and not isinstance(data_csv, list): data_csv = [data_csv] elif not isinstance(data_csv, list): data_csv = [{"value": data_csv}] elif isinstance(data_csv, list) and data_csv and not all(isinstance(i, dict) for i in data_csv): data_csv = [{"value": i} for i in data_csv] save_to_csv(data_csv, final_save_filepath) messagebox.showinfo("Save Successful", f"Data saved to:\n{final_save_filepath}", parent=self) self._update_status_bar(f"Data saved to {os.path.basename(final_save_filepath)}.") try: os.remove(self.last_manual_gdb_dump_filepath); logger.info(f"Deleted temp manual dump: {self.last_manual_gdb_dump_filepath}"); self.last_manual_gdb_dump_filepath = None; self._disable_save_buttons() except Exception as e_del: logger.error(f"Could not delete temp manual dump '{self.last_manual_gdb_dump_filepath}': {e_del}") except Exception as e: logger.error(f"Error saving manual dump: {e}", exc_info=True); messagebox.showerror("Save Error", f"Failed to save: {e}", parent=self); self._update_status_bar(f"Error saving.", True) def _gui_status_update(self, message: str) -> None: # Unchanged 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: # Unchanged self._update_gdb_raw_output(message, append=True) def _gui_json_data_update(self, data: Any) -> None: # Unchanged self._update_parsed_json_output(data) def _gui_add_execution_log_entry(self, entry: ExecutionLogEntry) -> None: # Unchanged if self.produced_files_tree and self.winfo_exists(): try: values = (entry.get("timestamp", ""), entry.get("breakpoint_spec", "N/A"), entry.get("variable", "N/A"), entry.get("file_produced", "N/A"), entry.get("status", "N/A"), entry.get("details", "")); item_id = self.produced_files_tree.insert("", tk.END, values=values); self.produced_files_tree.see(item_id) except Exception as e: logger.error(f"Failed add to tree: {e}. Entry: {entry}") def _clear_produced_files_tree(self) -> None: # Unchanged 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: # Unchanged selected_profile_name = self.profile_selection_combo.get() if not selected_profile_name: messagebox.showwarning("No Profile", "Select profile.", parent=self); return if self.profile_executor_instance and self.profile_executor_instance.is_running: messagebox.showwarning("Profile Running", "Profile running.", parent=self); return if self.gdb_session and self.gdb_session.is_alive(): messagebox.showerror("GDB Active", "Manual GDB active. Stop first.", parent=self); return profile_data = self.available_profiles_map.get(selected_profile_name) if not profile_data: messagebox.showerror("Error", f"No 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; 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) # type: ignore 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) # type: ignore 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("", 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): # Unchanged 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): # Unchanged 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_msg = "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"Profile output folder invalid: {self.last_run_output_path}") current_gui_status = self.profile_exec_status_var.get() if "STARTING PROFILE" in current_gui_status or "Requesting profile stop" in current_gui_status: 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(): final_status_message = f"Profile finished with issues: {exec_final_status}" elif exec_final_status not in ["Initialized", "Pending", "Processing Dumps"]: final_status_message = f"Profile run completed. State: {exec_final_status}" elif "Error:" in current_gui_status or "failed" in current_gui_status.lower(): final_status_message = f"Profile finished with issues: {current_gui_status}" else: final_status_message = f"Profile run completed. Last status: {current_gui_status}" self.profile_exec_status_var.set(final_status_message) 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(); self.profile_executor_instance = None; logger.info("Profile execution GUI updates completed.") def _stop_current_profile_action(self) -> None: # Unchanged if self.profile_executor_instance and self.profile_executor_instance.is_running: self.profile_exec_status_var.set("Requesting profile stop..."); self.profile_executor_instance.request_stop() if hasattr(self, 'stop_profile_button'): self.stop_profile_button.config(state=tk.DISABLED) else: self.profile_exec_status_var.set("No profile running to stop.") def _open_last_run_output_folder(self) -> None: self._open_folder_path(self.last_run_output_path, "Profile Output Folder") # Unchanged def _open_manual_dumps_folder(self) -> None: self._open_folder_path(self.manual_dumps_output_path, "Manual Dumps Folder") # Unchanged def _open_folder_path(self, folder_path: Optional[str], folder_desc: str) -> None: # Unchanged logger.info(f"Attempting to open {folder_desc}: '{folder_path}'") if not folder_path or not os.path.isdir(folder_path): messagebox.showwarning(f"No {folder_desc}", f"{folder_desc} path not set/exist.", parent=self) else: try: if sys.platform == "win32": os.startfile(folder_path) elif sys.platform == "darwin": subprocess.run(["open", folder_path], check=True) else: subprocess.run(["xdg-open", folder_path], check=True) except Exception as e: logger.error(f"Failed to open {folder_desc}: {e}", exc_info=True); messagebox.showerror("Error", f"Could not open {folder_desc.lower()}: {folder_path}\nError: {e}", parent=self) def _on_closing_window(self): # Unchanged 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 & exit?", default=messagebox.CANCEL, parent=self) if response is True: self._stop_current_profile_action(); active_profile_stop_requested = True elif response is None: logger.info("User cancelled exit."); return self.app_settings.set_setting("gui", "main_window_geometry", self.geometry()) self.app_settings.set_setting("general", "last_target_executable_path", self.exe_path_var.get()) self.app_settings.set_setting("general", "default_breakpoint", self.breakpoint_var.get()) self.app_settings.set_setting("general", "default_variable_to_dump", self.variable_var.get()) self.app_settings.set_setting("general", "default_program_parameters", self.params_var.get()) if not self.app_settings.save_settings() and self.winfo_exists(): messagebox.showwarning("Settings Error", "Could not save settings.", parent=self) should_destroy = True if self.gdb_session and self.gdb_session.is_alive(): if self.winfo_exists() and messagebox.askokcancel("Quit GDB Session", "Manual GDB active. Stop & exit?", parent=self): self._stop_gdb_session_action() elif self.winfo_exists(): should_destroy = False; logger.info("User cancelled exit (manual GDB active).") elif not self.winfo_exists(): self._stop_gdb_session_action() if should_destroy: logger.info("Proceeding with window destruction.") if self.gui_log_handler: logging.getLogger().removeHandler(self.gui_log_handler); self.gui_log_handler.close(); self.gui_log_handler = None if active_profile_stop_requested: logger.debug("Assuming profile executor thread terminates.") self.destroy(); logger.info("Tkinter window destroyed.") else: logger.info("Window destruction aborted.") class ScrolledTextLogHandler(logging.Handler): # Unchanged 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