fix parameters to analize function

This commit is contained in:
VALLONGOL 2025-05-27 09:37:39 +02:00
parent 433d19a8f0
commit ae194c8fe9
5 changed files with 237 additions and 275 deletions

View File

@ -3,8 +3,8 @@ import logging
import os
import re
import time
import json
import wexpect # NEW: Import wexpect
import json # Not directly used for parsing here, but good for context
import wexpect
from typing import Dict, List, Optional, Any, Callable
from .gdb_mi_session import GDBMISession
@ -42,19 +42,16 @@ class GDBInteractiveInspector:
or a string like '[name="var1",name="var2"]' (e.g., from print-values=0).
"""
names = []
if isinstance(payload_value, list):
# Handles cases like locals=[{name="var1"}, {name="var2"}]
if isinstance(payload_value, list): # Handles cases like locals=[{name="var1"}, {name="var2"}]
for item in payload_value:
if isinstance(item, dict) and "name" in item:
names.append(item["name"])
elif isinstance(payload_value, str):
# Handles format like 'locals=[name="var1",name="var2"]' or 'locals=[name="var1"]'
# Regex to find name="value" pairs within the string representation of the list
elif isinstance(payload_value, str): # Handles format like 'locals=[name="var1",name="var2"]'
logger.debug(f"Parsing string payload for var names: {payload_value}")
for match in re.finditer(r'name="((?:[^"\\]|\\.)*?)"', payload_value):
var_name = match.group(1).replace('\\\\', '\\').replace('\\"', '"')
names.append(var_name)
return list(set(names)) # Return unique names
return list(set(names))
def get_variables_in_scope(
@ -66,15 +63,6 @@ class GDBInteractiveInspector:
) -> Dict[str, List[str]]:
"""
Starts GDB, runs to a breakpoint, and lists locals and arguments.
Args:
target_executable: Path to the executable.
breakpoint_location: Location for the breakpoint (e.g., "main", "file.cpp:123").
program_args: Arguments to pass to the program.
status_callback: Optional callback for status updates.
Returns:
A dictionary like {"locals": ["var1", "var2"], "args": ["arg1"]} or empty lists on error.
"""
if status_callback is None:
status_callback = lambda msg: logger.info(f"Inspector Status: {msg}")
@ -101,7 +89,7 @@ class GDBInteractiveInspector:
status_callback(f"Setting breakpoint at: {breakpoint_location}")
bp_output = self._mi_session.send_mi_cmd(f"-break-insert {breakpoint_location}", timeout=cmd_timeout)
bp_mi_result = self._mi_session._parse_mi_result(bp_output) # Use the session's parser
bp_mi_result = self._mi_session._parse_mi_result(bp_output)
if not (bp_mi_result.get("result_type") == "^done" and "bkpt" in bp_mi_result.get("payload", {})):
status_callback(f"Error: Could not set or parse breakpoint at '{breakpoint_location}'. GDB output:\n{bp_output}")
@ -110,15 +98,29 @@ class GDBInteractiveInspector:
return result
logger.info(f"Breakpoint set successfully. MI Response: {bp_mi_result}")
# --- MODIFIED COMMAND EXECUTION ---
if program_args and program_args.strip():
set_args_command = f"-exec-arguments {program_args.strip()}"
logger.info(f"Setting program arguments with MI command: [{set_args_command}]")
args_set_output = self._mi_session.send_mi_cmd(set_args_command, timeout=cmd_timeout)
args_set_mi_result = self._mi_session._parse_mi_result(args_set_output)
if args_set_mi_result.get("result_type") != "^done":
status_callback(f"Error: Could not set program arguments. GDB output:\n{args_set_output}")
logger.error(f"Setting program arguments failed. Raw: {args_set_output}, Parsed: {args_set_mi_result}")
self._cleanup_session()
return result
logger.info(f"Program arguments set successfully. MI Response: {args_set_mi_result}")
status_callback(f"Running program with args: '{program_args}' to hit breakpoint...")
exec_command = f"-exec-run --thread-group i1 {program_args}".strip()
if self._mi_session.child: # Ensure child is not None
status_callback(f"Running program to hit breakpoint...")
exec_command = "-exec-run --thread-group i1"
logger.info(f"Constructed GDB exec command: [{exec_command}]")
# --- END MODIFIED COMMAND EXECUTION ---
if self._mi_session.child:
self._mi_session.child.sendline(exec_command + "\n")
else:
raise ConnectionError("GDB child process not available before -exec-run.")
stop_event_timeout = run_timeout
start_wait_time = time.time()
stopped_output_buffer = ""
@ -128,7 +130,7 @@ class GDBInteractiveInspector:
while time.time() - start_wait_time < stop_event_timeout:
if not self._mi_session or not self._mi_session.child:
logger.error("GDB session or child became None during run loop.")
program_exited = True # Treat as program exit to stop loop
program_exited = True
break
try:
chunk = self._mi_session.child.read_nonblocking(size=1024)
@ -153,14 +155,12 @@ class GDBInteractiveInspector:
status_callback("Program exited (EOF detected).")
logger.warning(f"EOF detected during program run. Output buffer:\n{stopped_output_buffer}")
break
except Exception as e_read: # Catch other potential read errors
except Exception as e_read:
status_callback(f"Error reading GDB output: {e_read}")
logger.error(f"Error reading GDB output during run: {e_read}", exc_info=True)
# Decide if this is fatal for the inspection
program_exited = True # Assume we cannot proceed
program_exited = True
break
if not breakpoint_hit and not program_exited:
status_callback(f"Timeout ({stop_event_timeout}s) waiting for breakpoint or program exit.")
logger.warning(f"Timeout waiting for breakpoint. Output buffer:\n{stopped_output_buffer}")
@ -172,23 +172,18 @@ class GDBInteractiveInspector:
return result
if breakpoint_hit:
if not self._mi_session or not self._mi_session.child: # Check again
if not self._mi_session or not self._mi_session.child:
logger.error("GDB session or child became None before fetching locals/args.")
self._cleanup_session()
return result
try:
# Ensure GDB is ready by expecting the prompt. This also consumes the *stopped output.
logger.debug(f"Attempting to expect prompt after breakpoint hit. Buffer before expect: {self._mi_session.child.buffer}")
self._mi_session.child.expect_exact(self._mi_session.mi_prompt, timeout=5) # Increased timeout slightly
self._mi_session.child.expect_exact(self._mi_session.mi_prompt, timeout=5)
logger.debug(f"Prompt received after breakpoint hit. Before prompt: {self._mi_session.child.before}")
except (wexpect.TIMEOUT, wexpect.EOF) as e_prompt:
logger.warning(f"Did not receive prompt immediately after breakpoint hit ({e_prompt}), proceeding cautiously.")
# The *stopped event might have already been consumed by read_nonblocking loop.
# We can still try to send commands.
status_callback("Fetching local variables...")
# Use print-values=0 for names only, which is simpler for MI parsing.
locals_output = self._mi_session.send_mi_cmd("-stack-list-locals --thread 1 --frame 0 0", timeout=cmd_timeout)
mi_locals_result = self._mi_session._parse_mi_result(locals_output)
logger.debug(f"Raw MI Locals Output: {locals_output}")
@ -200,7 +195,6 @@ class GDBInteractiveInspector:
result["locals"] = self._parse_mi_var_list_payload(locals_payload)
logger.info(f"Found locals: {result['locals']}")
status_callback("Fetching arguments...")
args_output = self._mi_session.send_mi_cmd("-stack-list-arguments --thread 1 --frame 0 0", timeout=cmd_timeout)
mi_args_result = self._mi_session._parse_mi_result(args_output)
@ -209,9 +203,8 @@ class GDBInteractiveInspector:
if mi_args_result.get("result_type") == "^done":
stack_args_payload = mi_args_result.get("payload", {}).get("stack-args")
# stack-args payload is typically a list of frames, e.g., [{level="0", args=[{name="arg1"}, ...]}]
if isinstance(stack_args_payload, list) and stack_args_payload:
frame_args_list_or_string = stack_args_payload[0].get("args") # Args are usually in the first frame
frame_args_list_or_string = stack_args_payload[0].get("args")
if frame_args_list_or_string is not None:
result["args"] = self._parse_mi_var_list_payload(frame_args_list_or_string)
logger.info(f"Found args: {result['args']}")
@ -220,13 +213,13 @@ class GDBInteractiveInspector:
except FileNotFoundError as e:
status_callback(f"Error: {e}")
logger.error(f"Inspector FileNotFoundError: {e}", exc_info=True)
except (ConnectionError, TimeoutError) as e: # This will catch our raised ConnectionError/TimeoutError
except (ConnectionError, TimeoutError) as e:
status_callback(f"GDB Session Error: {e}")
logger.error(f"Inspector GDB Session Error: {e}", exc_info=True)
except wexpect.TIMEOUT as e_wexpect_timeout: # Catch wexpect specific timeout if not caught by expect_exact
except wexpect.TIMEOUT as e_wexpect_timeout:
status_callback(f"GDB Communication Timeout: {e_wexpect_timeout}")
logger.error(f"Inspector wexpect.TIMEOUT: {e_wexpect_timeout}", exc_info=True)
except wexpect.EOF as e_wexpect_eof: # Catch wexpect specific EOF
except wexpect.EOF as e_wexpect_eof:
status_callback(f"GDB Communication EOF: {e_wexpect_eof}")
logger.error(f"Inspector wexpect.EOF: {e_wexpect_eof}", exc_info=True)
except Exception as e:
@ -238,7 +231,6 @@ class GDBInteractiveInspector:
return result
# Placeholder for test_action_validity - to be implemented later
def test_action_validity(
self,
target_executable: str,

View File

@ -59,7 +59,7 @@ class ProfileManagerWindow(tk.Toplevel):
self._profiles_data: List[Dict[str, Any]] = []
self._selected_profile_index: Optional[int] = None
self._selected_action_index_in_profile: Optional[int] = None # MODIFIED: Initialized to 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
@ -74,9 +74,9 @@ class ProfileManagerWindow(tk.Toplevel):
# StringVars per i conteggi
self.functions_count_var = tk.StringVar(value="Functions: N/A")
self.variables_count_var = tk.StringVar(value="Globals: N/A") # NEW
self.types_count_var = tk.StringVar(value="Types: N/A") # NEW
self.sources_count_var = tk.StringVar(value="Sources: N/A") # NEW
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()
@ -153,8 +153,8 @@ class ProfileManagerWindow(tk.Toplevel):
right_pane.rowconfigure(0, weight=0)
right_pane.rowconfigure(1, weight=0)
right_pane.rowconfigure(2, weight=0)
right_pane.rowconfigure(3, weight=0) # NEW: added row for symbol counts before debug actions
right_pane.rowconfigure(4, weight=1) # Debug Actions now at row 4
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")
@ -199,27 +199,23 @@ class ProfileManagerWindow(tk.Toplevel):
self.view_functions_button.grid(row=row_s, column=1, padx=(10,5), pady=2, sticky="w")
row_s += 1
# NEW: Global Variables Count and View Button
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
# NEW: Types Count and View Button
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
# NEW: Source Files Count and View Button
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) # Row changed to 4
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)
@ -244,7 +240,7 @@ class ProfileManagerWindow(tk.Toplevel):
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)) # Row changed to 1
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)
@ -284,7 +280,7 @@ class ProfileManagerWindow(tk.Toplevel):
self.profiles_listbox.selection_set(self._selected_profile_index)
self.profiles_listbox.activate(self._selected_profile_index)
return
elif response is None:
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)
@ -333,7 +329,6 @@ class ProfileManagerWindow(tk.Toplevel):
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("")
@ -363,7 +358,7 @@ class ProfileManagerWindow(tk.Toplevel):
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", "*.*")],
filetypes=[("Executable files", ("*.exe", "*")), ("All files", "*.*")], # For cross-platform
initialdir=initial_dir,
parent=self
)
@ -381,6 +376,7 @@ class ProfileManagerWindow(tk.Toplevel):
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)
@ -393,19 +389,22 @@ class ProfileManagerWindow(tk.Toplevel):
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
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()
logger.info(f"Profile '{profile_name}' (index {profile_index}) basic details updated.")
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",
@ -414,12 +413,13 @@ class ProfileManagerWindow(tk.Toplevel):
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 # Save failed, abort new profile creation
elif response is None: # User cancelled
return
new_p = json.loads(json.dumps(DEFAULT_PROFILE))
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
@ -432,38 +432,42 @@ class ProfileManagerWindow(tk.Toplevel):
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._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()
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._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
return # Save failed, abort duplication
original_profile = self._profiles_data[self._selected_profile_index]
duplicated_profile = json.loads(json.dumps(original_profile))
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}"
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)
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
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")
@ -476,64 +480,82 @@ class ProfileManagerWindow(tk.Toplevel):
self._profiles_list_changed_overall = True
new_list_size = len(self._profiles_data)
self._selected_profile_index = None
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:
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} has an empty name.", parent=self)
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}'.", parent=self)
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):
messagebox.showerror("Data Error", f"Profile '{name}' has malformed actions.", parent=self)
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):
messagebox.showerror("Data Error", f"Profile '{name}', action {idx_a+1} is malformed.", parent=self)
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
self._profiles_list_changed_overall = False
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 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"
@ -541,27 +563,33 @@ class ProfileManagerWindow(tk.Toplevel):
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:
if response is True: # Save
self._save_all_profiles_to_settings()
elif response is None:
# 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.")
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()
self.parent_window.focus_set() # Return focus to main window
self.destroy()
# --- Gestione Azioni (Listbox e Bottoni Azione) ---
def _populate_actions_listbox(self) -> None:
self.actions_listbox.delete(0, tk.END)
self._selected_action_index_in_profile = None
self._selected_action_index_in_profile = None # Reset selection
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]
bp = action.get("breakpoint_location", "N/A")[:30] # Truncate for display
num_vars = len(action.get("variables_to_dump", []))
fmt = action.get("output_format", "N/A")
cont = "Yes" if action.get("continue_after_dump", False) else "No"
@ -580,158 +608,179 @@ class ProfileManagerWindow(tk.Toplevel):
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
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 = self.target_exe_var.get()
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, is_new=True,
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
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"] = []
current_profile["actions"] = [] # Initialize if missing or malformed
current_profile["actions"].append(new_action_data)
self._profiles_list_changed_overall = True
self._current_profile_modified_in_form = True
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)
self._on_action_select_in_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
current_profile = self._profiles_data[self._selected_profile_index]
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)): return
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 = self.target_exe_var.get()
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,
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
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
# 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
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
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
# 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()
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()
else: logger.error("Could not remove action: 'actions' list missing or index invalid.")
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.")
# --- 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)
# Reset all counts and view buttons
self.functions_count_var.set("Functions: N/A")
self.view_functions_button.config(state=tk.DISABLED)
self.variables_count_var.set("Globals: N/A") # NEW
self.view_variables_button.config(state=tk.DISABLED) # NEW
self.types_count_var.set("Types: N/A") # NEW
self.view_types_button.config(state=tk.DISABLED) # NEW
self.sources_count_var.set("Sources: N/A") # NEW
self.view_sources_button.config(state=tk.DISABLED) # NEW
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()
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"
# Initialize counts and button states
funcs_count_text = "Functions: N/A"
view_funcs_btn_state = tk.DISABLED
vars_count_text = "Globals: N/A" # NEW
view_vars_btn_state = tk.DISABLED # NEW
types_count_text = "Types: N/A" # NEW
view_types_btn_state = tk.DISABLED # NEW
sources_count_text = "Sources: N/A" # NEW
view_sources_btn_state = tk.DISABLED # NEW
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
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:
analysis_data = profile.get("symbol_analysis")
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", {})
# Functions
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
if num_functions > 0: view_funcs_btn_state = tk.NORMAL
# Global Variables (NEW)
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
if num_variables > 0: view_vars_btn_state = tk.NORMAL
# Types (NEW)
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
if num_types > 0: view_types_btn_state = tk.NORMAL
# Source Files (NEW)
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
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")
@ -749,7 +798,7 @@ class ProfileManagerWindow(tk.Toplevel):
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"
# If target changed, disable viewing old data, force re-analysis.
# 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
@ -758,20 +807,20 @@ class ProfileManagerWindow(tk.Toplevel):
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_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:
status_text += "Status unclear. Consider re-analysing."
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:
else: # No analysis data in profile
status_text += "Not performed. Click 'Analyse' to generate."
status_color = "blue"
@ -782,12 +831,12 @@ class ProfileManagerWindow(tk.Toplevel):
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) # NEW
self.view_variables_button.config(state=view_vars_btn_state) # NEW
self.types_count_var.set(types_count_text) # NEW
self.view_types_button.config(state=view_types_btn_state) # NEW
self.sources_count_var.set(sources_count_text) # NEW
self.view_sources_button.config(state=view_sources_btn_state) # NEW
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:
@ -795,24 +844,25 @@ class ProfileManagerWindow(tk.Toplevel):
messagebox.showerror("Error", "No profile selected.", parent=self)
return
target_exe_for_analysis = self.target_exe_var.get()
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()
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):
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)
# MODIFIED: Pass GDBMISession class for symbol analysis
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),
@ -820,6 +870,7 @@ class ProfileManagerWindow(tk.Toplevel):
)
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):
@ -845,7 +896,7 @@ class ProfileManagerWindow(tk.Toplevel):
status_callback=gui_set_status
)
if analysis_data_dict:
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:
@ -857,6 +908,7 @@ class ProfileManagerWindow(tk.Toplevel):
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],
@ -866,22 +918,24 @@ class ProfileManagerWindow(tk.Toplevel):
if success:
profile_to_update["symbol_analysis"] = analysis_data
self._profiles_list_changed_overall = True
self._current_profile_modified_in_form = 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():
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()
self._update_analysis_status_display() # Refresh the analysis status and counts
# --- NEW: Methods to view other symbol lists ---
def _get_symbols_for_display(self, category: str) -> List[Union[str, Dict[str, Any]]]: # Modified return type hint
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 []
@ -923,23 +977,30 @@ class ProfileManagerWindow(tk.Toplevel):
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: # Modified symbols type hint
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] # Should be valid here
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
if os.path.normpath(analyzed_exe_path) == os.path.normpath(target_exe_in_form):
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 might be obsolete)" if is_obsolete else ""
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)

0
sim_log.txt Normal file
View File

102
temp.md
View File

@ -1,102 +0,0 @@
Assolutamente! Creare un manuale utente completo è un'ottima idea per rendere il tuo software accessibile.
Data la complessità e la lunghezza di un manuale completo, lo genererò in sezioni. Inizierò con la versione **inglese**, seguita dalla versione **italiana**.
Ecco la struttura che seguirò per entrambe le lingue:
**Manuale Utente: Cpp-Python GDB Debug Helper**
1. **Introduzione**
* Cos'è Cpp-Python GDB Debug Helper?
* A chi è rivolto?
* Caratteristiche principali
2. **Requisiti di Sistema e Configurazione Ambiente**
* Sistemi Operativi Supportati
* Python
* Librerie Python Richieste
* Installazione di GDB
* Compilazione dell'Applicazione C/C++ Target
* Installazione dello Strumento (Cpp-Python GDB Debug Helper)
3. **Guida Rapida (Quick Start)**
* Avvio dell'Applicazione
* Configurazione Iniziale (Percorso GDB)
* Prima Sessione di Debug Manuale
4. **Panoramica dell'Interfaccia Utente (Finestra Principale)**
* Barra dei Menu
* Area Stato Configurazione Critica
* Pannello Modalità (Tab Debug Manuale, Tab Esecuzione Profilo Automatizzato)
* Area Output e Log (Output Raw GDB, Output JSON Parsato, Log Applicazione)
* Barra di Stato
5. **Finestra di Configurazione (`Options > Configure Application...`)**
* Tab "Paths" (Percorsi)
* Tab "Timeouts"
* Tab "Dumper Options" (Opzioni Dumper)
* Salvataggio e Annullamento
6. **Modalità Debug Manuale**
* Impostare Eseguibile Target e Parametri
* Impostare Locazione Breakpoint
* Impostare Variabile/Espressione da Dumpare
* Pulsanti di Controllo Sessione (Start GDB, Set BP, Run/Continue, Dump Var, Stop GDB)
* Interpretare l'Output Raw GDB
* Interpretare l'Output JSON Parsato
* Salvare i Dati Dumpati (JSON/CSV)
7. **Gestore Profili (`Profiles > Manage Profiles...`)**
* Panoramica della Finestra Gestore Profili
* Gestione dei Profili (Nuovo, Duplica, Elimina, Salva Tutti)
* Modifica Dettagli Profilo (Nome, Eseguibile, Parametri)
* Analisi dei Simboli (Scopo, Esecuzione, Interpretazione Stato, Visualizzazione)
* Gestione Azioni di Debug (Aggiungi, Modifica, Rimuovi)
8. **Finestra Editor Azioni (da Gestore Profili)**
* Locazione Breakpoint (con pulsanti di aiuto)
* Variabili da Dumpare (con pulsanti di aiuto, inclusa ispezione scope live)
* Formato Output (JSON/CSV)
* Directory Output
* Pattern Nome File (spiegazione placeholder)
* Opzione "Continue after dump"
* Salvataggio e Annullamento Azione
9. **Modalità Esecuzione Profilo Automatizzato**
* Selezione di un Profilo
* Esecuzione del Profilo
* Monitoraggio Stato Esecuzione e Barra di Progresso
* Interruzione di un Profilo in Esecuzione
* Interpretare il "Log File Prodotti"
* Aprire la Cartella di Output
10. **Risoluzione Problemi / FAQ**
* GDB non trovato / Problemi script dumper
* "No debugging symbols found"
* Errori dump JSON / `<gdb_tool_error>`
* Esecuzione profilo fallisce o si blocca
* GUI non risponde
* Come ottenere più informazioni di debug da `gdb_dumper_debug.log`
11. **Casi d'Uso / Esempi**
* Dumpare un `std::vector` specifico all'ingresso di una funzione.
* Tracciare il valore di una variabile globale attraverso multipli breakpoint.
* Salvare automaticamente snapshot di una struttura dati complessa.
12. **Avanzato: Lo Script `gdb_dumper.py`**
* Ruolo e interazione con GDB.
* Posizione del file di log.
13. **Appendice (Opzionale)**
* Placeholder per i nomi dei file.
Cominciamo con la versione inglese.
Ottimo! Proseguiamo con le sezioni 9, 10 e 11 del manuale in inglese.
---
Okay, here are the final sections (12 and an optional 13) for the English user manual.
---
---
This concludes the English version of the user manual. It's quite extensive!
Now, I will proceed to generate the **Italian version** of this manual, following the same structure. This will also be a multi-turn process.
Pronto per iniziare con la versione italiana?

11
temp.py Normal file
View File

@ -0,0 +1,11 @@
```
--- END OF FILE symbol_analyzer.py ---
**Note Finali sull'Interruzione:**
* **`GDBMISession.send_mi_cmd` e `GDBController.send_cmd`:** Per rendere l'interruzione veramente efficace *durante* un comando GDB lungo (come `info functions` su un eseguibile enorme), questi metodi dovrebbero essere modificati per controllare `stop_event` in modo più granulare, ad esempio nei loro loop di lettura dell'output. Attualmente, `SymbolAnalyzer` interrompe solo *tra* le chiamate ai metodi di GDB (es. tra `list_functions` e `list_global_variables`). L'interruzione dentro `GDBInteractiveInspector` è un po' più reattiva perché ha un loop di attesa del breakpoint.
* **Chiusura Finestra Progresso:** La logica in `SymbolAnalysisProgressDialog` ora tenta di segnalare l'interruzione al thread di lavoro. Il thread di lavoro (in `ActionEditorWindow` o `ProfileManagerWindow`) deve quindi passare questo segnale (`stop_event`) ai metodi di `GDBInteractiveInspector` o `SymbolAnalyzer`.
* **Robustezza:** La gestione dell'interruzione di processi esterni e thread può essere complessa. Testa attentamente gli scenari di interruzione.
Questi moduli dovrebbero ora contenere le modifiche discusse. Ricontrollali attentamente nel contesto del tuo progetto completo.