# 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 SymbolListViewerDialog(tk.Toplevel): # NUOVA DIALOG PER VISUALIZZARE LISTE """A simple dialog to view a list of symbols.""" def __init__(self, parent: tk.Widget, symbols: List[str], title: str = "Symbol List"): super().__init__(parent) self.title(title) parent_x = parent.winfo_x() parent_y = parent.winfo_y() parent_width = parent.winfo_width() parent_height = parent.winfo_height() width = 500 height = 450 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() main_frame = ttk.Frame(self, padding="10") main_frame.pack(expand=True, fill=tk.BOTH) main_frame.rowconfigure(1, weight=1) main_frame.columnconfigure(0, weight=1) filter_frame = ttk.Frame(main_frame) filter_frame.grid(row=0, column=0, sticky="ew", pady=(0, 5)) ttk.Label(filter_frame, text="Filter:").pack(side=tk.LEFT, padx=(0,5)) self.filter_var = tk.StringVar() self.filter_var.trace_add("write", self._apply_filter) ttk.Entry(filter_frame, textvariable=self.filter_var, width=40).pack(side=tk.LEFT, expand=True, fill=tk.X) self.listbox = tk.Listbox(main_frame, selectmode=tk.SINGLE) self.listbox.grid(row=1, column=0, sticky="nsew") scrollbar_y = ttk.Scrollbar(main_frame, orient=tk.VERTICAL, command=self.listbox.yview) scrollbar_y.grid(row=1, column=1, sticky="ns") self.listbox.configure(yscrollcommand=scrollbar_y.set) scrollbar_x = ttk.Scrollbar(main_frame, orient=tk.HORIZONTAL, command=self.listbox.xview) scrollbar_x.grid(row=2, column=0, sticky="ew") self.listbox.configure(xscrollcommand=scrollbar_x.set) self._original_symbols = sorted(symbols) self._populate_listbox(self._original_symbols) button_frame = ttk.Frame(main_frame, padding=(0, 10, 0, 0)) button_frame.grid(row=3, column=0, columnspan=2, sticky="e") ttk.Button(button_frame, text="Close", command=self.destroy).pack() self.protocol("WM_DELETE_WINDOW", self.destroy) def _populate_listbox(self, symbols_to_show: List[str]): self.listbox.delete(0, tk.END) for item in symbols_to_show: self.listbox.insert(tk.END, item) def _apply_filter(self, *args): filter_text = self.filter_var.get().lower() if not filter_text: self._populate_listbox(self._original_symbols) else: filtered_list = [s for s in self._original_symbols if filter_text in s.lower()] self._populate_listbox(filtered_list) 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("1050x750") # Aumentata leggermente l'altezza per i nuovi widget 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 # StringVars per i conteggi self.functions_count_var = tk.StringVar(value="Functions: N/A") # self.variables_count_var = tk.StringVar(value="Globals: N/A") # Futuro # self.types_count_var = tk.StringVar(value="Types: N/A") # Futuro # self.sources_count_var = tk.StringVar(value="Sources: N/A") # Futuro 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("<>", 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=0) # Riga per Conteggi Simboli right_pane.rowconfigure(3, weight=1) # Riga per Azioni di Debug 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 Status & Control", 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) symbols_summary_frame = ttk.LabelFrame(right_pane, text="Analyzed Symbols Summary", padding="10") symbols_summary_frame.grid(row=2, column=0, sticky="new", pady=5) symbols_summary_frame.columnconfigure(0, weight=1) symbols_summary_frame.columnconfigure(1, weight=0) row_s = 0 ttk.Label(symbols_summary_frame, textvariable=self.functions_count_var).grid(row=row_s, column=0, sticky="w", padx=5, pady=2) self.view_functions_button = ttk.Button(symbols_summary_frame, text="View...", command=self._view_analyzed_functions, state=tk.DISABLED, width=8) self.view_functions_button.grid(row=row_s, column=1, padx=(10,5), pady=2, sticky="w") row_s += 1 # Placeholder per futuri conteggi e bottoni (Iterazione 3) # ttk.Label(symbols_summary_frame, textvariable=self.variables_count_var).grid(row=row_s, column=0, sticky="w", padx=5, pady=2) # self.view_variables_button = ttk.Button(symbols_summary_frame, text="View...", command=self._view_analyzed_variables, state=tk.DISABLED, width=8) # self.view_variables_button.grid(row=row_s, column=1, padx=(10,5), pady=2, sticky="w") # row_s += 1 # ... (e così via per tipi e sorgenti) actions_ui_frame = ttk.LabelFrame(right_pane, text="Debug Actions", padding="10") actions_ui_frame.grid(row=3, column=0, sticky="nsew", pady=5) # Riga 3 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=6) self.actions_listbox.grid(row=0, column=0, sticky="nsew", pady=5, padx=(0,5)) self.actions_listbox.bind("<>", 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, anchor="n") 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, anchor="n") 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, anchor="n") 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 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", "")) 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) self._update_analysis_status_display() def _clear_profile_form(self) -> None: self.profile_name_var.set("") self.target_exe_var.set("") 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) 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) 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() 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) 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: 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)) 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)) 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 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: self._select_profile_by_index(new_selection_idx) else: 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): 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: 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 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: needs_save_prompt = True if prompt_message: 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() elif response is None: return if self.progress_dialog and self.progress_dialog.winfo_exists(): logger.warning("Closing ProfileManagerWindow while symbol analysis dialog might be open.") self.parent_window.focus_set() self.destroy() 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] 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 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 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) self.functions_count_var.set("Functions: N/A") self.view_functions_button.config(state=tk.DISABLED) return profile = self._profiles_data[self._selected_profile_index] target_exe_in_form = self.target_exe_var.get() exe_display_name = os.path.basename(target_exe_in_form) if target_exe_in_form else "N/A" details_text_lines = [f"Target in Form: {exe_display_name}"] status_text = "Symbol Analysis: " status_color = "blue" funcs_count_text = "Functions: N/A" view_funcs_btn_state = tk.DISABLED analysis_button_state = tk.DISABLED if target_exe_in_form and os.path.isfile(target_exe_in_form): analysis_button_state = tk.NORMAL if not target_exe_in_form: status_text += "Target executable not specified in form." elif not os.path.isfile(target_exe_in_form): status_text += f"Target '{exe_display_name}' not found on disk." status_color = "red" else: analysis_data = profile.get("symbol_analysis") if analysis_data and isinstance(analysis_data, dict): symbols_dict = analysis_data.get("symbols", {}) num_functions = symbols_dict.get("functions_count", 0) funcs_count_text = f"Functions: {num_functions}" if num_functions > 0 : view_funcs_btn_state = tk.NORMAL 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_in_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_in_form): status_text += "TARGET CHANGED since last analysis. RE-ANALYSIS RECOMMENDED." status_color = "orange red" # Disabilita i bottoni View se il target è cambiato view_funcs_btn_state = tk.DISABLED 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" # Disabilita i bottoni View se l'eseguibile è cambiato view_funcs_btn_state = tk.DISABLED else: # Checksum mancanti o confronto non possibile status_text += "Status unclear. Consider re-analysing." status_color = "orange red" # Disabilita i bottoni View se lo stato è incerto view_funcs_btn_state = tk.DISABLED else: # Nessuna analisi salvata 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) self.functions_count_var.set(funcs_count_text) self.view_functions_button.config(state=view_funcs_btn_state) 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": [], "functions_count": 0, # "global_variables": [], # Per Iterazione 3 # "global_variables_count": 0,# Per Iterazione 3 # "types": [], # Per Iterazione 3 # "types_count": 0, # Per Iterazione 3 # "source_files": [], # Per Iterazione 3 # "source_files_count": 0 # Per Iterazione 3 } } 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...") gdb_version = temp_gdb_session.get_gdb_version(timeout=command_timeout) analysis_data_dict["gdb_version_info"] = gdb_version if gdb_version else "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 * 4) # Più tempo per info functions analysis_data_dict["symbols"]["functions"] = functions analysis_data_dict["symbols"]["functions_count"] = len(functions) # Salva il conteggio gui_log(f"Found {len(functions)} functions.") # --- In future iterations, call methods for other symbols and store counts --- # gui_set_status("Fetching global variables..."); gui_log("Fetching global variables...") # globals_list = temp_gdb_session.list_global_variables(timeout=command_timeout * 2) # analysis_data_dict["symbols"]["global_variables"] = globals_list # analysis_data_dict["symbols"]["global_variables_count"] = len(globals_list) # gui_log(f"Found {len(globals_list)} global variables.") # ... (similarly for types and source_files) ... 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 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 logger.info(f"Symbol analysis data updated for profile: '{profile_to_update.get('profile_name')}'.") if self.winfo_exists(): messagebox.showinfo("Analysis Complete", "Symbol analysis has finished successfully.", parent=self) else: 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) self._update_analysis_status_display() def _view_analyzed_functions(self) -> None: if self._selected_profile_index is None or \ not (0 <= self._selected_profile_index < len(self._profiles_data)): messagebox.showinfo("Info", "No profile selected or data available.", parent=self) return profile = self._profiles_data[self._selected_profile_index] analysis_data = profile.get("symbol_analysis") if not analysis_data or not isinstance(analysis_data.get("symbols"), dict): messagebox.showinfo("No Analysis Data", "No symbol analysis data available for this profile.", parent=self) return functions_list = analysis_data["symbols"].get("functions", []) if not functions_list: messagebox.showinfo("No Functions", "No functions found in the last analysis for this profile.", parent=self) return target_exe_in_form = self.target_exe_var.get() analyzed_exe_path = analysis_data.get("analyzed_executable_path", "") exe_name_for_title = os.path.basename(target_exe_in_form) if target_exe_in_form else "Unknown Executable" is_obsolete = True if os.path.normpath(analyzed_exe_path) == os.path.normpath(target_exe_in_form): current_checksum = self._calculate_file_checksum(target_exe_in_form) saved_checksum = analysis_data.get("executable_checksum") if current_checksum and saved_checksum and current_checksum == saved_checksum: is_obsolete = False title_suffix = " (Analysis might be obsolete)" if is_obsolete else "" dialog_title = f"Analyzed Functions for '{exe_name_for_title}'{title_suffix}" SymbolListViewerDialog(self, functions_list, title=dialog_title)