SXXXXXXX_CppPythonDebug/cpp_python_debug/gui/profile_manager_window.py
2025-05-23 15:34:47 +02:00

917 lines
48 KiB
Python

# File: cpp_python_debug/gui/profile_manager_window.py
import tkinter as tk
from tkinter import ttk, messagebox, filedialog, scrolledtext
import logging
import json
import os
import threading
import time
import hashlib
from typing import TYPE_CHECKING, List, Dict, Any, Optional, Callable
from .action_editor_window import ActionEditorWindow
from ..core.gdb_controller import GDBSession
if TYPE_CHECKING:
from ..core.config_manager import AppSettings
from .main_window import GDBGui
logger = logging.getLogger(__name__)
DEFAULT_ACTION = { # Riferimento per ActionEditorWindow se necessario
"breakpoint_location": "main",
"variables_to_dump": ["my_variable"],
"output_format": "json",
"output_directory": "./debug_dumps",
"filename_pattern": "{profile_name}_{breakpoint}_{variable}_{timestamp}.{format}",
"continue_after_dump": True
}
DEFAULT_PROFILE = {
"profile_name": "New Profile",
"target_executable": "",
"program_parameters": "",
"symbol_analysis": None, # Dati dell'analisi dei simboli
"actions": []
}
class SymbolAnalysisProgressDialog(tk.Toplevel):
"""
Dialog to show progress of symbol analysis.
(Will be moved to gui/dialogs.py in a later refactoring step)
"""
def __init__(self, parent: tk.Widget):
super().__init__(parent)
self.title("Symbol Analysis")
parent_x = parent.winfo_x()
parent_y = parent.winfo_y()
parent_width = parent.winfo_width()
parent_height = parent.winfo_height()
width = 600
height = 400
x = parent_x + (parent_width // 2) - (width // 2)
y = parent_y + (parent_height // 2) - (height // 2)
self.geometry(f'{width}x{height}+{x}+{y}')
self.transient(parent)
self.grab_set()
self.protocol("WM_DELETE_WINDOW", self._on_attempt_close)
self.analysis_can_be_closed = False
main_frame = ttk.Frame(self, padding="10")
main_frame.pack(expand=True, fill=tk.BOTH)
self.log_text_widget = scrolledtext.ScrolledText(main_frame, wrap=tk.WORD, height=15, state=tk.DISABLED, font=("Consolas", 9))
self.log_text_widget.pack(pady=5, padx=5, fill=tk.BOTH, expand=True)
self.status_label_var = tk.StringVar(value="Initializing symbol analysis...")
ttk.Label(main_frame, textvariable=self.status_label_var, justify=tk.LEFT).pack(pady=(5,2), fill=tk.X)
self.progressbar = ttk.Progressbar(main_frame, mode='indeterminate', length=300)
self.progressbar.pack(pady=(0,10), fill=tk.X, padx=5)
self.progressbar.start(15)
self.close_button = ttk.Button(main_frame, text="Close", command=self._on_close_button_click, state=tk.DISABLED)
self.close_button.pack(pady=5)
self.update_idletasks()
def _on_attempt_close(self):
if not self.analysis_can_be_closed:
messagebox.showwarning("Analysis in Progress",
"Symbol analysis is still in progress. Please wait.",
parent=self)
else:
self.destroy()
def _on_close_button_click(self):
self.destroy()
def log_message(self, message: str):
if not self.winfo_exists(): return
self.log_text_widget.config(state=tk.NORMAL)
self.log_text_widget.insert(tk.END, message + "\n")
self.log_text_widget.see(tk.END)
self.log_text_widget.config(state=tk.DISABLED)
try:
self.update_idletasks()
except tk.TclError:
pass
def set_status(self, status_message: str):
if not self.winfo_exists(): return
self.status_label_var.set(status_message)
try:
self.update_idletasks()
except tk.TclError:
pass
def analysis_complete_or_failed(self, success: bool):
if not self.winfo_exists(): return
self.progressbar.stop()
self.progressbar.config(mode='determinate')
self.progressbar['value'] = 100 if success else 0
self.analysis_can_be_closed = True
self.close_button.config(state=tk.NORMAL)
if success:
self.set_status("Analysis complete. You can close this window.")
else:
self.set_status("Analysis failed or was aborted. Check log. You can close this window.")
class ProfileManagerWindow(tk.Toplevel):
def __init__(self, parent: 'GDBGui', app_settings: 'AppSettings'):
super().__init__(parent)
self.parent_window = parent
self.app_settings: 'AppSettings' = app_settings
self.title("Profile Manager")
self.geometry("1050x700")
self.transient(parent)
self.grab_set()
self._profiles_data: List[Dict[str, Any]] = []
self._selected_profile_index: Optional[int] = None
self._selected_action_index_in_profile: Optional[int] = None
self._current_profile_modified_in_form: bool = False
self._profiles_list_changed_overall: bool = False
self.profile_name_var = tk.StringVar()
self.target_exe_var = tk.StringVar()
self.program_params_var = tk.StringVar()
self._current_profile_target_exe_details_label_var = tk.StringVar(value="Target: N/A")
self._current_profile_analysis_status_label_var = tk.StringVar(value="Symbol Analysis: Not Performed")
self.progress_dialog: Optional[SymbolAnalysisProgressDialog] = None
self._load_profiles_from_settings()
self._create_widgets()
self._populate_profiles_listbox()
if self._profiles_data:
self._select_profile_by_index(0)
else:
self._update_analysis_status_display()
self.protocol("WM_DELETE_WINDOW", self._on_closing_button)
self.profile_name_var.trace_add("write", self._mark_form_as_modified)
self.target_exe_var.trace_add("write", lambda *args: self._on_target_exe_changed_in_form())
self.program_params_var.trace_add("write", self._mark_form_as_modified)
def _mark_form_as_modified(self, *args):
self._current_profile_modified_in_form = True
def _on_target_exe_changed_in_form(self, *args):
self._mark_form_as_modified(*args)
self._update_analysis_status_display()
def _load_profiles_from_settings(self) -> None:
self._profiles_data = []
loaded_profiles = self.app_settings.get_profiles()
for profile_dict in loaded_profiles:
copied_profile = json.loads(json.dumps(profile_dict)) # Deep copy
if "actions" not in copied_profile or not isinstance(copied_profile["actions"], list):
copied_profile["actions"] = []
if "symbol_analysis" not in copied_profile:
copied_profile["symbol_analysis"] = None
self._profiles_data.append(copied_profile)
self._profiles_list_changed_overall = False
logger.debug(f"Loaded {len(self._profiles_data)} profiles into ProfileManagerWindow.")
def _create_widgets(self) -> None:
main_frame = ttk.Frame(self, padding="10")
main_frame.pack(expand=True, fill=tk.BOTH)
main_frame.columnconfigure(0, weight=1, minsize=250)
main_frame.columnconfigure(1, weight=3)
main_frame.rowconfigure(0, weight=1)
main_frame.rowconfigure(1, weight=0)
left_pane = ttk.Frame(main_frame)
left_pane.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
left_pane.rowconfigure(0, weight=1)
left_pane.rowconfigure(1, weight=0)
left_pane.columnconfigure(0, weight=1)
profiles_list_frame = ttk.LabelFrame(left_pane, text="Profiles", padding="5")
profiles_list_frame.grid(row=0, column=0, sticky="nsew", pady=(0,5))
profiles_list_frame.rowconfigure(0, weight=1)
profiles_list_frame.columnconfigure(0, weight=1)
self.profiles_listbox = tk.Listbox(profiles_list_frame, exportselection=False, selectmode=tk.SINGLE, width=30)
self.profiles_listbox.grid(row=0, column=0, sticky="nsew")
self.profiles_listbox.bind("<<ListboxSelect>>", self._on_profile_select)
listbox_scrollbar_y = ttk.Scrollbar(profiles_list_frame, orient=tk.VERTICAL, command=self.profiles_listbox.yview)
listbox_scrollbar_y.grid(row=0, column=1, sticky="ns")
self.profiles_listbox.configure(yscrollcommand=listbox_scrollbar_y.set)
profiles_list_controls_frame = ttk.Frame(left_pane)
profiles_list_controls_frame.grid(row=1, column=0, sticky="ew", pady=5)
button_width = 10
ttk.Button(profiles_list_controls_frame, text="New", command=self._new_profile, width=button_width).pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True)
self.duplicate_button = ttk.Button(profiles_list_controls_frame, text="Duplicate", command=self._duplicate_profile, state=tk.DISABLED, width=button_width)
self.duplicate_button.pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True)
self.delete_button = ttk.Button(profiles_list_controls_frame, text="Delete", command=self._delete_profile, state=tk.DISABLED, width=button_width)
self.delete_button.pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True)
right_pane = ttk.Frame(main_frame)
right_pane.grid(row=0, column=1, sticky="nsew")
right_pane.rowconfigure(0, weight=0)
right_pane.rowconfigure(1, weight=0)
right_pane.rowconfigure(2, weight=1)
right_pane.columnconfigure(0, weight=1)
details_form_frame = ttk.LabelFrame(right_pane, text="Profile Details", padding="10")
details_form_frame.grid(row=0, column=0, sticky="new", pady=(0,5))
details_form_frame.columnconfigure(1, weight=1)
ttk.Label(details_form_frame, text="Profile Name:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=3)
self.profile_name_entry = ttk.Entry(details_form_frame, textvariable=self.profile_name_var, state=tk.DISABLED)
self.profile_name_entry.grid(row=0, column=1, columnspan=2, sticky="ew", padx=5, pady=3)
ttk.Label(details_form_frame, text="Target Executable:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=3)
self.target_exe_entry = ttk.Entry(details_form_frame, textvariable=self.target_exe_var, state=tk.DISABLED)
self.target_exe_entry.grid(row=1, column=1, sticky="ew", padx=5, pady=3)
self.browse_exe_button = ttk.Button(details_form_frame, text="Browse...", command=self._browse_target_executable, state=tk.DISABLED)
self.browse_exe_button.grid(row=1, column=2, padx=5, pady=3)
ttk.Label(details_form_frame, text="Program Parameters:").grid(row=2, column=0, sticky=tk.W, padx=5, pady=3)
self.program_params_entry = ttk.Entry(details_form_frame, textvariable=self.program_params_var, state=tk.DISABLED)
self.program_params_entry.grid(row=2, column=1, columnspan=2, sticky="ew", padx=5, pady=3)
analysis_control_frame = ttk.LabelFrame(right_pane, text="Symbol Analysis", padding="10")
analysis_control_frame.grid(row=1, column=0, sticky="new", pady=5)
analysis_control_frame.columnconfigure(0, weight=1)
self.target_exe_details_label = ttk.Label(analysis_control_frame, textvariable=self._current_profile_target_exe_details_label_var, wraplength=500, justify=tk.LEFT)
self.target_exe_details_label.grid(row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=(2,5))
self.analysis_status_label = ttk.Label(analysis_control_frame, textvariable=self._current_profile_analysis_status_label_var, foreground="blue", wraplength=350)
self.analysis_status_label.grid(row=1, column=0, sticky="w", padx=5, pady=2)
self.analyse_symbols_button = ttk.Button(analysis_control_frame, text="Analyse Target Symbols", command=self._trigger_symbol_analysis, state=tk.DISABLED)
self.analyse_symbols_button.grid(row=1, column=1, sticky="e", padx=5, pady=2)
actions_ui_frame = ttk.LabelFrame(right_pane, text="Debug Actions", padding="10")
actions_ui_frame.grid(row=2, column=0, sticky="nsew", pady=5)
actions_ui_frame.rowconfigure(0, weight=1)
actions_ui_frame.columnconfigure(0, weight=1)
actions_ui_frame.columnconfigure(1, weight=0)
actions_ui_frame.columnconfigure(2, weight=0)
self.actions_listbox = tk.Listbox(actions_ui_frame, exportselection=False, selectmode=tk.SINGLE, height=8)
self.actions_listbox.grid(row=0, column=0, sticky="nsew", pady=5, padx=(0,5))
self.actions_listbox.bind("<<ListboxSelect>>", self._on_action_select_in_listbox)
actions_listbox_scrolly = ttk.Scrollbar(actions_ui_frame, orient=tk.VERTICAL, command=self.actions_listbox.yview)
actions_listbox_scrolly.grid(row=0, column=1, sticky="ns", pady=5)
self.actions_listbox.configure(yscrollcommand=actions_listbox_scrolly.set)
action_buttons_frame = ttk.Frame(actions_ui_frame)
action_buttons_frame.grid(row=0, column=2, sticky="ns", padx=(5,0), pady=5)
action_btn_width = 8
self.add_action_button = ttk.Button(action_buttons_frame, text="Add...", command=self._add_action, state=tk.DISABLED, width=action_btn_width)
self.add_action_button.pack(fill=tk.X, pady=2)
self.edit_action_button = ttk.Button(action_buttons_frame, text="Edit...", command=self._edit_action, state=tk.DISABLED, width=action_btn_width)
self.edit_action_button.pack(fill=tk.X, pady=2)
self.remove_action_button = ttk.Button(action_buttons_frame, text="Remove", command=self._remove_action, state=tk.DISABLED, width=action_btn_width)
self.remove_action_button.pack(fill=tk.X, pady=2)
bottom_buttons_frame = ttk.Frame(main_frame)
bottom_buttons_frame.grid(row=1, column=0, columnspan=2, sticky="sew", pady=(10,0))
bottom_buttons_inner_frame = ttk.Frame(bottom_buttons_frame)
bottom_buttons_inner_frame.pack(side=tk.RIGHT)
self.save_button = ttk.Button(bottom_buttons_inner_frame, text="Save All Changes", command=self._save_all_profiles_to_settings)
self.save_button.pack(side=tk.LEFT, padx=5, pady=5)
ttk.Button(bottom_buttons_inner_frame, text="Close", command=self._on_closing_button).pack(side=tk.LEFT, padx=5, pady=5)
def _populate_profiles_listbox(self) -> None:
self.profiles_listbox.delete(0, tk.END)
for i, profile in enumerate(self._profiles_data):
display_name = profile.get("profile_name", f"Unnamed Profile {i+1}")
self.profiles_listbox.insert(tk.END, display_name)
self._update_profile_action_buttons_state()
self._update_action_buttons_state()
def _on_profile_select(self, event: Optional[tk.Event] = None) -> None:
selection_indices = self.profiles_listbox.curselection()
new_selected_index: Optional[int] = None
if selection_indices:
new_selected_index = selection_indices[0]
if self._selected_profile_index is not None and \
self._selected_profile_index != new_selected_index and \
self._current_profile_modified_in_form:
current_profile_name_in_form = self.profile_name_var.get()
if not current_profile_name_in_form and self._profiles_data and 0 <= self._selected_profile_index < len(self._profiles_data):
current_profile_name_in_form = self._profiles_data[self._selected_profile_index].get('profile_name', 'the previously selected profile')
prompt = (f"Profile '{current_profile_name_in_form}' has unsaved changes in the form.\n"
"Do you want to apply these changes before switching?")
response = messagebox.askyesnocancel("Unsaved Changes", prompt, parent=self)
if response is True:
if not self._save_current_form_to_profile_data(self._selected_profile_index):
self.profiles_listbox.selection_clear(0, tk.END)
if self._selected_profile_index is not None:
self.profiles_listbox.selection_set(self._selected_profile_index)
self.profiles_listbox.activate(self._selected_profile_index)
return
elif response is None:
self.profiles_listbox.selection_clear(0, tk.END)
if self._selected_profile_index is not None:
self.profiles_listbox.selection_set(self._selected_profile_index)
self.profiles_listbox.activate(self._selected_profile_index)
return
if new_selected_index is not None:
self._select_profile_by_index(new_selected_index)
else:
self._clear_profile_form()
self._selected_profile_index = None
self._update_profile_action_buttons_state()
self._update_analysis_status_display()
def _get_data_from_form(self) -> Dict[str, Any]:
return {
"profile_name": self.profile_name_var.get(),
"target_executable": self.target_exe_var.get(),
"program_parameters": self.program_params_var.get(),
}
def _select_profile_by_index(self, index: int) -> None:
if not (0 <= index < len(self._profiles_data)):
self._clear_profile_form()
self._selected_profile_index = None
# _update_analysis_status_display() è chiamato da _clear_profile_form
return
self._selected_profile_index = index
profile = self._profiles_data[index]
self.profile_name_var.set(profile.get("profile_name", ""))
self.target_exe_var.set(profile.get("target_executable", "")) # Triggera _on_target_exe_changed_in_form
self.program_params_var.set(profile.get("program_parameters", ""))
self._populate_actions_listbox()
self._enable_profile_form_editing(True)
self._current_profile_modified_in_form = False
if not self.profiles_listbox.curselection() or self.profiles_listbox.curselection()[0] != index:
self.profiles_listbox.selection_clear(0, tk.END)
self.profiles_listbox.selection_set(index)
self.profiles_listbox.activate(index)
self.profiles_listbox.see(index)
self.add_action_button.config(state=tk.NORMAL)
# _update_analysis_status_display() è chiamato da _on_target_exe_changed_in_form
# o dovrebbe essere chiamato esplicitamente qui se il set di target_exe_var non lo fa sempre.
# Per sicurezza, lo chiamiamo esplicitamente dopo aver impostato tutte le var.
self._update_analysis_status_display()
def _clear_profile_form(self) -> None:
self.profile_name_var.set("")
self.target_exe_var.set("") # Triggera _on_target_exe_changed_in_form
self.program_params_var.set("")
self._populate_actions_listbox()
self._enable_profile_form_editing(False)
self._current_profile_modified_in_form = False
self.add_action_button.config(state=tk.DISABLED)
self.edit_action_button.config(state=tk.DISABLED)
self.remove_action_button.config(state=tk.DISABLED)
self._update_analysis_status_display()
def _enable_profile_form_editing(self, enable: bool) -> None:
state = tk.NORMAL if enable else tk.DISABLED
self.profile_name_entry.config(state=state)
self.target_exe_entry.config(state=state)
self.browse_exe_button.config(state=state)
self.program_params_entry.config(state=state)
# analyse_symbols_button state è gestito da _update_analysis_status_display
def _update_profile_action_buttons_state(self) -> None:
profile_selected = self._selected_profile_index is not None
self.duplicate_button.config(state=tk.NORMAL if profile_selected else tk.DISABLED)
self.delete_button.config(state=tk.NORMAL if profile_selected else tk.DISABLED)
def _browse_target_executable(self) -> None:
current_path = self.target_exe_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="Select Target Executable",
filetypes=[("Executable files", ("*.exe", "*")), ("All files", "*.*")],
initialdir=initial_dir,
parent=self
)
if path:
self.target_exe_var.set(path) # Triggera _on_target_exe_changed_in_form
def _save_current_form_to_profile_data(self, profile_index: int) -> bool:
if not (0 <= profile_index < len(self._profiles_data)):
logger.error(f"Invalid profile index {profile_index} for saving form.")
return False
profile_name = self.profile_name_var.get().strip()
if not profile_name:
messagebox.showerror("Validation Error", "Profile Name cannot be empty.", parent=self)
self.profile_name_entry.focus_set()
return False
for i, p_item in enumerate(self._profiles_data):
if i != profile_index and p_item.get("profile_name") == profile_name:
messagebox.showerror("Validation Error", f"Profile name '{profile_name}' already exists.", parent=self)
self.profile_name_entry.focus_set()
return False
target_profile = self._profiles_data[profile_index]
old_name = target_profile.get("profile_name")
target_profile["profile_name"] = profile_name
target_profile["target_executable"] = self.target_exe_var.get().strip() # Questo aggiorna il path
target_profile["program_parameters"] = self.program_params_var.get()
self._current_profile_modified_in_form = False
self._profiles_list_changed_overall = True
if old_name != profile_name:
self.profiles_listbox.delete(profile_index)
self.profiles_listbox.insert(profile_index, profile_name)
self.profiles_listbox.selection_set(profile_index)
# Dopo aver salvato il form, lo stato dell'analisi potrebbe dover essere rivalutato
# se target_executable è cambiato. _update_analysis_status_display lo farà.
self._update_analysis_status_display()
logger.info(f"Profile '{profile_name}' (index {profile_index}) basic details updated.")
return True
def _new_profile(self) -> None:
if self._selected_profile_index is not None and self._current_profile_modified_in_form:
# ... (logica gestione modifiche non salvate come prima) ...
response = messagebox.askyesnocancel("Unsaved Changes",
f"Profile '{self._profiles_data[self._selected_profile_index].get('profile_name')}' has unsaved changes in the form.\n"
"Do you want to save them before creating a new profile?",
default=messagebox.CANCEL, parent=self)
if response is True:
if not self._save_current_form_to_profile_data(self._selected_profile_index):
return
elif response is None:
return
new_p = json.loads(json.dumps(DEFAULT_PROFILE)) # Deep copy del template
base_name = "New Profile"
name_candidate = base_name
count = 1
existing_names = {p.get("profile_name") for p in self._profiles_data}
while name_candidate in existing_names:
name_candidate = f"{base_name} ({count})"
count += 1
new_p["profile_name"] = name_candidate
self._profiles_data.append(new_p)
self._profiles_list_changed_overall = True
self._populate_profiles_listbox()
self._select_profile_by_index(len(self._profiles_data) - 1)
self.profile_name_entry.focus_set()
self.profile_name_entry.selection_range(0, tk.END)
self._mark_form_as_modified()
def _duplicate_profile(self) -> None:
if self._selected_profile_index is None: return
if self._current_profile_modified_in_form:
if not self._save_current_form_to_profile_data(self._selected_profile_index):
return
original_profile = self._profiles_data[self._selected_profile_index]
duplicated_profile = json.loads(json.dumps(original_profile)) # Deep copy
base_name = f"{original_profile.get('profile_name', 'Profile')}_copy"
name_candidate = base_name
count = 1
existing_names = {p.get("profile_name") for p in self._profiles_data}
while name_candidate in existing_names:
name_candidate = f"{base_name}_{count}"
count += 1
duplicated_profile["profile_name"] = name_candidate
# L'analisi dei simboli duplicata si riferirà all'exe originale;
# l'utente dovrà rianalizzare se cambia l'exe del profilo duplicato.
# L'impostazione del nome del profilo non dovrebbe cancellare symbol_analysis
self._profiles_data.append(duplicated_profile)
self._profiles_list_changed_overall = True
self._populate_profiles_listbox()
self._select_profile_by_index(len(self._profiles_data) - 1)
self._mark_form_as_modified()
def _delete_profile(self) -> None:
if self._selected_profile_index is None or not self._profiles_data: return
profile_to_delete = self._profiles_data[self._selected_profile_index]
profile_name = profile_to_delete.get("profile_name", "this profile")
if not messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete '{profile_name}'?", parent=self):
return
idx_to_delete = self._selected_profile_index
del self._profiles_data[idx_to_delete]
self._profiles_list_changed_overall = True
new_list_size = len(self._profiles_data)
self._selected_profile_index = None
self._populate_profiles_listbox()
if new_list_size == 0:
self._clear_profile_form()
else:
new_selection_idx = min(idx_to_delete, new_list_size - 1)
if new_selection_idx >= 0: # Assicura che l'indice sia valido
self._select_profile_by_index(new_selection_idx)
else: # Se la lista è diventata vuota, pulisci
self._clear_profile_form()
def _save_all_profiles_to_settings(self) -> None:
if self._selected_profile_index is not None and self._current_profile_modified_in_form:
if not self._save_current_form_to_profile_data(self._selected_profile_index):
messagebox.showerror("Save Error", "Could not save changes from the form for the selected profile. Aborting save to settings.", parent=self)
return
profile_names_seen = set()
for i, profile in enumerate(self._profiles_data):
# ... (validazione nomi profili e struttura azioni come prima) ...
name = profile.get("profile_name", "").strip()
if not name:
messagebox.showerror("Validation Error", f"Profile at index {i} has an empty name.", parent=self)
return
if name in profile_names_seen:
messagebox.showerror("Validation Error", f"Duplicate profile name '{name}'.", parent=self)
return
profile_names_seen.add(name)
actions = profile.get("actions")
if not isinstance(actions, list):
messagebox.showerror("Data Error", f"Profile '{name}' has malformed actions.", parent=self)
return
for idx_a, action in enumerate(actions):
if not isinstance(action, dict):
messagebox.showerror("Data Error", f"Profile '{name}', action {idx_a+1} is malformed.", parent=self)
return
self.app_settings.set_profiles(self._profiles_data)
if self.app_settings.save_settings():
logger.info(f"All {len(self._profiles_data)} profiles saved to AppSettings.")
messagebox.showinfo("Profiles Saved", "All profile changes have been saved.", parent=self)
self._current_profile_modified_in_form = False
self._profiles_list_changed_overall = False
else:
messagebox.showerror("Save Error", "Could not save profiles to the settings file. Check logs.", parent=self)
def _on_closing_button(self) -> None:
# ... (come prima) ...
needs_save_prompt = False
prompt_message = ""
if self._selected_profile_index is not None and self._current_profile_modified_in_form:
needs_save_prompt = True
# ... (costruzione messaggio)
profile_name = self.profile_name_var.get() or self._profiles_data[self._selected_profile_index].get('profile_name', 'current profile')
prompt_message = f"Profile '{profile_name}' has unsaved changes in the form.\n"
if self._profiles_list_changed_overall: # Se la lista stessa è cambiata (add/del/rename)
needs_save_prompt = True
if prompt_message: # Se già c'era un messaggio per il form
prompt_message += "Additionally, the overall list of profiles (or their content) has changed.\n"
else:
prompt_message = "The list of profiles (or their content) has changed.\n"
if needs_save_prompt:
prompt_message += "Do you want to save all changes before closing?"
response = messagebox.askyesnocancel("Unsaved Changes", prompt_message, default=messagebox.CANCEL, parent=self)
if response is True:
self._save_all_profiles_to_settings()
# Se il salvataggio fallisce, _save_all_profiles_to_settings mostra un errore,
# ma la finestra si chiuderà comunque. Potremmo voler cambiare questo comportamento.
elif response is None: # Cancel
return
# Se False (No), procedi a chiudere senza salvare
if self.progress_dialog and self.progress_dialog.winfo_exists():
logger.warning("Closing ProfileManagerWindow while symbol analysis dialog might be open.")
# Potremmo voler provare a chiudere la dialog di progresso qui, o avvisare.
# Per ora, la dialog di progresso gestisce la sua chiusura.
self.parent_window.focus_set()
self.destroy()
# --- Gestione Azioni (Listbox e Bottoni Azione) ---
def _populate_actions_listbox(self) -> None:
self.actions_listbox.delete(0, tk.END)
self._selected_action_index_in_profile = None
if self._selected_profile_index is not None and \
0 <= self._selected_profile_index < len(self._profiles_data):
profile = self._profiles_data[self._selected_profile_index]
actions = profile.get("actions", [])
for i, action in enumerate(actions):
bp = action.get("breakpoint_location", "N/A")[:30] # Tronca per display
num_vars = len(action.get("variables_to_dump", []))
fmt = action.get("output_format", "N/A")
cont = "Yes" if action.get("continue_after_dump", False) else "No"
summary = f"BP: {bp}{'...' if len(action.get('breakpoint_location', '')) > 30 else ''} (Vars:{num_vars},Fmt:{fmt},Cont:{cont})"
self.actions_listbox.insert(tk.END, summary)
self._update_action_buttons_state()
def _on_action_select_in_listbox(self, event: Optional[tk.Event] = None) -> None:
selection_indices = self.actions_listbox.curselection()
if selection_indices:
self._selected_action_index_in_profile = selection_indices[0]
else:
self._selected_action_index_in_profile = None
self._update_action_buttons_state()
def _update_action_buttons_state(self) -> None:
profile_selected = self._selected_profile_index is not None
action_selected = self._selected_action_index_in_profile is not None
self.add_action_button.config(state=tk.NORMAL if profile_selected else tk.DISABLED)
self.edit_action_button.config(state=tk.NORMAL if action_selected else tk.DISABLED)
self.remove_action_button.config(state=tk.NORMAL if action_selected else tk.DISABLED)
def _add_action(self) -> None:
if self._selected_profile_index is None: return
current_profile = self._profiles_data[self._selected_profile_index]
current_profile_target_exe = self.target_exe_var.get()
symbol_analysis_data = current_profile.get("symbol_analysis")
editor = ActionEditorWindow(
self, action_data=None, is_new=True,
target_executable_path=current_profile_target_exe,
app_settings=self.app_settings,
symbol_analysis_data=symbol_analysis_data
)
new_action_data = editor.get_result()
if new_action_data:
if "actions" not in current_profile or not isinstance(current_profile["actions"], list):
current_profile["actions"] = []
current_profile["actions"].append(new_action_data)
self._profiles_list_changed_overall = True
self._current_profile_modified_in_form = True # Il profilo nel form è cambiato
self._populate_actions_listbox()
self.actions_listbox.selection_set(tk.END)
self._on_action_select_in_listbox()
def _edit_action(self) -> None:
if self._selected_profile_index is None or self._selected_action_index_in_profile is None: return
current_profile = self._profiles_data[self._selected_profile_index]
actions_list = current_profile.get("actions", [])
if not (0 <= self._selected_action_index_in_profile < len(actions_list)): return
action_to_edit = actions_list[self._selected_action_index_in_profile]
current_profile_target_exe = self.target_exe_var.get()
symbol_analysis_data = current_profile.get("symbol_analysis")
editor = ActionEditorWindow(
self, action_data=action_to_edit, is_new=False,
target_executable_path=current_profile_target_exe,
app_settings=self.app_settings,
symbol_analysis_data=symbol_analysis_data
)
updated_action_data = editor.get_result()
if updated_action_data:
current_profile["actions"][self._selected_action_index_in_profile] = updated_action_data
self._profiles_list_changed_overall = True
self._current_profile_modified_in_form = True
idx_to_reselect = self._selected_action_index_in_profile
self._populate_actions_listbox()
if idx_to_reselect is not None and 0 <= idx_to_reselect < self.actions_listbox.size():
self.actions_listbox.selection_set(idx_to_reselect)
self._on_action_select_in_listbox()
def _remove_action(self) -> None:
if self._selected_profile_index is None or self._selected_action_index_in_profile is None: return
# ... (come prima) ...
profile = self._profiles_data[self._selected_profile_index]
action_summary_to_delete = self.actions_listbox.get(self._selected_action_index_in_profile)
if not messagebox.askyesno("Confirm Delete Action",
f"Are you sure you want to delete this action?\n\n{action_summary_to_delete}",
parent=self): return
actions_list = profile.get("actions")
if isinstance(actions_list, list) and 0 <= self._selected_action_index_in_profile < len(actions_list):
del actions_list[self._selected_action_index_in_profile]
self._profiles_list_changed_overall = True
self._current_profile_modified_in_form = True
idx_to_reselect_after_delete = self._selected_action_index_in_profile
self._populate_actions_listbox()
num_actions_remaining = len(actions_list)
if num_actions_remaining > 0:
new_selection = min(idx_to_reselect_after_delete, num_actions_remaining - 1)
if new_selection >=0: self.actions_listbox.selection_set(new_selection)
self._on_action_select_in_listbox()
else: logger.error("Could not remove action: 'actions' list missing or index invalid.")
# --- Logica per Analisi Simboli ---
def _update_analysis_status_display(self) -> None:
if self._selected_profile_index is None or not (0 <= self._selected_profile_index < len(self._profiles_data)):
self._current_profile_target_exe_details_label_var.set("Target: N/A")
self._current_profile_analysis_status_label_var.set("Symbol Analysis: Select a profile.")
self.analyse_symbols_button.config(state=tk.DISABLED)
return
profile = self._profiles_data[self._selected_profile_index]
target_exe_from_form = self.target_exe_var.get() # Path attuale nel form
exe_display_name = os.path.basename(target_exe_from_form) if target_exe_from_form else "N/A"
details_text_lines = [f"Target in Form: {exe_display_name}"]
status_text = "Symbol Analysis: "
status_color = "blue" # Default
analysis_button_state = tk.DISABLED
if target_exe_from_form and os.path.isfile(target_exe_from_form):
analysis_button_state = tk.NORMAL # Abilita se il file nel form esiste
if not target_exe_from_form:
status_text += "Target executable not specified in form."
elif not os.path.isfile(target_exe_from_form):
status_text += f"Target '{exe_display_name}' not found on disk."
status_color = "red"
else: # Il file nel form esiste, ora controlla i dati di analisi salvati
analysis_data = profile.get("symbol_analysis")
if analysis_data and isinstance(analysis_data, dict):
saved_checksum = analysis_data.get("executable_checksum")
saved_analysis_ts_str = analysis_data.get("analysis_timestamp")
saved_exe_at_analysis = analysis_data.get("analyzed_executable_path", "Unknown")
saved_file_ts_str = analysis_data.get("executable_timestamp", "N/A")
details_text_lines.append(f"Last Analysis on File: {os.path.basename(saved_exe_at_analysis)}")
details_text_lines.append(f" File Timestamp (at analysis): {saved_file_ts_str}")
details_text_lines.append(f" Analysis Date: {saved_analysis_ts_str or 'N/A'}")
details_text_lines.append(f" Saved Checksum: {saved_checksum or 'N/A'}")
current_checksum_for_form_exe = self._calculate_file_checksum(target_exe_from_form)
details_text_lines.append(f" Current Form Exe Checksum: {current_checksum_for_form_exe or 'N/A (calc failed)'}")
if os.path.normpath(saved_exe_at_analysis) != os.path.normpath(target_exe_from_form):
status_text += "TARGET CHANGED since last analysis. RE-ANALYSIS RECOMMENDED."
status_color = "orange red"
elif saved_checksum and current_checksum_for_form_exe and saved_checksum == current_checksum_for_form_exe:
status_text += "Up-to-date."
status_color = "dark green"
elif saved_checksum and current_checksum_for_form_exe and saved_checksum != current_checksum_for_form_exe:
status_text += "EXECUTABLE CHANGED since last analysis. RE-ANALYSIS REQUIRED."
status_color = "red"
else: # Checksum mancanti o confronto non possibile
status_text += "Status unclear. Consider re-analysing."
status_color = "orange red"
else: # Nessuna analisi salvata per questo profilo
status_text += "Not performed. Click 'Analyse' to generate."
status_color = "blue"
self.analyse_symbols_button.config(state=analysis_button_state)
self._current_profile_target_exe_details_label_var.set("\n".join(details_text_lines))
self._current_profile_analysis_status_label_var.set(status_text)
self.analysis_status_label.config(foreground=status_color)
def _calculate_file_checksum(self, filepath: str, hash_type: str = "md5") -> Optional[str]:
if not os.path.isfile(filepath): return None
try:
hasher = hashlib.md5()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""): hasher.update(chunk)
return hasher.hexdigest()
except Exception as e:
logger.error(f"Error calculating MD5 for {filepath}: {e}", exc_info=True)
return None
def _trigger_symbol_analysis(self) -> None:
if self._selected_profile_index is None:
messagebox.showerror("Error", "No profile selected.", parent=self)
return
target_exe_for_analysis = self.target_exe_var.get()
if not target_exe_for_analysis or not os.path.isfile(target_exe_for_analysis):
messagebox.showerror("Error", "Target executable path in the form is invalid or file not found.", parent=self)
self._update_analysis_status_display()
return
gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path")
if not gdb_exe_path or not os.path.isfile(gdb_exe_path):
messagebox.showerror("GDB Error", f"GDB executable not found or not configured: {gdb_exe_path}", parent=self)
return
profile_to_update = self._profiles_data[self._selected_profile_index]
self.progress_dialog = SymbolAnalysisProgressDialog(self)
analysis_thread = threading.Thread(
target=self._perform_symbol_analysis_thread,
args=(profile_to_update, target_exe_for_analysis, gdb_exe_path, self.progress_dialog),
daemon=True
)
analysis_thread.start()
def _perform_symbol_analysis_thread(self, profile_to_update: Dict[str, Any],
target_exe_path: str, gdb_exe_path: str,
progress_dialog: SymbolAnalysisProgressDialog):
temp_gdb_session: Optional[GDBSession] = None
analysis_data_dict: Dict[str, Any] = {
"analyzed_executable_path": target_exe_path,
"executable_checksum": None,
"executable_timestamp": "N/A",
"analysis_timestamp": "N/A",
"gdb_version_info": "N/A",
"symbols": {"functions": []}
}
analysis_succeeded_overall = False
def gui_log(msg: str):
if progress_dialog and progress_dialog.winfo_exists():
self.after(0, progress_dialog.log_message, msg)
def gui_set_status(msg: str):
if progress_dialog and progress_dialog.winfo_exists():
self.after(0, progress_dialog.set_status, msg)
try:
gui_log(f"Starting GDB for symbol analysis of: {os.path.basename(target_exe_path)}")
gui_set_status(f"Initializing GDB for {os.path.basename(target_exe_path)}...")
temp_gdb_session = GDBSession(gdb_exe_path, target_exe_path, None, {})
startup_timeout = self.app_settings.get_setting("timeouts", "gdb_start", 30)
command_timeout = self.app_settings.get_setting("timeouts", "gdb_command", 30)
temp_gdb_session.start(timeout=startup_timeout)
gui_log("GDB session started.")
if not temp_gdb_session.symbols_found:
gui_log("WARNING: GDB reported no debugging symbols. Analysis may be limited.")
gui_set_status("Fetching GDB version..."); gui_log("Fetching GDB version...")
analysis_data_dict["gdb_version_info"] = temp_gdb_session.get_gdb_version(timeout=command_timeout) or "N/A"
gui_log(f"GDB Version: {analysis_data_dict['gdb_version_info']}")
gui_set_status("Fetching function list..."); gui_log("Fetching function list from GDB...")
functions = temp_gdb_session.list_functions(timeout=command_timeout * 3) # Dare più tempo a info functions
analysis_data_dict["symbols"]["functions"] = functions
gui_log(f"Found {len(functions)} functions.")
# Altre analisi (variabili, tipi) verranno qui nelle iterazioni future
# analysis_data_dict["symbols"]["global_variables"] = temp_gdb_session.list_global_variables(...)
gui_set_status("Calculating file checksum and timestamp..."); gui_log("Calculating file checksum and timestamp...")
analysis_data_dict["executable_checksum"] = self._calculate_file_checksum(target_exe_path)
try:
mtime = os.path.getmtime(target_exe_path)
analysis_data_dict["executable_timestamp"] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(mtime))
except OSError: pass # Lascia N/A
gui_log(f"Checksum (MD5): {analysis_data_dict['executable_checksum'] or 'N/A'}")
gui_log(f"File Timestamp: {analysis_data_dict['executable_timestamp']}")
analysis_data_dict["analysis_timestamp"] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
gui_log(f"Analysis performed at: {analysis_data_dict['analysis_timestamp']}")
analysis_succeeded_overall = True
gui_set_status("Symbol analysis successfully completed."); gui_log("\nSymbol analysis successfully completed.")
except Exception as e:
logger.error(f"Error during symbol analysis thread for '{target_exe_path}': {e}", exc_info=True)
error_msg = f"ERROR during analysis: {type(e).__name__} - {e}"
gui_log(f"\n{error_msg}")
gui_set_status(error_msg)
finally:
if temp_gdb_session and temp_gdb_session.is_alive():
gui_log("Closing GDB session..."); gui_set_status("Closing GDB session...")
quit_timeout = self.app_settings.get_setting("timeouts", "gdb_quit", 10)
try:
temp_gdb_session.quit(timeout=quit_timeout)
gui_log("GDB session closed.")
except Exception as e_quit:
logger.error(f"Error quitting GDB session in analysis thread: {e_quit}")
gui_log(f"Error closing GDB: {e_quit}")
self.after(0, self._finalize_symbol_analysis, profile_to_update, analysis_data_dict, analysis_succeeded_overall, progress_dialog)
def _finalize_symbol_analysis(self, profile_to_update: Dict[str, Any],
analysis_data: Dict[str, Any], success: bool,
progress_dialog: SymbolAnalysisProgressDialog):
"""Called from GUI thread to update profile data and UI after analysis."""
if success:
profile_to_update["symbol_analysis"] = analysis_data
self._profiles_list_changed_overall = True
self._current_profile_modified_in_form = True # Il contenuto del profilo è cambiato
logger.info(f"Symbol analysis data updated for profile: '{profile_to_update.get('profile_name')}'.")
if self.winfo_exists(): # Assicura che la finestra genitore esista ancora
messagebox.showinfo("Analysis Complete", "Symbol analysis has finished successfully.", parent=self)
else:
# Non aggiorniamo symbol_analysis nel profilo se fallisce, ma potremmo salvare dati parziali se utile
logger.error(f"Symbol analysis failed for profile: '{profile_to_update.get('profile_name')}'.")
if self.winfo_exists():
messagebox.showerror("Analysis Failed", "Symbol analysis did not complete successfully. Check logs.", parent=self)
if progress_dialog and progress_dialog.winfo_exists():
progress_dialog.analysis_complete_or_failed(success)
# La dialog ora ha un bottone Close abilitato, l'utente la chiuderà.
# Oppure: self.after(2000, lambda: progress_dialog.destroy() if progress_dialog.winfo_exists() else None)
self._update_analysis_status_display()