SXXXXXXX_CppPythonDebug/cpp_python_debug/gui/main_window.py

615 lines
55 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
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>", _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", "<No data>")
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