1690 lines
73 KiB
Python
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": 100,
|
|
"details": 600,
|
|
}
|
|
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()
|