SXXXXXXX_CppPythonDebug/cpp_python_debug/gui/main_window.py
2025-06-11 11:31:36 +02:00

1690 lines
73 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
from datetime import datetime
from typing import (
Optional,
Dict,
Any,
Callable,
List,
Tuple,
)
from cpp_python_debug.core.config_manager import AppSettings
from cpp_python_debug.core.gdb_controller import GDBSession
from cpp_python_debug.core.output_formatter import save_to_json, save_to_csv
from cpp_python_debug.core.profile_executor import ProfileExecutor, ExecutionLogEntry
from cpp_python_debug.core.file_utils import sanitize_filename_component
from cpp_python_debug.gui.config_window import ConfigWindow
from cpp_python_debug.gui.profile_manager_window import ProfileManagerWindow
logger = logging.getLogger(__name__)
APP_TITLE_NAME = "Cpp-Python GDB Debug Helper"
try:
from cpp_python_debug import _version as app_version_module
APP_VERSION_INFO_STRING = f"v{app_version_module.__version__}"
if (
hasattr(app_version_module, "GIT_COMMIT_HASH")
and app_version_module.GIT_COMMIT_HASH != "Unknown"
):
APP_VERSION_INFO_STRING += f" (commit {app_version_module.GIT_COMMIT_HASH[:7]})"
except ImportError:
logger.warning(
"cpp_python_debug._version.py not found, using default version display."
)
APP_VERSION_INFO_STRING = "(Development Version)"
MANUAL_DUMP_PROFILE_NAME_PLACEHOLDER = "ManualDump"
MANUAL_DUMP_FILENAME_PATTERN = (
"{profile_name}_{app_name}_{breakpoint}_{variable}_{timestamp}{extension}"
)
MANUAL_DUMPS_SUBFOLDER = (
"manual_gdb_dumps" # Sottocartella dentro la directory dei log dell'app
)
GDB_DUMP_EXTENSION = ".gdbdump.json"
class GDBGui(tk.Tk):
# --- MODIFICA: Aggiornato costruttore per accettare percorsi ---
def __init__(self, app_base_path: str, log_directory_path: str):
super().__init__()
self.app_base_path: str = app_base_path
self.log_directory_path: str = log_directory_path
logger.info(
f"GDBGui initialized with base_path: {self.app_base_path}, log_dir: {self.log_directory_path}"
)
# AppSettings ora potrebbe usare app_base_path se modificato per non usare più appdirs per il file principale
# Se AppSettings è già stato modificato per usare get_app_base_path(), non serve passarlo.
# Per ora, assumiamo che AppSettings sappia come trovare il suo file di config.
self.app_settings = AppSettings(
app_base_path_override=self.app_base_path
) # Passa la base se AppSettings la usa
# --- FINE MODIFICA ---
self.gui_log_handler: Optional[ScrolledTextLogHandler] = None
self.title(f"{APP_TITLE_NAME} - {APP_VERSION_INFO_STRING}")
logger.info(f"Settings file in use: {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...")
default_general_settings = self.app_settings._get_default_settings().get(
"general", {}
)
self.exe_path_var = tk.StringVar(
value=self.app_settings.get_setting(
"general",
"last_target_executable_path",
default_general_settings.get("last_target_executable_path", ""),
)
)
self.breakpoint_var = tk.StringVar(
value=self.app_settings.get_setting(
"general",
"default_breakpoint",
default_general_settings.get("default_breakpoint", "main"),
)
)
self.variable_var = tk.StringVar(
value=self.app_settings.get_setting(
"general",
"default_variable_to_dump",
default_general_settings.get("default_variable_to_dump", ""),
)
)
self.params_var = tk.StringVar(
value=self.app_settings.get_setting(
"general",
"default_program_parameters",
default_general_settings.get("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 = tk.StringVar(value="Ready.")
self._prepare_manual_dumps_directory()
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)
logger.info(
f"{APP_TITLE_NAME} GUI initialized. Version: {APP_VERSION_INFO_STRING}"
)
def _prepare_manual_dumps_directory(self):
# --- MODIFICA: Usa self.log_directory_path come base per MANUAL_DUMPS_SUBFOLDER ---
if not self.log_directory_path: # Dovrebbe essere sempre impostato da __init__
logger.error(
"Log directory path not set in GDBGui, cannot prepare manual dumps directory."
)
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)
return
self.manual_dumps_output_path = os.path.join(
self.log_directory_path, MANUAL_DUMPS_SUBFOLDER
)
# --- FINE MODIFICA ---
try:
os.makedirs(self.manual_dumps_output_path, exist_ok=True)
logger.info(
f"Manual dumps directory ensured/created: {self.manual_dumps_output_path}"
)
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)
def _create_menus(self): # (Invariato)
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): # (Invariato)
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): # (Invariato)
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): # (Invariato)
logger.info("Checking critical configurations and updating GUI status...")
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 for JSON dump)."
)
is_profile_currently_running = bool(
self.profile_executor_instance and self.profile_executor_instance.is_running
)
if hasattr(self, "start_gdb_button"):
can_start_gdb = gdb_ok and not is_profile_currently_running
self.start_gdb_button.config(
state=tk.NORMAL if can_start_gdb else tk.DISABLED
)
if not gdb_ok and not is_profile_currently_running:
self._reset_gui_to_stopped_state()
def _create_widgets(self): # (Invariato)
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): # (Invariato)
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): # (Invariato)
mode_notebook = ttk.Notebook(parent_frame)
mode_notebook.grid(row=1, column=0, columnspan=1, sticky="nsew", pady=5, padx=0)
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)
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)
def _populate_manual_debug_tab(self, parent_tab_frame: ttk.Frame): # (Invariato)
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
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, MyClass::foo",
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)
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)
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
and os.path.isdir(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: # (Invariato)
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): # (Invariato)
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")
self.available_profiles_map[name] = profile_item if name else {}
profile_display_names.append(name if name else "Unnamed Profile") # type: ignore
sorted_names = sorted(profile_display_names)
self.profile_selection_combo["values"] = sorted_names
is_profile_currently_running = bool(
self.profile_executor_instance and self.profile_executor_instance.is_running
)
gdb_is_ok = self.app_settings.get_setting(
"general", "gdb_executable_path"
) and os.path.isfile(
self.app_settings.get_setting("general", "gdb_executable_path")
)
if sorted_names:
self.profile_selection_combo.set(sorted_names[0])
self.run_profile_button.config(
state=(
tk.NORMAL
if not is_profile_currently_running and gdb_is_ok
else tk.DISABLED
)
)
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 defined. Create/manage via 'Profiles > Manage Profiles...'."
)
def _create_output_log_widgets(self, parent_frame: ttk.Frame): # (Invariato)
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): # (Invariato)
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): # (Invariato)
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 - %(name)-30s - [%(levelname)-7s] %(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)
logger.info("GUI logging handler initialized and added to root logger.")
def _browse_file(
self,
title: str,
target_var: tk.StringVar,
filetypes: Optional[List[Tuple[str, Any]]] = None,
): # (Invariato)
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)
if target_var == self.exe_path_var:
self.app_settings.set_setting(
"general", "last_target_executable_path", path
)
logger.info(f"Target executable path updated: {path}")
def _browse_target_exe(self):
self._browse_file(
"Select Target Executable",
self.exe_path_var,
[("Executable files", ("*.exe", "*")), ("All files", "*.*")],
) # (Invariato)
def _update_gdb_raw_output(self, text: str, append: bool = True): # (Invariato)
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): # (Invariato)
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):
if "status" in data_to_display and (
"filepath_written" in data_to_display
or "message" in data_to_display
or "variable_dumped" in data_to_display
):
status_text = f"Status: {data_to_display.get('status', 'N/A')}\nVar: {data_to_display.get('variable_dumped', 'N/A')}\nFile Written: {data_to_display.get('filepath_written', 'N/A')}\nFmt Req: {data_to_display.get('target_format_requested', 'N/A')}\nMsg: {data_to_display.get('message', 'N/A')}\n"
if data_to_display.get("details"):
status_text += f"Details: {data_to_display.get('details')}\n"
if data_to_display.get("raw_gdb_output"):
status_text += f"Raw GDB Output Snippet: {str(data_to_display.get('raw_gdb_output'))[:200]}...\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 dict: {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 JSON 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): # (Invariato)
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
): # (Invariato)
msg = f"Error GDB op '{op_name}': {err_details}"
logger.error(msg, exc_info=isinstance(err_details, Exception))
self._update_gdb_raw_output(f"GDB_OPERATION_ERROR: {msg}\n", True)
self._update_status_bar(f"Error: GDB op '{op_name}' failed.", True)
if self.winfo_exists():
messagebox.showerror("GDB Operation Error", msg, parent=self)
def _start_gdb_session_action(self):
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)
default_timeouts = self.app_settings._get_default_settings().get("timeouts", {})
startup_timeout = self.app_settings.get_setting(
"timeouts", "gdb_start", default_timeouts.get("gdb_start", 30)
)
quit_timeout_no_sym = self.app_settings.get_setting(
"timeouts", "gdb_quit", default_timeouts.get("gdb_quit", 10)
)
try:
self.gdb_session = GDBSession(
gdb_path=gdb_exe,
executable_path=target_exe,
gdb_script_full_path=gdb_script,
dumper_options=dumper_opts,
app_log_directory_path=self.log_directory_path,
)
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_timeout_no_sym)
self.gdb_session = None
self._reset_gui_to_stopped_state()
self._check_critical_configs_and_update_gui()
return
# --- BLOCCO MODIFICATO ---
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: # gdb_script_sourced_successfully è False
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
)
# Sposta il messagebox e l'aggiornamento dello status var QUI DENTRO L'ELSE
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!)"
)
# --- FINE BLOCCO MODIFICATO ---
elif gdb_script: # gdb_script specificato ma non è un file valido
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) if gdb_script else 'N/A'} (Path Invalid!)"
)
else: # Nessun gdb_script specificato
self._update_gdb_raw_output(
"No dumper script configured. JSON dump via 'dump_json' command unavailable.\n", True
)
self._update_status_bar("GDB active. No dumper script.")
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 GDB 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):
# (Come prima, usa i default corretti per i timeout)
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().strip()
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:
default_timeouts = self.app_settings._get_default_settings().get(
"timeouts", {}
)
cmd_timeout = self.app_settings.get_setting(
"timeouts", "gdb_command", default_timeouts.get("gdb_command", 30)
)
output = self.gdb_session.set_breakpoint(bp_loc, timeout=cmd_timeout)
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)
if self.winfo_exists():
messagebox.showwarning(
"BP Error",
f"Failed to set BP at '{bp_loc}'. Check GDB Output.",
parent=self,
)
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):
# (Come prima, usa i default corretti per i timeout)
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 = ""
default_timeouts = self.app_settings._get_default_settings().get(
"timeouts", {}
)
run_timeout = self.app_settings.get_setting(
"timeouts",
"program_run_continue",
default_timeouts.get("program_run_continue", 120),
)
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_timeout)
else:
self._update_status_bar("Continuing...")
self._update_gdb_raw_output("continue\n", True)
output = self.gdb_session.continue_execution(timeout=run_timeout)
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):
# (Come prima, usa i default corretti per i timeout e sanitize_filename_component importato)
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().strip()
if not var_expr:
messagebox.showerror(
"Input Error", "Variable/Expression to dump empty.", parent=self
)
return
if not self.manual_dumps_output_path or not os.path.isdir(
self.manual_dumps_output_path
):
messagebox.showerror(
"Directory Error", "Manual dumps output directory not set.", parent=self
)
self._prepare_manual_dumps_directory()
if not self.manual_dumps_output_path or not os.path.isdir(
self.manual_dumps_output_path
):
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_not_set"
)
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:
default_timeouts = self.app_settings._get_default_settings().get(
"timeouts", {}
)
dump_timeout = self.app_settings.get_setting(
"timeouts", "dump_variable", default_timeouts.get("dump_variable", 60)
)
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 _stop_gdb_session_action(self):
# (Come prima, usa i default corretti per i timeout)
if self.gdb_session and self.gdb_session.is_alive():
self._update_status_bar("Stopping GDB session...")
try:
default_timeouts = self.app_settings._get_default_settings().get(
"timeouts", {}
)
kill_timeout = self.app_settings.get_setting(
"timeouts", "kill_program", default_timeouts.get("kill_program", 20)
)
quit_timeout = self.app_settings.get_setting(
"timeouts", "gdb_quit", default_timeouts.get("gdb_quit", 10)
)
if self.program_started_once:
kill_output = self.gdb_session.kill_program(timeout=kill_timeout)
self._update_gdb_raw_output(f"Kill output:\n{kill_output}\n", True)
self.gdb_session.quit(timeout=quit_timeout)
self._update_gdb_raw_output("GDB quit sent.\n", True)
except Exception as e:
self._handle_gdb_operation_error("stop GDB 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 _reset_gui_to_stopped_state(self): # (Invariato)
gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path")
gdb_is_ok = bool(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, "save_json_button"):
self._disable_save_buttons()
if hasattr(self, "run_profile_button") and hasattr(
self, "profile_selection_combo"
):
can_run_profile = (
gdb_is_ok
and not is_prof_running
and bool(self.profile_selection_combo.get())
)
self.run_profile_button.config(
state=tk.NORMAL if can_run_profile else tk.DISABLED
)
self.profile_selection_combo.config(state="readonly" if not is_prof_running else tk.DISABLED) # type: ignore
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 _enable_save_buttons_if_data(self): # (Invariato)
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): # (Invariato)
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): # (Invariato)
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 = (
sanitize_filename_component(self.variable_var.get())
if self.variable_var.get()
else "manual_dump_var"
)
timestamp_from_temp_file = "data"
try:
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:
timestamp_from_temp_file = match_ts.group(1)
except Exception:
pass
default_fname_base = f"{var_sugg}_{timestamp_from_temp_file}"
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)
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: # (Invariato)
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"ProfileExecutor Status Update (via callback): {message}")
def _gui_gdb_output_update(self, message: str) -> None:
self._update_gdb_raw_output(message, append=True) # (Invariato)
def _gui_json_data_update(self, data: Any) -> None:
self._update_parsed_json_output(data) # (Invariato)
def _gui_add_execution_log_entry(
self, entry: ExecutionLogEntry
) -> None: # (Invariato)
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: # (Invariato)
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:
# (Come prima, ma con il passaggio di log_directory_path a ProfileExecutor)
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 Already Running",
"A profile is already in execution.",
parent=self,
)
return
if self.gdb_session and self.gdb_session.is_alive():
messagebox.showerror(
"GDB Session Active",
"A manual GDB session is active. Stop it first.",
parent=self,
)
return
profile_data_to_run = self.available_profiles_map.get(selected_profile_name)
if not profile_data_to_run:
messagebox.showerror(
"Profile Error",
f"Could not retrieve data for '{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_for_pb = self.profile_exec_status_label_big.master # type: ignore
self.profile_progressbar.grid(
row=1,
column=0,
columnspan=1,
sticky="ew",
padx=5,
pady=(2, 5),
in_=parent_frame_for_pb,
)
self.profile_progressbar.start(15)
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:
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.")
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.last_run_output_path = None
self.open_output_folder_button.config(state=tk.DISABLED) # type: ignore
# --- MODIFICA: Passa self.log_directory_path ---
self.profile_executor_instance = ProfileExecutor(
profile_data_to_run,
self.app_settings,
app_log_directory_path=self.log_directory_path,
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,
)
# --- FINE MODIFICA ---
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): # (Invariato)
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): # (Invariato)
if not self.winfo_exists():
logger.warning("Profile exec finished callback, but window gone.")
return
if self.profile_progressbar:
self.profile_progressbar.stop()
self.profile_progressbar.grid_remove()
final_status_message = (
"Profile execution finished (status unknown if executor missing)."
)
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
or "Status:" 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 status from executor"
)
)
if (
"Error" in exec_final_status
or "failed" in exec_final_status.lower()
or "Interrupted" in exec_final_status
):
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)
gdb_is_ok = self.app_settings.get_setting(
"general", "gdb_executable_path"
) and os.path.isfile(
self.app_settings.get_setting("general", "gdb_executable_path")
)
if hasattr(self, "profile_selection_combo") and self.profile_selection_combo.get(): # type: ignore
if hasattr(self, "run_profile_button") and gdb_is_ok:
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
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_menu_enable:
logger.warning(f"TclError re-enabling menubar: {e_menu_enable}")
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: # (Invariato)
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"
) # (Invariato)
def _open_manual_dumps_folder(self) -> None:
self._open_folder_path(
self.manual_dumps_output_path, "Manual Dumps Folder"
) # (Invariato)
def _open_folder_path(
self, folder_path: Optional[str], folder_desc: str
) -> None: # (Invariato)
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"Path for '{folder_desc}' not set/exist: {folder_path}",
parent=self,
)
else:
try:
if sys.platform == "win32":
os.startfile(os.path.normpath(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} at '{folder_path}': {e}",
exc_info=True,
)
messagebox.showerror(
"Error Opening Folder",
f"Could not open {folder_desc.lower()}: {folder_path}\nError: {e}",
parent=self,
)
def _on_closing_window(self): # (Invariato)
logger.info("Main 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(
"GDB Session Active", "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("Main window closing. Profile executor asked to stop.")
self.destroy()
logger.info("Main Tkinter window destroyed. App will exit.")
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: logging.LogRecord):
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
logger.debug("ScrolledTextLogHandler TclError, disabling.")
except Exception as e:
self._active = False
print(f"FATAL ScrolledTextLogHandler ERROR: {e}", file=sys.stderr)
if __name__ == "__main__":
if not logging.getLogger().hasHandlers():
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)-25s - %(levelname)-8s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger.info("Running GDBGui directly for testing (main_window.py).")
# Per testare GDBGui standalone, dobbiamo simulare i percorsi passati da __main__
test_app_base_path = os.path.dirname(
os.path.dirname(os.path.abspath(__file__))
) # Simula project_root
test_log_dir = os.path.join(test_app_base_path, "logs_test_gui")
try:
os.makedirs(test_log_dir, exist_ok=True)
except:
pass
app = GDBGui(app_base_path=test_app_base_path, log_directory_path=test_log_dir)
app.mainloop()