SXXXXXXX_CppPythonDebug/cpp_python_debug/gui/main_window.py
2025-05-23 09:38:49 +02:00

1023 lines
59 KiB
Python

# File: cpp_python_debug/gui/main_window.py
# Provides the Tkinter GUI for interacting with the GDB session and automated profiles.
import tkinter as tk
from tkinter import filedialog, messagebox, ttk, scrolledtext, Menu
import logging
import os
import json # For pretty-printing JSON in the GUI
import re
import subprocess # For opening folder cross-platform
import sys # To check platform
import threading # For running profile executor in a separate thread
from typing import Optional, Dict, Any
# Relative imports for modules within the same package
from ..core.gdb_controller import GDBSession
from ..core.output_formatter import save_to_json, save_to_csv
from ..core.config_manager import AppSettings
from ..core.profile_executor import ProfileExecutor, ExecutionLogEntry
from .config_window import ConfigWindow
from .profile_manager_window import ProfileManagerWindow
logger = logging.getLogger(__name__)
class GDBGui(tk.Tk):
def __init__(self):
super().__init__()
self.app_settings = AppSettings()
self.gui_log_handler = None
self.title(f"GDB Debug GUI - Settings: {os.path.basename(self.app_settings.config_filepath)}")
self.geometry(self.app_settings.get_setting("gui", "main_window_geometry", "850x650"))
self.gdb_session: Optional[GDBSession] = None # For manual GDB session
self.last_dumped_data: Any = None # For manual dump
self.program_started_once: bool = False # For manual run/continue logic
self.gdb_exe_status_var = tk.StringVar(value="GDB: Checking...")
self.gdb_dumper_status_var = tk.StringVar(value="Dumper Script: Checking...")
# StringVars for manual debug input fields
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", "")
)
# For Automated Profile Execution Tab
self.profile_executor_instance: Optional[ProfileExecutor] = None
self.available_profiles_map: Dict[str, Dict[str, Any]] = {} # Maps display name to profile data
self.profile_exec_status_var = tk.StringVar(value="Select a profile to run.") # Status for auto execution
self.produced_files_tree: Optional[ttk.Treeview] = None
self.last_run_output_path: Optional[str] = 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)
# Dynamic list of profiles for quick run could be added here later
def _open_config_window(self):
logger.debug("Opening configuration window.")
config_win = ConfigWindow(self, self.app_settings)
self.wait_window(config_win)
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() # Refresh profiles list
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 gdb_ok:
# Only enable manual start if no profile is running
if not (self.profile_executor_instance and self.profile_executor_instance.is_running):
self.start_gdb_button.config(state=tk.NORMAL)
# Other manual buttons depend on GDB session state
else:
self._reset_gui_to_stopped_state() # This will disable all manual session buttons
self.start_gdb_button.config(state=tk.DISABLED)
self.title(f"GDB Debug GUI - Settings: {os.path.basename(self.app_settings.config_filepath)}")
def _create_widgets(self):
main_frame = ttk.Frame(self, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
main_frame.rowconfigure(0, weight=0) # Config Status
main_frame.rowconfigure(1, weight=1) # Mode Notebook
main_frame.rowconfigure(2, weight=3) # Output/Log Notebook
main_frame.rowconfigure(3, weight=0) # Status Bar
main_frame.columnconfigure(0, weight=1)
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")
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(1, weight=1) # Treeview expansion
parent_tab_frame.rowconfigure(2, weight=0) # Button row
# Frame for Profile Selection and Control (row 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)
# ... (contenuto di auto_control_frame come prima) ...
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")
ttk.Label(auto_control_frame, textvariable=self.profile_exec_status_var, relief=tk.SUNKEN, anchor=tk.W, padding=3).grid(
row=1, column=0, columnspan=4, sticky="ew", padx=5, pady=(10,5), ipady=2
)
# Frame and Treeview for Produced Files Log (row 1)
produced_files_frame = ttk.LabelFrame(parent_tab_frame, text="Produced Files Log", padding="10")
produced_files_frame.grid(row=1, 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", "variable", "file", "status", "details"),
show="headings",
selectmode="browse"
)
self.produced_files_tree.grid(row=0, column=0, sticky="nsew")
# ... (definizione headings e columns come prima) ...
self.produced_files_tree.heading("timestamp", text="Time", anchor=tk.W)
self.produced_files_tree.heading("breakpoint", text="Breakpoint", 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", 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=200, minwidth=150, stretch=True)
self.produced_files_tree.column("status", width=80, minwidth=60, stretch=False)
self.produced_files_tree.column("details", width=200, 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") # Sotto il treeview
self.produced_files_tree.configure(yscrollcommand=tree_scrollbar_y.set, xscrollcommand=tree_scrollbar_x.set)
# NEW: Button to open output folder (row 2)
folder_button_frame = ttk.Frame(parent_tab_frame) # No LabelFrame needed
folder_button_frame.grid(row=2, column=0, sticky="ew", 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
)
# Pack it to the right or center as desired
self.open_output_folder_button.pack(side=tk.RIGHT, padx=5, pady=5) # Allineato a destra nel suo frame
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 _gui_add_execution_log_entry(self, entry: ExecutionLogEntry) -> None:
"""Callback for ProfileExecutor to add an entry to the produced files treeview."""
if self.produced_files_tree and self.winfo_exists():
try:
# Ensure all keys exist in the entry, providing defaults if not
values = (
entry.get("timestamp", ""),
entry.get("breakpoint", "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) # Scroll to the new item
except Exception as e:
logger.error(f"Failed to add entry to produced_files_tree: {e}. Entry: {entry}")
def _load_and_populate_profiles_for_automation_tab(self) -> None:
self.available_profiles_map.clear()
profiles = self.app_settings.get_profiles()
profile_display_names = []
for profile_item in profiles: # Renamed to avoid conflict with menubar 'profiles_menu'
name = profile_item.get("profile_name")
if name:
self.available_profiles_map[name] = profile_item
profile_display_names.append(name)
sorted_names = sorted(profile_display_names)
self.profile_selection_combo['values'] = sorted_names
# Safely get the textvariable and set its value
combo_text_var = self.profile_selection_combo.cget("textvariable")
if isinstance(combo_text_var, str): # If it's a string name of a tk variable
# This case is less common if we assign a tk.StringVar() directly
# For safety, one might re-fetch or ensure it's a StringVar instance.
# For now, assuming direct StringVar assignment or it works.
pass
if sorted_names:
self.profile_selection_combo.set(sorted_names[0]) # Set current value using set()
self.run_profile_button.config(state=tk.NORMAL)
self.profile_exec_status_var.set(f"Ready to run profile: {self.profile_selection_combo.get()}")
else:
self.profile_selection_combo.set("")
self.run_profile_button.config(state=tk.DISABLED)
self.profile_exec_status_var.set("No profiles. Create one via 'Profiles > Manage Profiles'.")
def _create_output_log_widgets(self, parent_frame: ttk.Frame):
output_log_notebook = ttk.Notebook(parent_frame)
output_log_notebook.grid(row=2, column=0, columnspan=1, sticky='nsew', pady=(5,0), padx=0)
self.gdb_raw_output_text = scrolledtext.ScrolledText(output_log_notebook, wrap=tk.WORD, height=10, state=tk.DISABLED, font=("Consolas", 9))
output_log_notebook.add(self.gdb_raw_output_text, text="GDB Raw Output")
self.parsed_json_output_text = scrolledtext.ScrolledText(output_log_notebook, wrap=tk.WORD, height=10, state=tk.DISABLED, font=("Consolas", 9))
output_log_notebook.add(self.parsed_json_output_text, text="Parsed JSON Output")
self.app_log_text = scrolledtext.ScrolledText(output_log_notebook, wrap=tk.WORD, height=10, 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.")
status_bar = ttk.Label(parent_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
status_bar.grid(row=3, column=0, columnspan=1, sticky=(tk.W, tk.E), pady=(5,0), ipady=2, padx=0)
def _setup_logging_redirect_to_gui(self):
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=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,
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):
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", "<No data to display>")
elif isinstance(data_to_display, (dict, list)):
try:
pretty_json = json.dumps(data_to_display, indent=2, ensure_ascii=False)
self.parsed_json_output_text.insert("1.0", pretty_json)
except Exception as e:
logger.error(f"Error pretty-printing JSON for GUI: {e}")
self.parsed_json_output_text.insert("1.0", f"Error displaying JSON: {e}\nRaw data: {str(data_to_display)}")
else: self.parsed_json_output_text.insert("1.0", str(data_to_display))
self.parsed_json_output_text.see("1.0")
self.parsed_json_output_text.config(state=tk.DISABLED)
def _update_status_bar(self, message: str, is_error: bool = False):
if hasattr(self, 'status_var'): self.status_var.set(message)
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(): # Check if window still exists before showing messagebox
messagebox.showerror("GDB Operation Error", error_message, parent=self)
def _start_gdb_session_action(self):
if self.profile_executor_instance and self.profile_executor_instance.is_running:
messagebox.showwarning("Profile Running", "An automated profile is running. Please stop it first.", parent=self)
return
gdb_exe = self.app_settings.get_setting("general", "gdb_executable_path")
target_exe = self.exe_path_var.get()
gdb_script = self.app_settings.get_setting("general", "gdb_dumper_script_path")
if not gdb_exe or not os.path.isfile(gdb_exe):
messagebox.showerror("Configuration Error", "GDB executable path is not configured correctly. Please check Options > Configure.", parent=self)
self._check_critical_configs_and_update_gui()
return
if not target_exe:
messagebox.showerror("Input Error", "Target executable path is required.", parent=self)
return
if not os.path.exists(target_exe):
messagebox.showerror("File Not Found", f"Target executable not found: {target_exe}", parent=self)
return
if gdb_script and not os.path.isfile(gdb_script):
messagebox.showwarning("Configuration Warning",
f"GDB dumper script path is set to:\n'{gdb_script}'\nbut the file was not found or is invalid.\n\n"
"JSON dumping via script will be unavailable. You can correct this in Options > Configure.",
parent=self)
gdb_script = None
self.gdb_dumper_status_var.set(f"Dumper: '{self.app_settings.get_setting('general', 'gdb_dumper_script_path')}' (Not Found!)")
if self.gdb_session and self.gdb_session.is_alive():
messagebox.showwarning("Session Active", "A GDB session is already active. Please stop it first.", parent=self)
return
self._update_status_bar("Starting GDB session...")
self._update_gdb_raw_output("Attempting to start GDB session...\n", append=False)
self._update_parsed_json_output(None)
try:
startup_timeout = self.app_settings.get_setting("timeouts", "gdb_start", 30)
current_dumper_options = self.app_settings.get_category_settings("dumper_options", {})
self.gdb_session = GDBSession(
gdb_path=gdb_exe,
executable_path=target_exe,
gdb_script_full_path=gdb_script,
dumper_options=current_dumper_options
)
self.gdb_session.start(timeout=startup_timeout)
self._update_gdb_raw_output(f"GDB session started for '{os.path.basename(target_exe)}'.\n")
if gdb_script and self.gdb_session.gdb_script_sourced_successfully:
self._update_gdb_raw_output(f"GDB dumper script '{os.path.basename(gdb_script)}' sourced successfully.\n", append=True)
self._update_status_bar(f"GDB active. Dumper '{os.path.basename(gdb_script)}' loaded.")
self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Loaded)")
elif gdb_script:
self._update_gdb_raw_output(f"Warning: GDB dumper script '{os.path.basename(gdb_script)}' specified but failed to load.\n", append=True)
self._update_status_bar(f"GDB active. Dumper script issues (check logs).", is_error=True)
self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Load Failed!)")
if self.winfo_exists():
messagebox.showwarning("Dumper Script Issue",
f"The GDB dumper script '{os.path.basename(gdb_script)}' may have failed to load.\n"
"JSON dumping might be affected. Check logs.", parent=self)
else:
self._update_gdb_raw_output("No GDB dumper script. JSON dump via script unavailable.\n", append=True)
self._update_status_bar("GDB session active. No dumper script.")
if not self.app_settings.get_setting("general", "gdb_dumper_script_path"):
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)
self.run_profile_button.config(state=tk.DISABLED) # Disable profile run when manual session starts
self.profile_selection_combo.config(state=tk.DISABLED)
self.program_started_once = False
self.last_dumped_data = None
self._disable_save_buttons()
except (FileNotFoundError, ConnectionError, TimeoutError) as e_specific:
self._handle_gdb_operation_error("start session", e_specific)
self.gdb_session = None
self._reset_gui_to_stopped_state()
except Exception as e:
logger.critical(f"!!! MAIN_WINDOW CATCH-ALL: Unhandled exception type: {type(e).__name__}, message: '{e}'", exc_info=True)
self._handle_gdb_operation_error("start session (unexpected from main_window catch-all)", e)
self.gdb_session = None
self._reset_gui_to_stopped_state()
def _set_gdb_breakpoint_action(self):
if not self.gdb_session or not self.gdb_session.is_alive():
messagebox.showerror("Error", "GDB session is not active.", parent=self)
return
bp_location = self.breakpoint_var.get()
if not bp_location:
messagebox.showerror("Input Error", "Breakpoint location cannot be empty.", parent=self)
return
self._update_status_bar(f"Setting breakpoint at '{bp_location}'...")
try:
command_timeout = self.app_settings.get_setting("timeouts", "gdb_command")
output = self.gdb_session.set_breakpoint(bp_location, timeout=command_timeout)
self._update_gdb_raw_output(output, append=True)
bp_name_display = bp_location[:20] + "..." if len(bp_location) > 20 else bp_location
if "Breakpoint" in output and "not defined" not in output.lower() and "pending" not in output.lower():
self.set_bp_button.config(text=f"BP: {bp_name_display} (Set)")
self.run_button.config(state=tk.NORMAL)
self._update_status_bar(f"Breakpoint set at '{bp_location}'.")
elif "pending" in output.lower():
self.set_bp_button.config(text=f"BP: {bp_name_display} (Pend)")
self.run_button.config(state=tk.NORMAL)
self._update_status_bar(f"BP '{bp_location}' pending.")
messagebox.showinfo("Breakpoint Pending", f"Breakpoint at '{bp_location}' is pending.", parent=self)
else:
self._update_status_bar(f"Issue setting BP '{bp_location}'. Check GDB output.", is_error=True)
except (ConnectionError, TimeoutError) as e:
self._handle_gdb_operation_error(f"set breakpoint '{bp_location}'", e)
except Exception as e:
self._handle_gdb_operation_error(f"set breakpoint '{bp_location}' (unexpected)", e)
def _run_or_continue_gdb_action(self):
if not self.gdb_session or not self.gdb_session.is_alive():
messagebox.showerror("Error", "GDB session is not active.", parent=self)
return
self._update_parsed_json_output(None)
self._disable_save_buttons()
try:
output = ""
run_timeout = self.app_settings.get_setting("timeouts", "program_run_continue")
dumper_script_path = self.app_settings.get_setting("general", "gdb_dumper_script_path")
dumper_is_valid_and_loaded = dumper_script_path and os.path.isfile(dumper_script_path) and \
self.gdb_session and self.gdb_session.gdb_script_sourced_successfully
if not self.program_started_once:
params_str = self.params_var.get()
self._update_status_bar(f"Running program with params: '{params_str}'...")
self._update_gdb_raw_output(f"Executing: run {params_str}\n", append=True)
output = self.gdb_session.run_program(params_str, timeout=run_timeout)
else:
self._update_status_bar("Continuing program execution...")
self._update_gdb_raw_output("Executing: continue\n", append=True)
output = self.gdb_session.continue_execution(timeout=run_timeout)
self._update_gdb_raw_output(output, append=True)
dump_button_state = tk.DISABLED
if dumper_is_valid_and_loaded:
dump_button_state = tk.NORMAL
if "Breakpoint" in output or re.search(r"Hit Breakpoint \d+", output, re.IGNORECASE):
self._update_status_bar("Breakpoint hit. Ready to dump variables.")
self.dump_var_button.config(state=dump_button_state)
self.program_started_once = True
self.run_button.config(text="3. Continue")
elif "Program exited normally" in output or "exited with code" in output:
self._update_status_bar("Program exited.")
self.dump_var_button.config(state=tk.DISABLED)
self.run_button.config(text="3. Run Program (Restart)")
self.program_started_once = False
elif "received signal" in output.lower() or "segmentation fault" in output.lower():
self._update_status_bar("Program signal/crash. Check GDB output.", is_error=True)
self.dump_var_button.config(state=dump_button_state)
self.program_started_once = True
self.run_button.config(text="3. Continue (Risky)")
else:
self._update_status_bar("Program running/unknown state.")
self.dump_var_button.config(state=dump_button_state)
self.program_started_once = True
self.run_button.config(text="3. Continue")
except (ConnectionError, TimeoutError) as e:
self._handle_gdb_operation_error("run/continue", e)
except Exception as e:
self._handle_gdb_operation_error("run/continue (unexpected)", e)
def _dump_gdb_variable_action(self):
if not self.gdb_session or not self.gdb_session.is_alive():
messagebox.showerror("Error", "GDB session is not active.", parent=self)
return
dumper_script_path = self.app_settings.get_setting("general", "gdb_dumper_script_path")
if not dumper_script_path or not os.path.isfile(dumper_script_path) or \
not self.gdb_session.gdb_script_sourced_successfully:
messagebox.showwarning("Dumper Script Error",
"GDB dumper script is not available, not found, or failed to load.\n"
"JSON dump cannot proceed. Check configuration and logs.", parent=self)
self._check_critical_configs_and_update_gui()
return
var_expr = self.variable_var.get()
if not var_expr:
messagebox.showerror("Input Error", "Variable/Expression to dump cannot be empty.", parent=self)
return
self._update_status_bar(f"Dumping '{var_expr}' to JSON...")
self._update_gdb_raw_output(f"Attempting JSON dump of: {var_expr}\n", append=True)
try:
dump_timeout = self.app_settings.get_setting("timeouts", "dump_variable")
dumped_data = self.gdb_session.dump_variable_to_json(var_expr, timeout=dump_timeout)
self.last_dumped_data = dumped_data
self._update_parsed_json_output(dumped_data)
if isinstance(dumped_data, dict) and "_gdb_tool_error" in dumped_data:
error_msg = dumped_data.get("details", dumped_data["_gdb_tool_error"])
self._update_status_bar(f"Error dumping '{var_expr}': {error_msg}", is_error=True)
self._disable_save_buttons()
if "raw_gdb_output" in dumped_data:
self._update_gdb_raw_output(f"--- Raw GDB output for failed dump of '{var_expr}' ---\n{dumped_data['raw_gdb_output']}\n--- End ---\n", append=True)
elif dumped_data is not None:
self._update_status_bar(f"Successfully dumped '{var_expr}'.")
self._enable_save_buttons_if_data()
else:
self._update_status_bar(f"Dump of '{var_expr}' returned no data.", is_error=True)
self._disable_save_buttons()
except (ConnectionError, TimeoutError) as e:
self._handle_gdb_operation_error(f"dump variable '{var_expr}'", e)
self.last_dumped_data = None; self._disable_save_buttons(); self._update_parsed_json_output({"error": str(e)})
except Exception as e:
self._handle_gdb_operation_error(f"dump variable '{var_expr}' (unexpected)", e)
self.last_dumped_data = None; self._disable_save_buttons(); self._update_parsed_json_output({"error": str(e)})
def _reset_gui_to_stopped_state(self):
gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path")
gdb_is_configured_correctly = gdb_exe_path and os.path.isfile(gdb_exe_path)
if gdb_is_configured_correctly 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)
self.set_bp_button.config(state=tk.DISABLED, text="2. Set Breakpoint")
self.run_button.config(state=tk.DISABLED, text="3. Run Program")
self.dump_var_button.config(state=tk.DISABLED)
self.stop_gdb_button.config(state=tk.DISABLED)
# Enable profile controls if GDB is ok and no profile is running
if gdb_is_configured_correctly and not (self.profile_executor_instance and self.profile_executor_instance.is_running):
if self.profile_selection_combo.get(): # If a profile is selected in combobox
self.run_profile_button.config(state=tk.NORMAL)
else:
self.run_profile_button.config(state=tk.DISABLED)
self.profile_selection_combo.config(state="readonly")
else: # If GDB not ok, or profile is running
self.run_profile_button.config(state=tk.DISABLED)
if self.profile_executor_instance and self.profile_executor_instance.is_running:
self.profile_selection_combo.config(state=tk.DISABLED)
else:
self.profile_selection_combo.config(state="readonly")
self._disable_save_buttons()
self.program_started_once = False
self.last_dumped_data = None
if not (self.profile_executor_instance and self.profile_executor_instance.is_running):
self._update_status_bar("GDB session stopped or not active.")
# No need to call _check_critical_configs_and_update_gui() here as it caused recursion.
# It's called at specific points like startup or after config changes.
def _stop_gdb_session_action(self): # For manual session
if self.gdb_session and self.gdb_session.is_alive():
self._update_status_bar("Stopping GDB session...")
try:
kill_timeout = self.app_settings.get_setting("timeouts", "kill_program")
quit_timeout = self.app_settings.get_setting("timeouts", "gdb_quit")
if self.program_started_once: # Only kill if program was actually run
kill_output = self.gdb_session.kill_program(timeout=kill_timeout)
self._update_gdb_raw_output(f"Kill command output:\n{kill_output}\n", append=True)
self.gdb_session.quit(timeout=quit_timeout)
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 UI potentially
else:
# messagebox.showinfo("Info", "GDB session is not active or already stopped.", parent=self)
self._reset_gui_to_stopped_state()
self._load_and_populate_profiles_for_automation_tab()
def _enable_save_buttons_if_data(self):
if self.last_dumped_data and not (isinstance(self.last_dumped_data, dict) and "_gdb_tool_error" in self.last_dumped_data):
self.save_json_button.config(state=tk.NORMAL)
self.save_csv_button.config(state=tk.NORMAL)
else: self._disable_save_buttons()
def _disable_save_buttons(self):
self.save_json_button.config(state=tk.DISABLED)
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 has been dumped to save.", parent=self)
return
file_ext = f".{format_type.lower()}"; file_types = [(f"{format_type.upper()} files", f"*{file_ext}"), ("All files", "*.*")]
var_name_suggestion = self.variable_var.get().replace(" ", "_").replace("*", "ptr").replace("->", "_").replace(":", "_")
default_filename = f"{var_name_suggestion}_dump{file_ext}" if var_name_suggestion else f"gdb_dump{file_ext}"
filepath = filedialog.asksaveasfilename(defaultextension=file_ext, filetypes=file_types, title=f"Save Dumped Data as {format_type.upper()}", initialfile=default_filename, parent=self)
if not filepath: return
self._update_status_bar(f"Saving data as {format_type.upper()} to {os.path.basename(filepath)}...")
try:
if format_type == "json": save_to_json(self.last_dumped_data, filepath)
elif format_type == "csv":
data_for_csv = self.last_dumped_data
if isinstance(data_for_csv, dict) and not isinstance(data_for_csv, list): data_for_csv = [data_for_csv]
elif not isinstance(data_for_csv, list): data_for_csv = [{"value": data_for_csv}]
elif isinstance(data_for_csv, list) and data_for_csv and not all(isinstance(item, dict) for item in data_for_csv):
data_for_csv = [{"value": item} for item in data_for_csv]
save_to_csv(data_for_csv, filepath)
messagebox.showinfo("Save Successful", f"Data saved to:\n{filepath}", parent=self)
self._update_status_bar(f"Data saved to {os.path.basename(filepath)}.")
except Exception as e:
logger.error(f"Error saving data as {format_type} to {filepath}: {e}", exc_info=True)
messagebox.showerror("Save Error", f"Failed to save data to {filepath}:\n{e}", parent=self)
self._update_status_bar(f"Error saving data as {format_type}.", is_error=True)
def _gui_status_update(self, message: str) -> None:
if hasattr(self, 'profile_exec_status_var') and self.profile_exec_status_var.get() : # Check if widget exists
self.profile_exec_status_var.set(message)
logger.info(f"ProfileExec Status: {message}")
def _gui_gdb_output_update(self, message: str) -> None:
self._update_gdb_raw_output(message, append=True)
def _gui_json_data_update(self, data: Any) -> None:
self._update_parsed_json_output(data)
def _run_selected_profile_action(self) -> None:
self.last_run_output_path = None
self.open_output_folder_button.config(state=tk.DISABLED)
selected_profile_name = self.profile_selection_combo.get()
if not selected_profile_name:
messagebox.showwarning("No Profile Selected", "Please select a profile to run.", parent=self)
return
if self.profile_executor_instance and self.profile_executor_instance.is_running:
messagebox.showwarning("Profile Running", "A profile is already running. Please stop it first.", parent=self)
return
if self.gdb_session and self.gdb_session.is_alive(): # Manual session active
messagebox.showerror("GDB Session Active", "A manual GDB session is active. Please stop it first via 'Manual Debug' tab.", parent=self)
return
profile_data = self.available_profiles_map.get(selected_profile_name)
if not profile_data:
messagebox.showerror("Error", f"Could not find data for profile '{selected_profile_name}'.", parent=self)
return
self.start_gdb_button.config(state=tk.DISABLED)
self.set_bp_button.config(state=tk.DISABLED)
self.run_button.config(state=tk.DISABLED)
self.dump_var_button.config(state=tk.DISABLED)
self.stop_gdb_button.config(state=tk.DISABLED)
self.run_profile_button.config(state=tk.DISABLED)
self.stop_profile_button.config(state=tk.NORMAL)
self.profile_selection_combo.config(state=tk.DISABLED)
try: # Disable menu items
self.menubar.entryconfig("Profiles", state=tk.DISABLED)
self.menubar.entryconfig("Options", state=tk.DISABLED)
except tk.TclError: pass # In case menubar or entries are not yet fully available
self.profile_executor_instance = ProfileExecutor(
profile_data,
self.app_settings,
status_update_callback=self._gui_status_update,
gdb_output_callback=self._gui_gdb_output_update,
json_output_callback=self._gui_json_data_update,
execution_log_callback=self._gui_add_execution_log_entry
)
self.profile_exec_status_var.set(f"Starting profile '{selected_profile_name}' in background...")
# Clear previous outputs before starting a new profile run
self._update_gdb_raw_output("", append=False)
self._update_parsed_json_output(None)
executor_thread = threading.Thread(target=self._profile_executor_thread_target, daemon=True)
executor_thread.start()
def _profile_executor_thread_target(self):
if self.profile_executor_instance:
try:
self.profile_executor_instance.run()
finally:
if self.winfo_exists(): # Check if main window still exists
self.after(0, self._on_profile_execution_finished)
def _on_profile_execution_finished(self):
"""Called (in main thread) after profile executor thread finishes."""
if not self.winfo_exists(): # Check if main window still exists
logger.warning("_on_profile_execution_finished called but window no longer exists.")
return
final_status_message = "Profile execution finished." # Default final message
# Attempt to get the output path from the executor instance and enable button
# Also, try to get a more specific final status from the executor if available
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):
self.open_output_folder_button.config(state=tk.NORMAL)
logger.info(f"Output folder for last run: {self.last_run_output_path}")
else:
self.open_output_folder_button.config(state=tk.DISABLED)
logger.warning(f"Output folder path from executor not valid or not found: {self.last_run_output_path}")
else: # If executor doesn't have the attribute (should not happen with current ProfileExecutor)
self.open_output_folder_button.config(state=tk.DISABLED)
logger.warning("profile_executor_instance does not have 'current_run_output_path' attribute.")
# Use the status already set by the executor's status_updater callback if it's meaningful
current_gui_status = self.profile_exec_status_var.get()
if current_gui_status and not current_gui_status.startswith("Starting profile") and not current_gui_status.startswith("Requesting profile stop"):
# If the status var has something other than the initial "Starting..." or "Stopping..." messages
if "Error:" in current_gui_status or "failed" in current_gui_status.lower() or "issues" in current_gui_status.lower():
final_status_message = f"Profile finished with issues: {current_gui_status}"
else:
final_status_message = f"Profile run completed. Last status: {current_gui_status}"
# If current_gui_status is still "Starting..." or "Stopping...", the default "Profile execution finished." is fine
# or we could check profile_executor_instance.profile_execution_summary["status"]
elif hasattr(self.profile_executor_instance, 'profile_execution_summary'):
executor_final_status = self.profile_executor_instance.profile_execution_summary.get("status", "Unknown")
if "Error" in executor_final_status or "failed" in executor_final_status.lower():
final_status_message = f"Profile finished with issues (from summary): {executor_final_status}"
elif executor_final_status != "Initialized" and executor_final_status != "Pending":
final_status_message = f"Profile run completed. Final state: {executor_final_status}"
self.profile_exec_status_var.set(final_status_message)
# Re-enable UI components
# Enable run button only if a profile is actually selected in the combobox
if self.profile_selection_combo.get():
self.run_profile_button.config(state=tk.NORMAL)
else:
self.run_profile_button.config(state=tk.DISABLED)
self.stop_profile_button.config(state=tk.DISABLED)
self.profile_selection_combo.config(state="readonly") # Re-enable combobox selection
try: # Safely re-enable menu items
if self.menubar.winfo_exists(): # Check if menubar itself exists
self.menubar.entryconfig("Profiles", state=tk.NORMAL)
self.menubar.entryconfig("Options", state=tk.NORMAL)
except tk.TclError as e:
logger.warning(f"TclError re-enabling menubar items: {e}")
# This will re-evaluate the state of manual GDB buttons correctly
# and also ensure GDB path status is current.
self._check_critical_configs_and_update_gui()
# Clear the executor instance as it has finished its job
self.profile_executor_instance = None
logger.info("Profile execution GUI updates completed, executor instance cleared.")
def _open_last_run_output_folder(self) -> None:
"""Opens the output folder of the last successfully completed profile run."""
if not self.last_run_output_path or not os.path.isdir(self.last_run_output_path):
messagebox.showwarning("No Output Folder",
"The output folder for the last run is not available or does not exist.",
parent=self)
self.open_output_folder_button.config(state=tk.DISABLED) # Re-disable if path became invalid
return
try:
logger.info(f"Opening output folder: {self.last_run_output_path}")
if sys.platform == "win32":
os.startfile(self.last_run_output_path)
elif sys.platform == "darwin": # macOS
subprocess.run(["open", self.last_run_output_path], check=True)
else: # Linux and other UNIX-like
subprocess.run(["xdg-open", self.last_run_output_path], check=True)
except FileNotFoundError: # For xdg-open or open if not found
messagebox.showerror("Error",
f"Could not find the file manager command ('xdg-open' or 'open'). Please open the folder manually:\n{self.last_run_output_path}",
parent=self)
except Exception as e:
logger.error(f"Failed to open output folder '{self.last_run_output_path}': {e}")
messagebox.showerror("Error Opening Folder",
f"Could not open the output folder:\n{self.last_run_output_path}\n\nError: {e}",
parent=self)
def _stop_current_profile_action(self) -> None:
if self.profile_executor_instance and self.profile_executor_instance.is_running:
self.profile_exec_status_var.set("Requesting profile stop...")
self.profile_executor_instance.request_stop()
self.stop_profile_button.config(state=tk.DISABLED)
else:
self.profile_exec_status_var.set("No profile currently running to stop.")
def _on_closing_window(self):
logger.info("Window closing sequence initiated.")
active_profile_stop_requested = False
if self.profile_executor_instance and self.profile_executor_instance.is_running:
response = messagebox.askyesnocancel("Profile Running",
"An automated profile is currently running.\n"
"Do you want to stop it and exit?",
default=messagebox.CANCEL, parent=self)
if response is True:
self._stop_current_profile_action()
active_profile_stop_requested = True
# Give a small grace period for the thread to notice the stop request.
# This is not a perfect solution for immediate shutdown.
# A more robust solution would involve joining the thread with a timeout,
# or a more direct way to interrupt gdb_session.
# For now, we'll proceed after requesting stop.
logger.info("Requested stop for active profile. Proceeding with shutdown.")
elif response is None:
logger.info("User cancelled exit while automated profile is running.")
return
# If False (No), user wants to exit without stopping.
self.app_settings.set_setting("gui", "main_window_geometry", self.geometry())
self.app_settings.set_setting("general", "last_target_executable_path", self.exe_path_var.get())
self.app_settings.set_setting("general", "default_breakpoint", self.breakpoint_var.get())
self.app_settings.set_setting("general", "default_variable_to_dump", self.variable_var.get())
self.app_settings.set_setting("general", "default_program_parameters", self.params_var.get())
save_success = self.app_settings.save_settings()
if not save_success:
if self.winfo_exists(): messagebox.showwarning("Settings Error", "Could not save application settings. Check logs.", parent=self)
should_destroy = True
if self.gdb_session and self.gdb_session.is_alive():
if self.winfo_exists() and messagebox.askokcancel("Quit GDB Session", "A manual GDB session is active. Stop it and exit?", parent=self):
logger.info("User chose to stop active manual GDB session and exit.")
self._stop_gdb_session_action()
elif self.winfo_exists(): # User chose not to stop manual GDB
logger.info("User cancelled exit while manual GDB session is active.")
should_destroy = False
elif not self.winfo_exists(): # Window already gone
self._stop_gdb_session_action() # Try to stop anyway
if should_destroy:
logger.info("Proceeding with window destruction.")
if self.gui_log_handler:
logging.getLogger().removeHandler(self.gui_log_handler)
self.gui_log_handler.close()
self.gui_log_handler = None # Important to break reference
# Ensure the executor thread has a chance to finish if stop was requested
# This is still a bit tricky if the executor is deep in a blocking GDB call.
if active_profile_stop_requested:
# A join with timeout could be attempted on the thread here if we stored the thread.
# For now, rely on daemon=True and the request_stop flag.
logger.debug("Assuming profile executor thread will terminate due to stop request or daemon nature.")
self.destroy()
logger.info("Tkinter window destroyed.")
else:
logger.info("Window destruction aborted by user.")
class ScrolledTextLogHandler(logging.Handler):
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 as e: # Handle widget destroyed error
# print(f"ScrolledTextLogHandler TclError (widget likely destroyed): {e} - Record: {self.format(record)}")
self._active = False # Stop trying to use a dead widget
except Exception as e:
# print(f"ScrolledTextLogHandler unexpected error: {e} - Record: {self.format(record)}")
self._active = False
def close(self):
self._active = False
super().close()