SXXXXXXX_CppPythonDebug/cpp_python_debug/gui/main_window.py

1569 lines
72 KiB
Python

# File: cpp_python_debug/gui/main_window.py
# Provides the Tkinter GUI for interacting with the GDB session and automated profiles.
import tkinter as tk
from tkinter import filedialog, messagebox, ttk, scrolledtext, Menu
import logging
import os
import json # For pretty-printing JSON in the GUI
import re
import threading # For running profile executor in a separate thread
import subprocess # For opening folder cross-platform
import sys # To check platform
from typing import (
Optional,
Dict,
Any,
Callable,
List,
Tuple,
) # Ensuring typing imports are present
# Relative imports for modules within the same package
from ..core.gdb_controller import GDBSession
from ..core.output_formatter import save_to_json, save_to_csv
from ..core.config_manager import AppSettings
from ..core.profile_executor import (
ProfileExecutor,
ExecutionLogEntry,
) # ExecutionLogEntry needs to be defined or imported if used as type hint
from .config_window import ConfigWindow
from .profile_manager_window import ProfileManagerWindow
logger = logging.getLogger(__name__)
# --- Import Version Info FOR THE WRAPPER ITSELF ---
try:
# Use absolute import based on package name
from cpp_python_debug import _version as wrapper_version
WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})"
WRAPPER_BUILD_INFO = f"Wrapper Built: {wrapper_version.BUILD_TIMESTAMP}"
except ImportError:
# This might happen if you run the wrapper directly from source
# without generating its _version.py first (if you use that approach for the wrapper itself)
WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)"
WRAPPER_BUILD_INFO = "Wrapper build time unknown"
# --- End Import Version Info ---
# --- Constants for Version Generation ---
DEFAULT_VERSION = "0.0.0+unknown"
DEFAULT_COMMIT = "Unknown"
DEFAULT_BRANCH = "Unknown"
# --- End Constants ---
class GDBGui(tk.Tk):
def __init__(self):
super().__init__()
self.app_settings = AppSettings()
self.gui_log_handler: Optional[ScrolledTextLogHandler] = None # Added type hint
self.title(
f"GDB Debug GUI - {WRAPPER_APP_VERSION_STRING} - Settings: {os.path.basename(self.app_settings.config_filepath)} "
)
self.geometry(
self.app_settings.get_setting("gui", "main_window_geometry", "850x780")
)
self.gdb_session: Optional[GDBSession] = None
self.last_dumped_data: Any = None
self.program_started_once: bool = False
self.gdb_exe_status_var = tk.StringVar(value="GDB: Checking...")
self.gdb_dumper_status_var = tk.StringVar(value="Dumper Script: Checking...")
self.exe_path_var = tk.StringVar(
value=self.app_settings.get_setting(
"general", "last_target_executable_path", ""
)
)
self.breakpoint_var = tk.StringVar(
value=self.app_settings.get_setting("general", "default_breakpoint", "main")
)
self.variable_var = tk.StringVar(
value=self.app_settings.get_setting(
"general", "default_variable_to_dump", ""
)
)
self.params_var = tk.StringVar(
value=self.app_settings.get_setting(
"general", "default_program_parameters", ""
)
)
self.profile_executor_instance: Optional[ProfileExecutor] = None
self.available_profiles_map: Dict[str, Dict[str, Any]] = {}
self.profile_exec_status_var = tk.StringVar(value="Select a profile to run.")
self.produced_files_tree: Optional[ttk.Treeview] = None
self.last_run_output_path: Optional[str] = None
self.profile_progressbar: Optional[ttk.Progressbar] = None
self.status_bar_widget: Optional[ttk.Label] = (
None # For status bar widget reference
)
self.status_var: Optional[tk.StringVar] = None # For status bar text variable
self._create_menus()
self._create_widgets() # This will now correctly call the existing _create_status_bar
self._setup_logging_redirect_to_gui()
self._check_critical_configs_and_update_gui()
self._load_and_populate_profiles_for_automation_tab()
self.protocol("WM_DELETE_WINDOW", self._on_closing_window)
def _create_menus(self):
# ... (implementation as before)
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):
# ... (implementation as before)
logger.debug("Opening configuration window.")
config_win = ConfigWindow(self, self.app_settings)
self.wait_window(config_win)
logger.debug("Configuration window closed.")
self._check_critical_configs_and_update_gui()
def _open_profile_manager_window(self):
# ... (implementation as before)
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):
# ... (implementation as before) ...
logger.info(
"Checking critical configurations (GDB executable and Dumper script)."
)
gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path")
dumper_script_path = self.app_settings.get_setting(
"general", "gdb_dumper_script_path"
)
gdb_ok = False
if gdb_exe_path and os.path.isfile(gdb_exe_path):
self.gdb_exe_status_var.set(f"GDB: {os.path.basename(gdb_exe_path)} (OK)")
gdb_ok = True
elif gdb_exe_path:
self.gdb_exe_status_var.set(f"GDB: '{gdb_exe_path}' (Not Found/Invalid!)")
else:
self.gdb_exe_status_var.set(
"GDB: Not Configured! Please set in Options > Configure."
)
if dumper_script_path and os.path.isfile(dumper_script_path):
self.gdb_dumper_status_var.set(
f"Dumper: {os.path.basename(dumper_script_path)} (OK)"
)
elif dumper_script_path:
self.gdb_dumper_status_var.set(
f"Dumper: '{dumper_script_path}' (Not Found/Invalid!)"
)
else:
self.gdb_dumper_status_var.set("Dumper: Not Configured (Optional).")
if hasattr(self, "start_gdb_button"): # Check if widget exists
if gdb_ok and not (
self.profile_executor_instance
and self.profile_executor_instance.is_running
):
self.start_gdb_button.config(state=tk.NORMAL)
else:
self.start_gdb_button.config(state=tk.DISABLED)
if not gdb_ok:
self._reset_gui_to_stopped_state()
self.title(
f"GDB Debug GUI - {WRAPPER_APP_VERSION_STRING} - Settings: {os.path.basename(self.app_settings.config_filepath)}"
)
def _create_widgets(self):
# ... (implementation as before, with adjusted row weights) ...
main_frame = ttk.Frame(self, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
main_frame.rowconfigure(0, weight=0)
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) # This call should now work
def _create_config_status_widgets(self, parent_frame: ttk.Frame):
# ... (implementation as before) ...
config_status_frame = ttk.LabelFrame(
parent_frame, text="Critical Configuration Status", padding=(10, 5, 10, 10)
)
config_status_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5, padx=0)
config_status_frame.columnconfigure(1, weight=1)
config_status_frame.columnconfigure(3, weight=1)
ttk.Label(config_status_frame, text="GDB:").grid(
row=0, column=0, sticky=tk.W, padx=(5, 0), pady=5
)
self.gdb_exe_status_label = ttk.Label(
config_status_frame,
textvariable=self.gdb_exe_status_var,
relief="sunken",
padding=(5, 2),
anchor=tk.W,
)
self.gdb_exe_status_label.grid(
row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 10), pady=5
)
ttk.Label(config_status_frame, text="Dumper:").grid(
row=0, column=2, sticky=tk.W, padx=(5, 0), pady=5
)
self.gdb_dumper_status_label = ttk.Label(
config_status_frame,
textvariable=self.gdb_dumper_status_var,
relief="sunken",
padding=(5, 2),
anchor=tk.W,
)
self.gdb_dumper_status_label.grid(
row=0, column=3, sticky=(tk.W, tk.E), padx=(0, 10), pady=5
)
ttk.Button(
config_status_frame, text="Configure...", command=self._open_config_window
).grid(row=0, column=4, padx=(5, 5), pady=5, sticky=tk.E)
def _create_mode_notebook_widgets(self, parent_frame: ttk.Frame):
# ... (implementation as before) ...
mode_notebook = ttk.Notebook(parent_frame)
mode_notebook.grid(row=1, column=0, columnspan=1, sticky="nsew", pady=5, padx=0)
manual_debug_frame = ttk.Frame(mode_notebook, padding="5")
mode_notebook.add(manual_debug_frame, text="Manual Debug")
self._populate_manual_debug_tab(manual_debug_frame)
self.automated_exec_frame = ttk.Frame(mode_notebook, padding="10") # type: ignore
mode_notebook.add(self.automated_exec_frame, text="Automated Profile Execution")
self._populate_automated_execution_tab(self.automated_exec_frame)
def _populate_manual_debug_tab(self, parent_tab_frame: ttk.Frame):
# ... (implementation as before) ...
parent_tab_frame.columnconfigure(0, weight=1)
manual_target_settings_frame = ttk.LabelFrame(
parent_tab_frame, text="Target & Debug Session Settings", padding="10"
)
manual_target_settings_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5)
manual_target_settings_frame.columnconfigure(1, weight=1)
row_idx = 0
ttk.Label(manual_target_settings_frame, text="Target Executable:").grid(
row=row_idx, column=0, sticky=tk.W, padx=5, pady=2
)
ttk.Entry(
manual_target_settings_frame, textvariable=self.exe_path_var, width=70
).grid(row=row_idx, column=1, sticky=(tk.W, tk.E), padx=5, pady=2)
ttk.Button(
manual_target_settings_frame,
text="Browse...",
command=self._browse_target_exe,
).grid(row=row_idx, column=2, padx=5, pady=2)
row_idx += 1
ttk.Label(manual_target_settings_frame, text="Program Parameters:").grid(
row=row_idx, column=0, sticky=tk.W, padx=5, pady=2
)
ttk.Entry(manual_target_settings_frame, textvariable=self.params_var).grid(
row=row_idx, column=1, columnspan=2, sticky=(tk.W, tk.E), padx=5, pady=2
)
row_idx += 1
ttk.Label(manual_target_settings_frame, text="Breakpoint Location:").grid(
row=row_idx, column=0, sticky=tk.W, padx=5, pady=2
)
ttk.Entry(manual_target_settings_frame, textvariable=self.breakpoint_var).grid(
row=row_idx, column=1, columnspan=2, sticky=(tk.W, tk.E), padx=5, pady=2
)
row_idx += 1
bp_help_text = "Examples: main, myfile.cpp:123, MyClass::myMethod"
ttk.Label(
manual_target_settings_frame,
text=bp_help_text,
foreground="gray",
font=("TkDefaultFont", 8),
).grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0, 5))
row_idx += 1
ttk.Label(manual_target_settings_frame, text="Variable/Expression:").grid(
row=row_idx, column=0, sticky=tk.W, padx=5, pady=2
)
ttk.Entry(manual_target_settings_frame, textvariable=self.variable_var).grid(
row=row_idx, column=1, columnspan=2, sticky=(tk.W, tk.E), padx=5, pady=2
)
manual_session_control_frame = ttk.LabelFrame(
parent_tab_frame, text="Session Control", padding="10"
)
manual_session_control_frame.grid(
row=1, column=0, sticky=(tk.W, tk.E), pady=(10, 5)
)
button_flow_frame = ttk.Frame(manual_session_control_frame)
button_flow_frame.pack(fill=tk.X, expand=True)
self.start_gdb_button = ttk.Button(
button_flow_frame,
text="1. Start GDB",
command=self._start_gdb_session_action,
state=tk.DISABLED,
)
self.start_gdb_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
self.set_bp_button = ttk.Button(
button_flow_frame,
text="2. Set BP",
command=self._set_gdb_breakpoint_action,
state=tk.DISABLED,
)
self.set_bp_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
self.run_button = ttk.Button(
button_flow_frame,
text="3. Run",
command=self._run_or_continue_gdb_action,
state=tk.DISABLED,
)
self.run_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
self.dump_var_button = ttk.Button(
button_flow_frame,
text="4. Dump Var",
command=self._dump_gdb_variable_action,
state=tk.DISABLED,
)
self.dump_var_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
self.stop_gdb_button = ttk.Button(
button_flow_frame,
text="Stop GDB",
command=self._stop_gdb_session_action,
state=tk.DISABLED,
)
self.stop_gdb_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
manual_save_data_frame = ttk.LabelFrame(
parent_tab_frame, text="Save Dumped Data", padding="10"
)
manual_save_data_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=5)
self.save_json_button = ttk.Button(
manual_save_data_frame,
text="Save as JSON",
command=lambda: self._save_dumped_data("json"),
state=tk.DISABLED,
)
self.save_json_button.pack(side=tk.LEFT, padx=5, pady=5)
self.save_csv_button = ttk.Button(
manual_save_data_frame,
text="Save as CSV",
command=lambda: self._save_dumped_data("csv"),
state=tk.DISABLED,
)
self.save_csv_button.pack(side=tk.LEFT, padx=5, pady=5)
def _populate_automated_execution_tab(self, parent_tab_frame: ttk.Frame) -> None:
# ... (implementation as in your last correct version with dynamic wraplength and progressbar in grid) ...
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:
if (
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"
)
self.profile_progressbar.grid(row=1, column=0, sticky="ew", padx=5, pady=(2, 5))
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")
self.produced_files_tree.heading("timestamp", text="Time", anchor=tk.W)
self.produced_files_tree.heading(
"breakpoint_spec", text="Breakpoint Spec", anchor=tk.W
)
self.produced_files_tree.heading("variable", text="Variable", anchor=tk.W)
self.produced_files_tree.heading("file", text="File Produced", anchor=tk.W)
self.produced_files_tree.heading("status", text="Status", anchor=tk.W)
self.produced_files_tree.heading("details", text="Details", anchor=tk.W)
self.produced_files_tree.column(
"timestamp", width=130, minwidth=120, stretch=False
)
self.produced_files_tree.column(
"breakpoint_spec", width=150, minwidth=100, stretch=True
)
self.produced_files_tree.column(
"variable", width=150, minwidth=100, stretch=True
)
self.produced_files_tree.column("file", width=180, minwidth=150, stretch=True)
self.produced_files_tree.column("status", width=80, minwidth=60, stretch=False)
self.produced_files_tree.column(
"details", width=180, minwidth=150, stretch=True
)
tree_scrollbar_y = ttk.Scrollbar(
produced_files_frame,
orient=tk.VERTICAL,
command=self.produced_files_tree.yview,
)
tree_scrollbar_y.grid(row=0, column=1, sticky="ns")
tree_scrollbar_x = ttk.Scrollbar(
produced_files_frame,
orient=tk.HORIZONTAL,
command=self.produced_files_tree.xview,
)
tree_scrollbar_x.grid(row=1, column=0, sticky="ew")
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 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):
# ... (implementation as before) ...
self.available_profiles_map.clear()
profiles_list = self.app_settings.get_profiles()
profile_display_names = []
for profile_item in profiles_list:
name = profile_item.get("profile_name")
if name:
self.available_profiles_map[name] = profile_item
profile_display_names.append(name)
sorted_names = sorted(profile_display_names)
self.profile_selection_combo["values"] = sorted_names
if sorted_names:
self.profile_selection_combo.set(sorted_names[0])
if not (
self.profile_executor_instance
and self.profile_executor_instance.is_running
):
self.run_profile_button.config(state=tk.NORMAL)
self.profile_exec_status_var.set(
f"Ready to run profile: {self.profile_selection_combo.get()}"
)
else:
self.profile_selection_combo.set("")
self.run_profile_button.config(state=tk.DISABLED)
self.profile_exec_status_var.set(
"No profiles. Create one via 'Profiles > Manage Profiles'."
)
def _create_output_log_widgets(self, parent_frame: ttk.Frame):
# ... (implementation as before, possibly adjust 'log_text_height') ...
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 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")
# --- DEFINIZIONE DI _create_status_bar ---
def _create_status_bar(self, parent_frame: ttk.Frame):
"""Creates the status bar."""
self.status_var = tk.StringVar(
value="Ready. Configure GDB via Options menu if needed."
)
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=(tk.W, tk.E),
pady=(5, 0),
ipady=2,
padx=0,
)
def _setup_logging_redirect_to_gui(self):
# ... (implementation as before) ...
if not hasattr(self, "app_log_text") or not self.app_log_text:
logger.error("app_log_text widget not available for GUI logging setup.")
return
self.gui_log_handler = ScrolledTextLogHandler(self.app_log_text)
formatter = logging.Formatter(
"%(asctime)s [%(levelname)-7s] %(name)s: %(message)s", datefmt="%H:%M:%S"
)
self.gui_log_handler.setFormatter(formatter)
self.gui_log_handler.setLevel(logging.INFO)
logging.getLogger().addHandler(self.gui_log_handler)
def _browse_file(
self,
title: str,
target_var: tk.StringVar,
filetypes: Optional[List[Tuple[str, Any]]] = None,
): # Added types
# ... (implementation as before) ...
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 if initial_dir else None
), # Pass None if not determined
parent=self, # Ensure dialog is on top of this window
)
if path:
target_var.set(path)
def _browse_target_exe(self):
# ... (implementation as before) ...
self._browse_file(
"Select Target Application Executable",
self.exe_path_var,
[("Executable files", ("*.exe", "*")), ("All files", "*.*")],
) # Adjusted for cross-platform
def _update_gdb_raw_output(self, text: str, append: bool = True):
# ... (implementation as before) ...
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):
# ... (implementation as before) ...
if (
not hasattr(self, "parsed_json_output_text")
or not self.parsed_json_output_text.winfo_exists()
):
return
self.parsed_json_output_text.config(state=tk.NORMAL)
self.parsed_json_output_text.delete("1.0", tk.END)
if data_to_display is None:
self.parsed_json_output_text.insert("1.0", "<No data to display>")
elif isinstance(data_to_display, (dict, list)):
try:
pretty_json = json.dumps(data_to_display, indent=2, ensure_ascii=False)
self.parsed_json_output_text.insert("1.0", pretty_json)
except Exception as e:
logger.error(f"Error pretty-printing JSON for GUI: {e}")
self.parsed_json_output_text.insert(
"1.0",
f"Error displaying JSON: {e}\nRaw data: {str(data_to_display)}",
)
else:
self.parsed_json_output_text.insert("1.0", str(data_to_display))
self.parsed_json_output_text.see("1.0")
self.parsed_json_output_text.config(state=tk.DISABLED)
def _update_status_bar(self, message: str, is_error: bool = False):
# ... (implementation as before) ...
if hasattr(self, "status_var") and self.status_var is not None:
self.status_var.set(message) # Check if status_var is None
def _handle_gdb_operation_error(self, operation_name: str, error_details: Any):
# ... (implementation as before) ...
error_message = (
f"Error during GDB operation '{operation_name}': {error_details}"
)
logger.error(error_message)
self._update_gdb_raw_output(f"ERROR: {error_message}\n", append=True)
self._update_status_bar(f"Error: {operation_name} failed.", is_error=True)
if self.winfo_exists():
messagebox.showerror("GDB Operation Error", error_message, parent=self)
def _start_gdb_session_action(self):
if self.profile_executor_instance and self.profile_executor_instance.is_running:
messagebox.showwarning("Profile Running", "An automated profile is running. Please stop it first.", parent=self)
return
gdb_exe = self.app_settings.get_setting("general", "gdb_executable_path")
target_exe = self.exe_path_var.get()
gdb_script = self.app_settings.get_setting("general", "gdb_dumper_script_path")
# Validazioni iniziali per gdb_exe, target_exe
if not gdb_exe or not os.path.isfile(gdb_exe):
messagebox.showerror("Configuration Error", "GDB executable path is not configured correctly. Please check Options > Configure.", parent=self)
self._check_critical_configs_and_update_gui()
return
if not target_exe:
messagebox.showerror("Input Error", "Target executable path is required.", parent=self)
return
if not os.path.exists(target_exe):
messagebox.showerror("File Not Found", f"Target executable not found: {target_exe}", parent=self)
return
# Verifica se lo script dumper è specificato e valido
dumper_script_invalid = False
if gdb_script:
if not os.path.isfile(gdb_script):
dumper_script_invalid = True
self.gdb_dumper_status_var.set(f"Dumper: '{self.app_settings.get_setting('general', 'gdb_dumper_script_path')}' (Not Found!)")
else: # Nessun dumper script configurato
self.gdb_dumper_status_var.set("Dumper: Not Configured (Optional).")
if self.gdb_session and self.gdb_session.is_alive():
messagebox.showwarning("Session Active", "A GDB session is already active. Please stop it first.", parent=self)
return
self._update_status_bar("Starting GDB session...")
self._update_gdb_raw_output("Attempting to start GDB session...\n", append=False)
self._update_parsed_json_output(None)
try:
startup_timeout = self.app_settings.get_setting("timeouts", "gdb_start", 30)
quit_timeout_on_no_symbols = self.app_settings.get_setting("timeouts", "gdb_quit", 10)
# MODIFICA: Ottieni tutte le dumper_options configurate, incluse le nuove opzioni diagnostiche
current_dumper_options = self.app_settings.get_category_settings("dumper_options", {})
self.gdb_session = GDBSession(
gdb_path=gdb_exe, executable_path=target_exe,
gdb_script_full_path=gdb_script,
dumper_options=current_dumper_options # MODIFICA: Passa il dizionario completo delle dumper_options
)
self.gdb_session.start(timeout=startup_timeout)
self._update_gdb_raw_output(f"GDB session started for '{os.path.basename(target_exe)}'.\n")
# Logging diagnostico (mantenuto)
logger.info(f"MAIN_WINDOW_CHECK: gdb_script path = '{gdb_script}'")
if self.gdb_session:
logger.info(f"MAIN_WINDOW_CHECK: self.gdb_session.gdb_script_path (internal GDBSession path) = '{self.gdb_session.gdb_script_path}'")
logger.info(f"MAIN_WINDOW_CHECK: self.gdb_session.gdb_script_sourced_successfully = {self.gdb_session.gdb_script_sourced_successfully}")
logger.info(f"MAIN_WINDOW_CHECK: self.gdb_session.symbols_found = {self.gdb_session.symbols_found}")
else:
logger.error("MAIN_WINDOW_CHECK: self.gdb_session is None at the point of checking dumper/symbols status!")
symbols_ok = self.gdb_session and self.gdb_session.symbols_found
dumper_loaded_successfully = self.gdb_session and self.gdb_session.gdb_script_sourced_successfully
if not symbols_ok:
self._update_gdb_raw_output("ERROR: No debugging symbols found in the executable. GDB session will be terminated.\n", append=True)
# Mostra il warning all'utente
if self.winfo_exists():
messagebox.showwarning("No Debug Symbols - Session Aborted",
f"GDB reported no debugging symbols found in:\n{os.path.basename(target_exe)}\n\n"
"The GDB session will be terminated as debugging capabilities are severely limited.",
parent=self)
self._update_status_bar("GDB session aborted: No debug symbols.", is_error=True)
# Termina la sessione GDB e resetta la GUI
if self.gdb_session and self.gdb_session.is_alive():
try:
self.gdb_session.quit(timeout=quit_timeout_on_no_symbols)
except Exception as e_quit:
logger.error(f"Exception during GDB quit (no symbols scenario): {e_quit}")
self.gdb_session = None
self._reset_gui_to_stopped_state()
self._check_critical_configs_and_update_gui() # Per aggiornare lo stato GDB/Dumper
return # Esci dalla funzione _start_gdb_session_action
# Se siamo qui, i simboli sono stati trovati (symbols_ok è True)
# Procedi con la logica del dumper script
if gdb_script:
if dumper_script_invalid:
self._update_gdb_raw_output(f"Warning: GDB dumper script path '{gdb_script}' is invalid and was not found.\n", append=True)
self._update_status_bar(f"GDB active. Dumper script path invalid.", is_error=True)
self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Path Invalid!)")
elif dumper_loaded_successfully:
self._update_gdb_raw_output(f"GDB dumper script '{os.path.basename(gdb_script)}' sourced successfully.\n", append=True)
self._update_status_bar(f"GDB active. Dumper '{os.path.basename(gdb_script)}' loaded.")
self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Loaded)")
else: # path valido, ma caricamento fallito
self._update_gdb_raw_output(f"Warning: GDB dumper script '{os.path.basename(gdb_script)}' specified but FAILED to load correctly.\n", append=True)
self._update_status_bar(f"GDB active. Dumper script load issue (check logs).", is_error=True)
if self.winfo_exists():
messagebox.showwarning("Dumper Script Issue",
f"The GDB dumper script '{os.path.basename(gdb_script)}' may have failed to load correctly.\n"
"JSON dumping might be affected. Check logs.",
parent=self)
self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Load Failed!)")
# Nessun dumper script specificato
else:
self._update_gdb_raw_output("No GDB dumper script specified. JSON dump via script unavailable.\n", append=True)
self._update_status_bar("GDB session active. No dumper script.")
self.gdb_dumper_status_var.set("Dumper: Not Configured (Optional).")
# Abilita i bottoni della GUI dato che i simboli sono OK
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)
# Disabilita i controlli del profilo se la sessione manuale è attiva
if hasattr(self, 'run_profile_button'): self.run_profile_button.config(state=tk.DISABLED)
if hasattr(self, 'profile_selection_combo'): self.profile_selection_combo.config(state=tk.DISABLED)
self.program_started_once = False
self.last_dumped_data = None
self._disable_save_buttons()
except (FileNotFoundError, ConnectionError, TimeoutError) as e_specific:
self._handle_gdb_operation_error("start session", e_specific)
self.gdb_session = None
self._reset_gui_to_stopped_state()
self._check_critical_configs_and_update_gui()
except Exception as e:
logger.critical(f"!!! MAIN_WINDOW CATCH-ALL: Unhandled exception type: {type(e).__name__}, message: '{e}'", exc_info=True)
self._handle_gdb_operation_error("start session (unexpected from main_window catch-all)", e)
self.gdb_session = None
self._reset_gui_to_stopped_state()
self._check_critical_configs_and_update_gui() # Resetta la GUI
def _set_gdb_breakpoint_action(self):
# ... (implementation as before)
if not self.gdb_session or not self.gdb_session.is_alive():
messagebox.showerror("Error", "GDB session is not active.", parent=self)
return
bp_location = self.breakpoint_var.get()
if not bp_location:
messagebox.showerror(
"Input Error", "Breakpoint location cannot be empty.", parent=self
)
return
self._update_status_bar(f"Setting breakpoint at '{bp_location}'...")
try:
command_timeout = self.app_settings.get_setting("timeouts", "gdb_command")
output = self.gdb_session.set_breakpoint(
bp_location, timeout=command_timeout
)
self._update_gdb_raw_output(output, append=True)
bp_name_display = (
bp_location[:20] + "..." if len(bp_location) > 20 else bp_location
)
if (
"Breakpoint" in output
and "not defined" not in output.lower()
and "pending" not in output.lower()
):
self.set_bp_button.config(text=f"BP: {bp_name_display} (Set)")
self.run_button.config(state=tk.NORMAL)
self._update_status_bar(f"Breakpoint set at '{bp_location}'.")
elif "pending" in output.lower():
self.set_bp_button.config(text=f"BP: {bp_name_display} (Pend)")
self.run_button.config(state=tk.NORMAL)
self._update_status_bar(f"BP '{bp_location}' pending.")
messagebox.showinfo(
"Breakpoint Pending",
f"Breakpoint at '{bp_location}' is pending.",
parent=self,
)
else:
self._update_status_bar(
f"Issue setting BP '{bp_location}'. Check GDB output.",
is_error=True,
)
except (ConnectionError, TimeoutError) as e:
self._handle_gdb_operation_error(f"set breakpoint '{bp_location}'", e)
except Exception as e:
self._handle_gdb_operation_error(
f"set breakpoint '{bp_location}' (unexpected)", e
)
def _run_or_continue_gdb_action(self):
# ... (implementation as before)
if not self.gdb_session or not self.gdb_session.is_alive():
messagebox.showerror("Error", "GDB session is not active.", parent=self)
return
self._update_parsed_json_output(None)
self._disable_save_buttons()
try:
output = ""
run_timeout = self.app_settings.get_setting(
"timeouts", "program_run_continue"
)
dumper_script_path = self.app_settings.get_setting(
"general", "gdb_dumper_script_path"
)
dumper_is_valid_and_loaded = (
dumper_script_path
and os.path.isfile(dumper_script_path)
and self.gdb_session
and self.gdb_session.gdb_script_sourced_successfully
)
if not self.program_started_once:
params_str = self.params_var.get()
self._update_status_bar(
f"Running program with params: '{params_str}'..."
)
self._update_gdb_raw_output(
f"Executing: run {params_str}\n", append=True
)
output = self.gdb_session.run_program(params_str, timeout=run_timeout)
else:
self._update_status_bar("Continuing program execution...")
self._update_gdb_raw_output("Executing: continue\n", append=True)
output = self.gdb_session.continue_execution(timeout=run_timeout)
self._update_gdb_raw_output(output, append=True)
dump_button_state = tk.DISABLED
if dumper_is_valid_and_loaded:
dump_button_state = tk.NORMAL
if "Breakpoint" in output or re.search(
r"Hit Breakpoint \d+", output, re.IGNORECASE
):
self._update_status_bar("Breakpoint hit. Ready to dump variables.")
self.dump_var_button.config(state=dump_button_state)
self.program_started_once = True
self.run_button.config(text="3. Continue")
elif "Program exited normally" in output or "exited with code" in output:
self._update_status_bar("Program exited.")
self.dump_var_button.config(state=tk.DISABLED)
self.run_button.config(text="3. Run Program (Restart)")
self.program_started_once = False
elif (
"received signal" in output.lower()
or "segmentation fault" in output.lower()
):
self._update_status_bar(
"Program signal/crash. Check GDB output.", is_error=True
)
self.dump_var_button.config(state=dump_button_state)
self.program_started_once = True
self.run_button.config(text="3. Continue (Risky)")
else:
self._update_status_bar("Program running/unknown state.")
self.dump_var_button.config(state=dump_button_state)
self.program_started_once = True
self.run_button.config(text="3. Continue")
except (ConnectionError, TimeoutError) as e:
self._handle_gdb_operation_error("run/continue", e)
except Exception as e:
self._handle_gdb_operation_error("run/continue (unexpected)", e)
def _dump_gdb_variable_action(self):
# ... (implementation as before)
if not self.gdb_session or not self.gdb_session.is_alive():
messagebox.showerror("Error", "GDB session is not active.", parent=self)
return
dumper_script_path = self.app_settings.get_setting(
"general", "gdb_dumper_script_path"
)
if (
not dumper_script_path
or not os.path.isfile(dumper_script_path)
or not self.gdb_session.gdb_script_sourced_successfully
):
messagebox.showwarning(
"Dumper Script Error",
"GDB dumper script is not available, not found, or failed to load.\nJSON dump cannot proceed. Check configuration and logs.",
parent=self,
)
self._check_critical_configs_and_update_gui()
return
var_expr = self.variable_var.get()
if not var_expr:
messagebox.showerror(
"Input Error",
"Variable/Expression to dump cannot be empty.",
parent=self,
)
return
self._update_status_bar(f"Dumping '{var_expr}' to JSON...")
self._update_gdb_raw_output(
f"Attempting JSON dump of: {var_expr}\n", append=True
)
try:
dump_timeout = self.app_settings.get_setting("timeouts", "dump_variable")
dumped_data = self.gdb_session.dump_variable_to_json(
var_expr, timeout=dump_timeout
)
self.last_dumped_data = dumped_data
self._update_parsed_json_output(dumped_data)
if isinstance(dumped_data, dict) and "_gdb_tool_error" in dumped_data:
error_msg = dumped_data.get("details", dumped_data["_gdb_tool_error"])
self._update_status_bar(
f"Error dumping '{var_expr}': {error_msg}", is_error=True
)
self._disable_save_buttons()
if "raw_gdb_output" in dumped_data:
self._update_gdb_raw_output(
f"--- Raw GDB output for failed dump of '{var_expr}' ---\n{dumped_data['raw_gdb_output']}\n--- End ---\n",
append=True,
)
elif dumped_data is not None:
self._update_status_bar(f"Successfully dumped '{var_expr}'.")
self._enable_save_buttons_if_data()
else:
self._update_status_bar(
f"Dump of '{var_expr}' returned no data.", is_error=True
)
self._disable_save_buttons()
except (ConnectionError, TimeoutError) as e:
self._handle_gdb_operation_error(f"dump variable '{var_expr}'", e)
self.last_dumped_data = None
self._disable_save_buttons()
self._update_parsed_json_output({"error": str(e)})
except Exception as e:
self._handle_gdb_operation_error(
f"dump variable '{var_expr}' (unexpected)", e
)
self.last_dumped_data = None
self._disable_save_buttons()
self._update_parsed_json_output({"error": str(e)})
def _reset_gui_to_stopped_state(self):
# ... (implementation as before)
gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path")
gdb_is_configured_correctly = gdb_exe_path and os.path.isfile(gdb_exe_path)
is_profile_running = (
self.profile_executor_instance and self.profile_executor_instance.is_running
)
if hasattr(self, "start_gdb_button"):
if gdb_is_configured_correctly and not is_profile_running:
self.start_gdb_button.config(state=tk.NORMAL)
else:
self.start_gdb_button.config(state=tk.DISABLED)
if hasattr(self, "set_bp_button"):
self.set_bp_button.config(state=tk.DISABLED, text="2. Set Breakpoint")
if hasattr(self, "run_button"):
self.run_button.config(state=tk.DISABLED, text="3. Run Program")
if hasattr(self, "dump_var_button"):
self.dump_var_button.config(state=tk.DISABLED)
if hasattr(self, "stop_gdb_button"):
self.stop_gdb_button.config(state=tk.DISABLED)
if hasattr(self, "run_profile_button") and hasattr(
self, "profile_selection_combo"
):
if gdb_is_configured_correctly and not is_profile_running:
if self.profile_selection_combo.get():
self.run_profile_button.config(state=tk.NORMAL)
else:
self.profile_selection_combo.config(state="readonly") # Keep it readable even if disabled
self.run_profile_button.config(state=tk.DISABLED)
else:
self.run_profile_button.config(state=tk.DISABLED)
if is_profile_running:
self.profile_selection_combo.config(state=tk.DISABLED)
else:
self.profile_selection_combo.config(state="readonly")
if hasattr(self, "save_json_button"):
self._disable_save_buttons()
self.program_started_once = False
self.last_dumped_data = None
if (
not is_profile_running
and hasattr(self, "status_var")
and self.status_var is not None
):
self._update_status_bar("GDB session stopped or not active.")
def _stop_gdb_session_action(self):
# ... (implementation as before)
if self.gdb_session and self.gdb_session.is_alive():
self._update_status_bar("Stopping GDB session...")
try:
kill_timeout = self.app_settings.get_setting("timeouts", "kill_program")
quit_timeout = self.app_settings.get_setting("timeouts", "gdb_quit")
if self.program_started_once:
kill_output = self.gdb_session.kill_program(timeout=kill_timeout)
self._update_gdb_raw_output(
f"Kill command output:\n{kill_output}\n", append=True
)
self.gdb_session.quit(timeout=quit_timeout)
self._update_gdb_raw_output(
"GDB session quit command sent.\n", append=True
)
except Exception as e:
self._handle_gdb_operation_error("stop session", e)
finally:
self.gdb_session = None
self._reset_gui_to_stopped_state()
self._load_and_populate_profiles_for_automation_tab()
else:
self._reset_gui_to_stopped_state()
self._load_and_populate_profiles_for_automation_tab()
def _enable_save_buttons_if_data(self):
# ... (implementation as before)
if self.last_dumped_data and not (
isinstance(self.last_dumped_data, dict)
and "_gdb_tool_error" in self.last_dumped_data
):
if hasattr(self, "save_json_button"):
self.save_json_button.config(state=tk.NORMAL)
if hasattr(self, "save_csv_button"):
self.save_csv_button.config(state=tk.NORMAL)
else:
self._disable_save_buttons()
def _disable_save_buttons(self):
# ... (implementation as before)
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, format_type: str):
# ... (implementation as before)
if self.last_dumped_data is None or (
isinstance(self.last_dumped_data, dict)
and "_gdb_tool_error" in self.last_dumped_data
):
messagebox.showwarning(
"No Data", "No valid data has been dumped to save.", parent=self
)
return
file_ext = f".{format_type.lower()}"
file_types = [
(f"{format_type.upper()} files", f"*{file_ext}"),
("All files", "*.*"),
]
var_name_suggestion = (
self.variable_var.get()
.replace(" ", "_")
.replace("*", "ptr")
.replace("->", "_")
.replace(":", "_")
)
default_filename = (
f"{var_name_suggestion}_dump{file_ext}"
if var_name_suggestion
else f"gdb_dump{file_ext}"
)
filepath = filedialog.asksaveasfilename(
defaultextension=file_ext,
filetypes=file_types,
title=f"Save Dumped Data as {format_type.upper()}",
initialfile=default_filename,
parent=self,
)
if not filepath:
return
self._update_status_bar(
f"Saving data as {format_type.upper()} to {os.path.basename(filepath)}..."
)
try:
if format_type == "json":
save_to_json(self.last_dumped_data, filepath)
elif format_type == "csv":
data_for_csv = self.last_dumped_data
if isinstance(data_for_csv, dict) and not isinstance(
data_for_csv, list
):
data_for_csv = [data_for_csv]
elif not isinstance(data_for_csv, list):
data_for_csv = [{"value": data_for_csv}]
elif (
isinstance(data_for_csv, list)
and data_for_csv
and not all(isinstance(item, dict) for item in data_for_csv)
):
data_for_csv = [{"value": item} for item in data_for_csv]
save_to_csv(data_for_csv, filepath)
messagebox.showinfo(
"Save Successful", f"Data saved to:\n{filepath}", parent=self
)
self._update_status_bar(f"Data saved to {os.path.basename(filepath)}.")
except Exception as e:
logger.error(
f"Error saving data as {format_type} to {filepath}: {e}", exc_info=True
)
messagebox.showerror(
"Save Error", f"Failed to save data to {filepath}:\n{e}", parent=self
)
self._update_status_bar(
f"Error saving data as {format_type}.", is_error=True
)
def _gui_status_update(self, message: str) -> None:
# ... (implementation as before)
if (
hasattr(self, "profile_exec_status_var")
and self.profile_exec_status_var is not None
):
self.profile_exec_status_var.set(message)
logger.info(f"ProfileExec Status: {message}")
def _gui_gdb_output_update(self, message: str) -> None:
# ... (implementation as before)
self._update_gdb_raw_output(message, append=True)
def _gui_json_data_update(self, data: Any) -> None:
# ... (implementation as before)
self._update_parsed_json_output(data)
def _gui_add_execution_log_entry(self, entry: ExecutionLogEntry) -> None:
# ... (implementation as before, ensure "breakpoint_spec" key is used for the treeview)
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 to add entry to produced_files_tree: {e}. Entry: {entry}"
)
def _clear_produced_files_tree(self) -> None:
# ... (implementation as before)
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:
# ... (implementation as before)
selected_profile_name = self.profile_selection_combo.get()
if not selected_profile_name:
messagebox.showwarning(
"No Profile Selected", "Please select a profile to run.", parent=self
)
return
if self.profile_executor_instance and self.profile_executor_instance.is_running:
messagebox.showwarning(
"Profile Running",
"A profile is already running. Please stop it first.",
parent=self,
)
return
if self.gdb_session and self.gdb_session.is_alive():
messagebox.showerror(
"GDB Session Active",
"A manual GDB session is active. Please stop it first via 'Manual Debug' tab.",
parent=self,
)
return
profile_data = self.available_profiles_map.get(selected_profile_name)
if not profile_data:
messagebox.showerror(
"Error",
f"Could not find data for profile '{selected_profile_name}'.",
parent=self,
)
return
self.profile_exec_status_var.set(
f"AVVIO PROFILO '{selected_profile_name}' IN CORSO..."
)
if self.profile_progressbar and hasattr(
self.profile_exec_status_label_big, "master"
):
parent_frame_for_progress = self.profile_exec_status_label_big.master
self.profile_progressbar.grid(
row=1,
column=0,
columnspan=1,
sticky="ew",
padx=5,
pady=(2, 5),
in_=parent_frame_for_progress,
)
self.profile_progressbar.start(15)
if hasattr(self, "run_profile_button"):
self.run_profile_button.config(state=tk.DISABLED)
if hasattr(self, "stop_profile_button"):
self.stop_profile_button.config(state=tk.NORMAL)
if hasattr(self, "profile_selection_combo"):
self.profile_selection_combo.config(state=tk.DISABLED)
try:
if self.menubar.winfo_exists():
self.menubar.entryconfig("Profiles", state=tk.DISABLED)
self.menubar.entryconfig("Options", state=tk.DISABLED)
except tk.TclError:
logger.warning("TclError disabling menubar items during profile run.")
if hasattr(self, "start_gdb_button"):
self.start_gdb_button.config(state=tk.DISABLED)
if hasattr(self, "set_bp_button"):
self.set_bp_button.config(state=tk.DISABLED)
if hasattr(self, "run_button"):
self.run_button.config(state=tk.DISABLED)
if hasattr(self, "dump_var_button"):
self.dump_var_button.config(state=tk.DISABLED)
if hasattr(self, "stop_gdb_button"):
self.stop_gdb_button.config(state=tk.DISABLED)
self.last_run_output_path = None
if hasattr(self, "open_output_folder_button"):
self.open_output_folder_button.config(state=tk.DISABLED)
self.profile_executor_instance = ProfileExecutor(
profile_data,
self.app_settings,
status_update_callback=self._gui_status_update,
gdb_output_callback=self._gui_gdb_output_update,
json_output_callback=self._gui_json_data_update,
execution_log_callback=self._gui_add_execution_log_entry,
)
self._clear_produced_files_tree()
self._update_gdb_raw_output("", append=False)
self._update_parsed_json_output(None)
executor_thread = threading.Thread(
target=self._profile_executor_thread_target, daemon=True
)
executor_thread.start()
def _profile_executor_thread_target(self):
# ... (implementation as before)
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):
# ... (implementation as before)
if not self.winfo_exists():
logger.warning(
"_on_profile_execution_finished called but window no longer exists."
)
return
if self.profile_progressbar:
self.profile_progressbar.stop()
self.profile_progressbar.grid_remove()
final_status_message = "Profile execution finished."
if self.profile_executor_instance:
if hasattr(self.profile_executor_instance, "current_run_output_path"):
self.last_run_output_path = (
self.profile_executor_instance.current_run_output_path
)
if self.last_run_output_path and os.path.isdir(
self.last_run_output_path
):
if hasattr(self, "open_output_folder_button"):
self.open_output_folder_button.config(state=tk.NORMAL)
else:
if hasattr(self, "open_output_folder_button"):
self.open_output_folder_button.config(state=tk.DISABLED)
logger.warning(
f"Output folder path from executor not valid or not found: {self.last_run_output_path}"
)
else:
if hasattr(self, "open_output_folder_button"):
self.open_output_folder_button.config(state=tk.DISABLED)
current_gui_status = self.profile_exec_status_var.get()
if (
"AVVIO PROFILO" in current_gui_status
or "Requesting profile stop" in current_gui_status
):
if hasattr(self.profile_executor_instance, "profile_execution_summary"):
executor_final_status = (
self.profile_executor_instance.profile_execution_summary.get(
"status", "Unknown"
)
)
if (
"Error" in executor_final_status
or "failed" in executor_final_status.lower()
or "issues" in executor_final_status.lower()
):
final_status_message = (
f"Profile finished with issues: {executor_final_status}"
)
elif executor_final_status not in [
"Initialized",
"Pending",
"Processing Dumps",
]:
final_status_message = f"Profile run completed. Final state: {executor_final_status}"
else:
final_status_message = "Profile execution completed."
else:
final_status_message = "Profile execution completed."
elif (
"Error:" in current_gui_status
or "failed" in current_gui_status.lower()
or "issues" in current_gui_status.lower()
):
final_status_message = (
f"Profile finished with issues: {current_gui_status}"
)
else:
final_status_message = (
f"Profile run completed. Last status: {current_gui_status}"
)
self.profile_exec_status_var.set(final_status_message)
if (
hasattr(self, "profile_selection_combo")
and self.profile_selection_combo.get()
):
if hasattr(self, "run_profile_button"):
self.run_profile_button.config(state=tk.NORMAL)
else:
if hasattr(self, "run_profile_button"):
self.run_profile_button.config(state=tk.DISABLED)
if hasattr(self, "stop_profile_button"):
self.stop_profile_button.config(state=tk.DISABLED)
if hasattr(self, "profile_selection_combo"):
self.profile_selection_combo.config(state="readonly")
try:
if self.menubar.winfo_exists():
self.menubar.entryconfig("Profiles", state=tk.NORMAL)
self.menubar.entryconfig("Options", state=tk.NORMAL)
except tk.TclError as e:
logger.warning(f"TclError re-enabling menubar items: {e}")
self._check_critical_configs_and_update_gui()
self.profile_executor_instance = None
logger.info(
"Profile execution GUI updates completed, executor instance cleared."
)
def _stop_current_profile_action(self) -> None:
# ... (implementation as before)
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 currently running to stop.")
def _open_last_run_output_folder(self) -> None:
logger.info("Attempting to open last run output folder.")
logger.info(f"Current self.last_run_output_path: '{self.last_run_output_path}'")
if not self.last_run_output_path:
logger.warning("self.last_run_output_path is None or empty.")
messagebox.showwarning("No Output Folder", "The output folder path is not set.", parent=self)
if hasattr(self, 'open_output_folder_button'): self.open_output_folder_button.config(state=tk.DISABLED)
return
is_dir = os.path.isdir(self.last_run_output_path)
logger.info(f"Path '{self.last_run_output_path}' is_dir: {is_dir}")
if not is_dir:
logger.warning(f"Path '{self.last_run_output_path}' is not a valid directory.")
messagebox.showwarning("No Output Folder", "The output folder for the last run is not available or does not exist.", parent=self)
if hasattr(self, 'open_output_folder_button'): self.open_output_folder_button.config(state=tk.DISABLED)
return
try:
logger.info(f"Proceeding to open folder: {self.last_run_output_path} on platform: {sys.platform}")
if sys.platform == "win32":
logger.info(f"Executing: os.startfile('{self.last_run_output_path}')")
os.startfile(self.last_run_output_path)
logger.info("os.startfile executed.")
elif sys.platform == "darwin":
command = ["open", self.last_run_output_path]
logger.info(f"Executing: {command}")
subprocess.run(command, check=True)
logger.info(f"{command} executed.")
else: # Assume Linux/altri Unix-like
command = ["xdg-open", self.last_run_output_path]
logger.info(f"Executing: {command}")
subprocess.run(command, check=True)
logger.info(f"{command} executed.")
except FileNotFoundError: # Per xdg-open o open se non trovati
logger.error(f"File manager command ('xdg-open' or 'open') not found on this system.", exc_info=True)
messagebox.showerror("Error", f"Could not find the file manager command ('xdg-open' or 'open'). Please open the folder manually:\n{self.last_run_output_path}", parent=self)
except subprocess.CalledProcessError as cpe: # Nuovo blocco except
logger.error(f"Command to open folder failed with exit code {cpe.returncode}.", exc_info=True)
logger.error(f"Stderr from failed command: {cpe.stderr.decode(errors='replace') if cpe.stderr else 'N/A'}")
logger.error(f"Stdout from failed command: {cpe.stdout.decode(errors='replace') if cpe.stdout else 'N/A'}")
messagebox.showerror("Error Opening Folder", f"The command to open the folder failed (code {cpe.returncode}).\nPath: {self.last_run_output_path}\nError: {cpe.stderr.decode(errors='replace') if cpe.stderr else 'Unknown subprocess error'}", parent=self)
except Exception as e:
logger.error(f"Failed to open output folder '{self.last_run_output_path}': {e}", exc_info=True)
messagebox.showerror("Error Opening Folder", f"Could not open the output folder:\n{self.last_run_output_path}\n\nError: {e}", parent=self)
def _on_closing_window(self):
# ... (implementation as before)
logger.info("Window closing sequence initiated.")
active_profile_stop_requested = False
if self.profile_executor_instance and self.profile_executor_instance.is_running:
response = messagebox.askyesnocancel(
"Profile Running",
"An automated profile is currently running.\nDo you want to stop it and exit?",
default=messagebox.CANCEL,
parent=self,
)
if response is True:
self._stop_current_profile_action()
active_profile_stop_requested = True
logger.info(
"Requested stop for active profile. Proceeding with shutdown."
)
elif response is None:
logger.info("User cancelled exit while automated profile is running.")
return
self.app_settings.set_setting("gui", "main_window_geometry", self.geometry())
self.app_settings.set_setting(
"general", "last_target_executable_path", self.exe_path_var.get()
)
self.app_settings.set_setting(
"general", "default_breakpoint", self.breakpoint_var.get()
)
self.app_settings.set_setting(
"general", "default_variable_to_dump", self.variable_var.get()
)
self.app_settings.set_setting(
"general", "default_program_parameters", self.params_var.get()
)
save_success = self.app_settings.save_settings()
if not save_success:
if self.winfo_exists():
messagebox.showwarning(
"Settings Error",
"Could not save application settings. Check logs.",
parent=self,
)
should_destroy = True
if self.gdb_session and self.gdb_session.is_alive():
if self.winfo_exists() and messagebox.askokcancel(
"Quit GDB Session",
"A manual GDB session is active. Stop it and exit?",
parent=self,
):
logger.info("User chose to stop active manual GDB session and exit.")
self._stop_gdb_session_action()
elif self.winfo_exists():
logger.info("User cancelled exit while manual GDB session is active.")
should_destroy = False
elif not self.winfo_exists():
self._stop_gdb_session_action()
if should_destroy:
logger.info("Proceeding with window destruction.")
if self.gui_log_handler:
logging.getLogger().removeHandler(self.gui_log_handler)
self.gui_log_handler.close()
self.gui_log_handler = None
if active_profile_stop_requested:
logger.debug(
"Assuming profile executor thread will terminate due to stop request or daemon nature."
)
self.destroy()
logger.info("Tkinter window destroyed.")
else:
logger.info("Window destruction aborted by user.")
class ScrolledTextLogHandler(logging.Handler):
# ... (implementation as before)
def __init__(self, text_widget: scrolledtext.ScrolledText):
super().__init__()
self.text_widget = text_widget
self._active = True
def emit(self, record):
if (
not self._active
or not hasattr(self.text_widget, "winfo_exists")
or not self.text_widget.winfo_exists()
):
return
try:
log_entry = self.format(record)
self.text_widget.config(state=tk.NORMAL)
self.text_widget.insert(tk.END, log_entry + "\n")
self.text_widget.see(tk.END)
self.text_widget.config(state=tk.DISABLED)
except tk.TclError:
self._active = False
except Exception:
self._active = False # In caso di altri errori, disattiva per prevenire loop