1010 lines
53 KiB
Python
1010 lines
53 KiB
Python
# File: cpp_python_debug/gui/profile_manager_window.py
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, filedialog
|
|
import logging
|
|
import json
|
|
import os
|
|
import threading
|
|
import time
|
|
from typing import TYPE_CHECKING, List, Dict, Any, Optional, Callable, Union
|
|
|
|
# Importa classi di dialogo dal nuovo modulo
|
|
from .dialogs import SymbolAnalysisProgressDialog, SymbolListViewerDialog, FunctionSelectorDialog
|
|
|
|
# Importa utility file dal nuovo modulo
|
|
from ..core import file_utils
|
|
|
|
# Importa le classi SymbolAnalyzer e GDBMISession dal nuovo modulo
|
|
from ..core.symbol_analyzer import SymbolAnalyzer
|
|
from ..core.gdb_mi_session import GDBMISession # NEW: Import GDBMISession
|
|
|
|
# Import per ActionEditorWindow (è nel package corrente)
|
|
from .action_editor_window import ActionEditorWindow
|
|
|
|
if TYPE_CHECKING:
|
|
from ..core.config_manager import AppSettings
|
|
from .main_window import GDBGui
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_ACTION = {
|
|
"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,
|
|
"dump_on_every_hit": True
|
|
}
|
|
|
|
DEFAULT_PROFILE = {
|
|
"profile_name": "New Profile",
|
|
"target_executable": "",
|
|
"program_parameters": "",
|
|
"symbol_analysis": None,
|
|
"actions": [DEFAULT_ACTION.copy()]
|
|
}
|
|
|
|
|
|
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")
|
|
|
|
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")
|
|
self.types_count_var = tk.StringVar(value="Types: N/A")
|
|
self.sources_count_var = tk.StringVar(value="Sources: N/A")
|
|
|
|
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=0)
|
|
right_pane.rowconfigure(3, weight=0)
|
|
right_pane.rowconfigure(4, 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 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
|
|
|
|
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
|
|
|
|
ttk.Label(symbols_summary_frame, textvariable=self.types_count_var).grid(row=row_s, column=0, sticky="w", padx=5, pady=2)
|
|
self.view_types_button = ttk.Button(symbols_summary_frame, text="View...", command=self._view_analyzed_types, state=tk.DISABLED, width=8)
|
|
self.view_types_button.grid(row=row_s, column=1, padx=(10,5), pady=2, sticky="w")
|
|
row_s += 1
|
|
|
|
ttk.Label(symbols_summary_frame, textvariable=self.sources_count_var).grid(row=row_s, column=0, sticky="w", padx=5, pady=2)
|
|
self.view_sources_button = ttk.Button(symbols_summary_frame, text="View...", command=self._view_analyzed_sources, state=tk.DISABLED, width=8)
|
|
self.view_sources_button.grid(row=row_s, column=1, padx=(10,5), pady=2, sticky="w")
|
|
row_s += 1
|
|
|
|
actions_ui_frame = ttk.LabelFrame(right_pane, text="Debug Actions", padding="10")
|
|
actions_ui_frame.grid(row=4, 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=6)
|
|
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")
|
|
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: # Cancelled switch
|
|
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", "*.*")], # For cross-platform
|
|
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
|
|
|
|
# Check for duplicate names, excluding the current profile being edited
|
|
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()
|
|
# symbol_analysis and actions are managed separately
|
|
|
|
self._current_profile_modified_in_form = False
|
|
self._profiles_list_changed_overall = True # Mark that something in the profile data changed
|
|
|
|
# Update listbox if name changed
|
|
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() # Reflect changes to target exe
|
|
logger.info(f"Profile '{profile_name}' (index {profile_index}) basic details updated in internal list.")
|
|
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 # Save failed, abort new profile creation
|
|
elif response is None: # User cancelled
|
|
return
|
|
|
|
new_p = json.loads(json.dumps(DEFAULT_PROFILE)) # Deep copy of default
|
|
|
|
# Generate a unique name
|
|
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) # Select the new profile
|
|
self.profile_name_entry.focus_set()
|
|
self.profile_name_entry.selection_range(0, tk.END)
|
|
self._mark_form_as_modified() # New profile is considered modified until first explicit save of details
|
|
|
|
def _duplicate_profile(self) -> None:
|
|
if self._selected_profile_index is None:
|
|
return
|
|
|
|
if self._current_profile_modified_in_form:
|
|
# Save changes in the form to the current profile data before duplicating
|
|
if not self._save_current_form_to_profile_data(self._selected_profile_index):
|
|
return # Save failed, abort duplication
|
|
|
|
original_profile = self._profiles_data[self._selected_profile_index]
|
|
duplicated_profile = json.loads(json.dumps(original_profile)) # Deep copy
|
|
|
|
# Generate a unique name for the duplicated 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}" # Use underscore if copy(count) exists
|
|
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) # Select the duplicated profile
|
|
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 # Reset selection before repopulating
|
|
self._populate_profiles_listbox()
|
|
|
|
if new_list_size == 0:
|
|
self._clear_profile_form()
|
|
else:
|
|
# Try to select a reasonable item after deletion
|
|
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: # Should not happen if new_list_size > 0
|
|
self._clear_profile_form()
|
|
|
|
|
|
def _save_all_profiles_to_settings(self) -> None:
|
|
# First, ensure any pending changes in the form for the currently selected profile are saved to our internal list
|
|
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
|
|
|
|
# Basic validation on all profiles before saving to settings
|
|
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} (0-based) has an empty name. Please provide a name.", parent=self)
|
|
if self._selected_profile_index != i: # If it's not the current one
|
|
self._select_profile_by_index(i) # Select it so user can fix
|
|
self.profile_name_entry.focus_set()
|
|
return
|
|
if name in profile_names_seen:
|
|
messagebox.showerror("Validation Error", f"Duplicate profile name '{name}' found. Profile names must be unique.", parent=self)
|
|
# Attempt to select the first instance of the duplicate if not current
|
|
first_occurrence_index = next((idx for idx, p in enumerate(self._profiles_data) if p.get("profile_name") == name), -1)
|
|
if first_occurrence_index != -1 and self._selected_profile_index != first_occurrence_index :
|
|
self._select_profile_by_index(first_occurrence_index)
|
|
self.profile_name_entry.focus_set()
|
|
return
|
|
profile_names_seen.add(name)
|
|
|
|
# Validate actions structure (basic check)
|
|
actions = profile.get("actions")
|
|
if not isinstance(actions, list): # Must be a list
|
|
messagebox.showerror("Data Error", f"Profile '{name}' has malformed actions data (not a list). Please correct or re-create actions.", parent=self)
|
|
return
|
|
for idx_a, action in enumerate(actions):
|
|
if not isinstance(action, dict): # Each action must be a dictionary
|
|
messagebox.showerror("Data Error", f"Profile '{name}', action #{idx_a+1} is malformed (not a dictionary). Please re-create this action.", parent=self)
|
|
return
|
|
# Further validation of action content could be added here if needed
|
|
|
|
# If all validations pass, save to settings
|
|
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 # Reset modified flag for current form
|
|
self._profiles_list_changed_overall = False # Reset overall change flag
|
|
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 = ""
|
|
|
|
# Check if current form has unsaved changes
|
|
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"
|
|
|
|
# Check if the overall list of profiles has changed (e.g., new profile, deleted, duplicated, or content of other profiles changed)
|
|
if self._profiles_list_changed_overall:
|
|
needs_save_prompt = True
|
|
if prompt_message: # If already have a message for current 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: # Save
|
|
self._save_all_profiles_to_settings()
|
|
# If save was successful, _profiles_list_changed_overall and _current_profile_modified_in_form are reset
|
|
# So, we can proceed to close. If save failed, the user saw an error, they might choose to close or not.
|
|
elif response is None: # Cancel close
|
|
return
|
|
|
|
# If analysis is running (progress_dialog exists and is visible)
|
|
if self.progress_dialog and self.progress_dialog.winfo_exists():
|
|
logger.warning("Closing ProfileManagerWindow while symbol analysis dialog might be open. This should ideally be handled (e.g., by disabling close or stopping analysis).")
|
|
# For now, we'll just log it. A more robust solution might involve:
|
|
# 1. Disabling the close button of ProfileManagerWindow while analysis is running.
|
|
# 2. Prompting the user to stop the analysis.
|
|
# 3. Or, ensuring the analysis thread is properly managed on close.
|
|
|
|
self.parent_window.focus_set() # Return focus to main window
|
|
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"
|
|
# NEW: Add dump_on_every_hit to summary
|
|
dump_freq = "Always" if action.get("dump_on_every_hit", True) else "Once"
|
|
summary = (f"BP: {bp}{'...' if len(action.get('breakpoint_location', '')) > 30 else ''} "
|
|
f"(Vars:{num_vars}, Fmt:{fmt}, Cont:{cont}, Hit:{dump_freq})")
|
|
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
|
|
|
|
# Save current profile form data to internal list if modified, before opening action editor
|
|
if self._current_profile_modified_in_form:
|
|
if not self._save_current_form_to_profile_data(self._selected_profile_index):
|
|
return # Abort if save failed
|
|
|
|
current_profile = self._profiles_data[self._selected_profile_index]
|
|
current_profile_target_exe = current_profile.get("target_executable", "") # Get from saved data
|
|
current_profile_program_params = current_profile.get("program_parameters", "") # Get from saved data
|
|
symbol_analysis_data = current_profile.get("symbol_analysis")
|
|
|
|
editor = ActionEditorWindow(
|
|
self,
|
|
action_data=None, # For new action
|
|
is_new=True,
|
|
target_executable_path=current_profile_target_exe,
|
|
app_settings=self.app_settings,
|
|
symbol_analysis_data=symbol_analysis_data,
|
|
program_parameters_for_scope=current_profile_program_params # Pass program params
|
|
)
|
|
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"] = [] # Initialize if missing or malformed
|
|
current_profile["actions"].append(new_action_data)
|
|
self._profiles_list_changed_overall = True # Mark that overall list has changed
|
|
# self._current_profile_modified_in_form = True # This is more for the profile details form
|
|
self._populate_actions_listbox()
|
|
self.actions_listbox.selection_set(tk.END) # Select the new action
|
|
self._on_action_select_in_listbox() # Update button states
|
|
|
|
def _edit_action(self) -> None:
|
|
if self._selected_profile_index is None or self._selected_action_index_in_profile is None:
|
|
return
|
|
|
|
# Save current profile form data to internal list if modified
|
|
if self._current_profile_modified_in_form:
|
|
if not self._save_current_form_to_profile_data(self._selected_profile_index):
|
|
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)):
|
|
logger.error("Selected action index is out of bounds for the current profile's actions.")
|
|
return
|
|
|
|
action_to_edit = actions_list[self._selected_action_index_in_profile]
|
|
current_profile_target_exe = current_profile.get("target_executable", "")
|
|
current_profile_program_params = current_profile.get("program_parameters", "")
|
|
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,
|
|
program_parameters_for_scope=current_profile_program_params # Pass program params
|
|
)
|
|
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 # Preserve selection
|
|
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
|
|
|
|
# No need to save form here as removing an action doesn't depend on form values
|
|
# but on the selected action in the listbox.
|
|
|
|
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 # Modifying actions changes the profile
|
|
idx_to_reselect_after_delete = self._selected_action_index_in_profile
|
|
self._populate_actions_listbox() # Repopulates and resets selection
|
|
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() # Update button states based on new selection (or lack thereof)
|
|
else:
|
|
logger.error("Could not remove action: 'actions' list missing or index invalid for the current profile.")
|
|
|
|
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)
|
|
self.variables_count_var.set("Globals: N/A")
|
|
self.view_variables_button.config(state=tk.DISABLED)
|
|
self.types_count_var.set("Types: N/A")
|
|
self.view_types_button.config(state=tk.DISABLED)
|
|
self.sources_count_var.set("Sources: N/A")
|
|
self.view_sources_button.config(state=tk.DISABLED)
|
|
return
|
|
|
|
profile = self._profiles_data[self._selected_profile_index]
|
|
target_exe_in_form = self.target_exe_var.get() # Use current form value for checks
|
|
|
|
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
|
|
vars_count_text = "Globals: N/A"
|
|
view_vars_btn_state = tk.DISABLED
|
|
types_count_text = "Types: N/A"
|
|
view_types_btn_state = tk.DISABLED
|
|
sources_count_text = "Sources: N/A"
|
|
view_sources_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 # Can analyse if exe path is valid
|
|
|
|
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: # Target in form is a valid file
|
|
analysis_data = profile.get("symbol_analysis") # Use analysis data from the *profile object*
|
|
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
|
|
|
|
num_variables = symbols_dict.get("global_variables_count", 0)
|
|
vars_count_text = f"Globals: {num_variables}"
|
|
if num_variables > 0: view_vars_btn_state = tk.NORMAL
|
|
|
|
num_types = symbols_dict.get("types_count", 0)
|
|
types_count_text = f"Types: {num_types}"
|
|
if num_types > 0: view_types_btn_state = tk.NORMAL
|
|
|
|
num_sources = symbols_dict.get("source_files_count", 0)
|
|
sources_count_text = f"Sources: {num_sources}"
|
|
if num_sources > 0: view_sources_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 = file_utils.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"
|
|
# Analysis data is for a different exe, so disable view buttons for that old data
|
|
view_funcs_btn_state = tk.DISABLED
|
|
view_vars_btn_state = tk.DISABLED
|
|
view_types_btn_state = tk.DISABLED
|
|
view_sources_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 (checksum mismatch). RE-ANALYSIS REQUIRED."
|
|
status_color = "red"
|
|
view_funcs_btn_state = tk.DISABLED
|
|
view_vars_btn_state = tk.DISABLED
|
|
view_types_btn_state = tk.DISABLED
|
|
view_sources_btn_state = tk.DISABLED
|
|
else: # Checksum missing or calc failed
|
|
status_text += "Status unclear (checksum mismatch or unavailable). Consider re-analysing."
|
|
status_color = "orange red"
|
|
view_funcs_btn_state = tk.DISABLED
|
|
view_vars_btn_state = tk.DISABLED
|
|
view_types_btn_state = tk.DISABLED
|
|
view_sources_btn_state = tk.DISABLED
|
|
else: # No analysis data in profile
|
|
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)
|
|
self.variables_count_var.set(vars_count_text)
|
|
self.view_variables_button.config(state=view_vars_btn_state)
|
|
self.types_count_var.set(types_count_text)
|
|
self.view_types_button.config(state=view_types_btn_state)
|
|
self.sources_count_var.set(sources_count_text)
|
|
self.view_sources_button.config(state=view_sources_btn_state)
|
|
|
|
|
|
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() # Use value from the form
|
|
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() # Refresh 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): # Check if GDB is configured
|
|
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]
|
|
|
|
# Create and show progress dialog
|
|
self.progress_dialog = SymbolAnalysisProgressDialog(self)
|
|
|
|
symbol_analyzer = SymbolAnalyzer(gdb_exe_path, self.app_settings, gdb_session_class=GDBMISession)
|
|
|
|
# Run analysis in a separate thread
|
|
analysis_thread = threading.Thread(
|
|
target=self._perform_symbol_analysis_thread,
|
|
args=(profile_to_update, target_exe_for_analysis, symbol_analyzer, self.progress_dialog),
|
|
daemon=True
|
|
)
|
|
analysis_thread.start()
|
|
|
|
|
|
def _perform_symbol_analysis_thread(self, profile_to_update: Dict[str, Any],
|
|
target_exe_path: str, symbol_analyzer: SymbolAnalyzer,
|
|
progress_dialog: SymbolAnalysisProgressDialog):
|
|
|
|
analysis_data_dict: Dict[str, Any] = {}
|
|
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 symbol analysis for: {os.path.basename(target_exe_path)}")
|
|
gui_set_status(f"Analyzing {os.path.basename(target_exe_path)}...")
|
|
|
|
analysis_data_dict = symbol_analyzer.analyze(
|
|
target_exe_path=target_exe_path,
|
|
progress_callback=gui_log,
|
|
status_callback=gui_set_status
|
|
)
|
|
|
|
if analysis_data_dict: # Check if analyze returned a non-empty dict
|
|
analysis_succeeded_overall = True
|
|
gui_set_status("Symbol analysis successfully completed."); gui_log("\nSymbol analysis successfully completed.")
|
|
else:
|
|
gui_set_status("Symbol analysis failed. Check logs."); gui_log("\nSymbol analysis failed.")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error orchestrating symbol analysis thread for '{target_exe_path}': {e}", exc_info=True)
|
|
error_msg = f"THREAD ERROR during analysis: {type(e).__name__} - {e}"
|
|
gui_log(f"\n{error_msg}")
|
|
gui_set_status(error_msg)
|
|
finally:
|
|
# Schedule the final update on the GUI thread
|
|
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 # Analysis data is part of profile content
|
|
logger.info(f"Symbol analysis data updated for profile: '{profile_to_update.get('profile_name')}'.")
|
|
if self.winfo_exists(): # Check if window still exists
|
|
messagebox.showinfo("Analysis Complete", "Symbol analysis has finished successfully.", parent=self)
|
|
else:
|
|
# Don't clear old analysis data if new one failed, unless it was explicitly for a new target
|
|
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)
|
|
# Consider destroying it or just enabling close button if you want user to review logs in it
|
|
|
|
self._update_analysis_status_display() # Refresh the analysis status and counts
|
|
|
|
|
|
def _get_symbols_for_display(self, category: str) -> List[Union[str, Dict[str, Any]]]:
|
|
if self._selected_profile_index is None or \
|
|
not (0 <= self._selected_profile_index < len(self._profiles_data)):
|
|
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):
|
|
return []
|
|
|
|
return analysis_data["symbols"].get(category, [])
|
|
|
|
|
|
def _view_analyzed_functions(self) -> None:
|
|
functions_list = self._get_symbols_for_display("functions")
|
|
if not functions_list:
|
|
messagebox.showinfo("No Functions", "No functions found in the last analysis for this profile.", parent=self)
|
|
return
|
|
self._show_symbol_list_dialog("Functions", functions_list)
|
|
|
|
def _view_analyzed_variables(self) -> None:
|
|
variables_list = self._get_symbols_for_display("global_variables")
|
|
if not variables_list:
|
|
messagebox.showinfo("No Global Variables", "No global variables found in the last analysis for this profile.", parent=self)
|
|
return
|
|
self._show_symbol_list_dialog("Global Variables", variables_list)
|
|
|
|
def _view_analyzed_types(self) -> None:
|
|
types_list = self._get_symbols_for_display("types")
|
|
if not types_list:
|
|
messagebox.showinfo("No Types", "No types found in the last analysis for this profile.", parent=self)
|
|
return
|
|
self._show_symbol_list_dialog("Types", types_list)
|
|
|
|
def _view_analyzed_sources(self) -> None:
|
|
source_files_list = self._get_symbols_for_display("source_files")
|
|
if not source_files_list:
|
|
messagebox.showinfo("No Source Files", "No source files found in the last analysis for this profile.", parent=self)
|
|
return
|
|
self._show_symbol_list_dialog("Source Files", source_files_list)
|
|
|
|
def _show_symbol_list_dialog(self, symbol_type: str, symbols: List[Union[str, Dict[str, Any]]]) -> None:
|
|
"""Helper to show the SymbolListViewerDialog with appropriate title."""
|
|
if self._selected_profile_index is None : return # Should not happen if view button is enabled
|
|
|
|
target_exe_in_form = self.target_exe_var.get()
|
|
profile = self._profiles_data[self._selected_profile_index]
|
|
analysis_data = profile.get("symbol_analysis")
|
|
|
|
if not analysis_data: # Should also not happen if button enabled
|
|
messagebox.showerror("Error", "No analysis data available to view.", parent=self)
|
|
return
|
|
|
|
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 # Assume obsolete unless proven otherwise
|
|
if target_exe_in_form and os.path.isfile(target_exe_in_form) and \
|
|
os.path.normpath(analyzed_exe_path) == os.path.normpath(target_exe_in_form):
|
|
current_checksum = file_utils.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 data might be obsolete for current form target)" if is_obsolete else ""
|
|
dialog_title = f"Analyzed {symbol_type} for '{exe_name_for_title}'{title_suffix}"
|
|
|
|
SymbolListViewerDialog(self, symbols, title=dialog_title) |