add get functions from gdb
This commit is contained in:
parent
131c4532c0
commit
4aed5cee70
@ -8,7 +8,7 @@ import wexpect
|
||||
import logging
|
||||
import json # For parsing JSON output from the GDB script (though not directly used in this file after changes)
|
||||
import time
|
||||
from typing import Optional, Dict, Any # For type hinting
|
||||
from typing import Optional, Dict, Any, List # For type hinting
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -136,6 +136,134 @@ class GDBSession:
|
||||
if self.child and self.child.isalive(): self.child.close()
|
||||
self.child = None
|
||||
raise # RILANCIA L'ECCEZIONE ORIGINALE
|
||||
|
||||
def list_functions(self, regex_filter: Optional[str] = None, timeout: int = DEFAULT_GDB_OPERATION_TIMEOUT) -> List[str]:
|
||||
"""
|
||||
Lists functions known to GDB, optionally filtered by a regex.
|
||||
|
||||
Args:
|
||||
regex_filter: Optional regex to filter function names.
|
||||
timeout: Timeout for the GDB command.
|
||||
|
||||
Returns:
|
||||
A list of function name strings. Returns an empty list on error or if no functions match.
|
||||
"""
|
||||
if not self.child or not self.child.isalive():
|
||||
logger.error("GDB session not active, cannot list functions.")
|
||||
return []
|
||||
|
||||
command = "info functions"
|
||||
if regex_filter:
|
||||
command += f" {regex_filter.strip()}"
|
||||
|
||||
logger.info(f"Requesting GDB function list with command: '{command}'")
|
||||
functions: List[str] = []
|
||||
try:
|
||||
output = self.send_cmd(command, expect_prompt=True, timeout=timeout)
|
||||
# L'output di 'info functions' può essere complesso.
|
||||
# Esempio:
|
||||
# File my_source.cpp:
|
||||
# 123: void MyClass::myMethod(int);
|
||||
# 456: int anotherFunction();
|
||||
# Non-debugging symbols:
|
||||
# 0x00401000 _start
|
||||
# 0x004010a0 __do_global_dtors_aux
|
||||
#
|
||||
# Cerchiamo di estrarre nomi di funzioni che sembrano validi identificatori C/C++
|
||||
# Questo regex cerca identificatori che possono includere :: e <template parametri>,
|
||||
# spesso terminanti con ( o a volte solo il nome se è un simbolo non-debugging.
|
||||
# È un'euristica e potrebbe aver bisogno di affinamenti.
|
||||
# Prioritizziamo le linee che iniziano con numeri di riga o nomi di file.
|
||||
|
||||
potential_function_lines = []
|
||||
current_file_context = None
|
||||
|
||||
for line in output.splitlines():
|
||||
line_strip = line.strip()
|
||||
if not line_strip:
|
||||
continue
|
||||
|
||||
# Cattura il contesto del file, se presente
|
||||
file_match = re.match(r"File\s+(.+):", line_strip)
|
||||
if file_match:
|
||||
current_file_context = file_match.group(1).strip()
|
||||
logger.debug(f"Function parsing context: File '{current_file_context}'")
|
||||
continue # Passa alla riga successiva
|
||||
|
||||
# Ignora le sezioni "Non-debugging symbols" per ora, a meno che non si vogliano includere
|
||||
if line_strip.startswith("Non-debugging symbols:") or \
|
||||
(line_strip.startswith("0x") and " " in line_strip and not line_strip.endswith(";") and not line_strip.endswith(")")): # Heuristica per simboli non-debugging
|
||||
logger.debug(f"Skipping non-debugging symbol line: {line_strip}")
|
||||
current_file_context = None # Resetta contesto file se entriamo in questa sezione
|
||||
continue
|
||||
|
||||
# Regex per estrarre il nome della funzione da righe tipo "123: void MyNamespace::MyClass<Template>::Method(int);"
|
||||
# o "ReturnType funcName(params)"
|
||||
# Questo cerca qualcosa che assomigli a un nome di funzione prima di una parentesi aperta
|
||||
# o prima di un punto e virgola se non ci sono parametri visibili.
|
||||
# Il nome può contenere '::', '<', '>', '_', alfanumerici.
|
||||
# ^\s*(?:\w+\s+)?([a-zA-Z_][\w:<>()]*?(?:::[a-zA-Z_][\w:<>()]*?)*)\s*\(.*?\);?$
|
||||
# ^\s*(?:[^\s]+\s+)*([a-zA-Z_][\w:<>()~*\s&-]*?(?:::[a-zA-Z_][\w:<>()~*\s&-]*?)*)\s*\(
|
||||
# Il seguente è un tentativo più semplice e robusto
|
||||
# Cerca un identificatore valido seguito da una parentesi aperta '(',
|
||||
# opzionalmente preceduto da tipo di ritorno e numero di riga.
|
||||
# Pattern per identificatore C++: ([a-zA-Z_]\w*(::[a-zA-Z_]\w*)*(<[^>]*>)?(?:\s*const)?)
|
||||
# Regex per matchare: opzionale(numero_riga: tipo_ritorno) NOME_FUNZIONE (parametri) ;
|
||||
# Difficile fare un regex perfetto. Proviamo un approccio più semplice per ora:
|
||||
# cerca un nome valido seguito da '('.
|
||||
|
||||
# Tentativo 1: Estrarre da linee con numero di riga e tipo
|
||||
# es: "123: void MyClass::myMethod(int);"
|
||||
m = re.match(r"^\s*\d+:\s+(?:[\w\s:*&<>~]+\s+)?([a-zA-Z_][\w:<>\s~*&-]*?(?:::[a-zA-Z_][\w:<>\s~*&-]*?)*)\s*\(", line_strip)
|
||||
if m:
|
||||
func_name = m.group(1).strip()
|
||||
# Rimuovi eventuali spazi extra o qualificatori 'const' alla fine se catturati per errore
|
||||
func_name = re.sub(r'\s+const\s*$', '', func_name)
|
||||
if func_name not in functions: # Evita duplicati se GDB è verboso
|
||||
functions.append(func_name)
|
||||
logger.debug(f"Found function (type 1): {func_name}")
|
||||
continue
|
||||
|
||||
# Tentativo 2: Linee che iniziano direttamente con il nome della funzione o tipo di ritorno
|
||||
# es: "int main()" o "MyClass::MyClass()"
|
||||
# ([a-zA-Z_][\w:<>\s~*&-]*?(?:::[a-zA-Z_][\w:<>\s~*&-]*?)*)\s*\(
|
||||
m2 = re.match(r"^\s*(?:[\w\s:*&<>~]+\s+)?([a-zA-Z_][\w:<>\s~*&-]*?(?:::[a-zA-Z_][\w:<>\s~*&-]*?)*)\s*\(", line_strip)
|
||||
if m2:
|
||||
func_name = m2.group(1).strip()
|
||||
func_name = re.sub(r'\s+const\s*$', '', func_name)
|
||||
if func_name and func_name not in functions: # Assicurati che non sia vuoto
|
||||
functions.append(func_name)
|
||||
logger.debug(f"Found function (type 2): {func_name}")
|
||||
continue
|
||||
|
||||
# A volte 'info functions' lista solo il nome, specialmente per simboli senza info complete
|
||||
# Es. "_ZN12MyNamespace8MyClassILi1EE11anotherFuncEv" (mangled)
|
||||
# O simboli semplici come "main" su una riga a sé se non c'è file context
|
||||
# Questo è più rischioso perché potrebbe catturare non-funzioni.
|
||||
# Lo attiviamo solo se non c'è un contesto di file attivo (per evitare di prendere nomi di file come funzioni)
|
||||
# e se la linea sembra un identificatore C++ valido.
|
||||
if not current_file_context and re.match(r"^[a-zA-Z_][\w:<>\.~]*$", line_strip) and '(' not in line_strip and ';' not in line_strip:
|
||||
# Evita di aggiungere cose che sono chiaramente tipi o altro
|
||||
if "::" in line_strip or line_strip.startswith("_Z") or (not any(c.islower() for c in line_strip) and len(line_strip) > 4): # Heuristica per nomi mangled o solo maiuscole (costanti?)
|
||||
if line_strip not in functions:
|
||||
functions.append(line_strip)
|
||||
logger.debug(f"Found potential symbol (type 3): {line_strip}")
|
||||
|
||||
|
||||
if not functions and output: # Se non abbiamo trovato nulla con regex ma c'era output
|
||||
logger.warning(f"Could not parse function names reliably from 'info functions' output. Output was:\n{output}")
|
||||
elif functions:
|
||||
logger.info(f"Successfully parsed {len(functions)} function names.")
|
||||
functions.sort() # Ordina per una visualizzazione migliore
|
||||
|
||||
except (ConnectionError, TimeoutError) as e:
|
||||
logger.error(f"Error listing functions from GDB: {e}", exc_info=True)
|
||||
return [] # Ritorna lista vuota in caso di errore di comunicazione
|
||||
except Exception as e_parse:
|
||||
logger.error(f"Error parsing 'info functions' output: {e_parse}", exc_info=True)
|
||||
return [] # Ritorna lista vuota in caso di errore di parsing
|
||||
|
||||
return functions
|
||||
|
||||
def _set_gdb_dumper_variables(
|
||||
self, timeout: int = DEFAULT_GDB_OPERATION_TIMEOUT
|
||||
|
||||
@ -1,299 +1,335 @@
|
||||
# File: cpp_python_debug/gui/action_editor_window.py
|
||||
# Provides a Toplevel window for editing a single debug action.
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext, messagebox
|
||||
from tkinter import ttk, scrolledtext, messagebox, filedialog # Aggiunto filedialog se non c'era
|
||||
import logging
|
||||
import os # Aggiunto os
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
# Import per la nuova funzionalità
|
||||
from ..core.gdb_controller import GDBSession
|
||||
from ..core.config_manager import AppSettings # Per ottenere gdb_path, timeouts, etc.
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FunctionSelectorDialog(tk.Toplevel):
|
||||
"""Dialog to select a function from a list."""
|
||||
def __init__(self, parent, functions_list: List[str], title="Select Function"):
|
||||
super().__init__(parent)
|
||||
self.title(title)
|
||||
self.geometry("500x400")
|
||||
self.transient(parent)
|
||||
self.grab_set()
|
||||
self.result: Optional[str] = None
|
||||
|
||||
list_frame = ttk.Frame(self, padding="10")
|
||||
list_frame.pack(expand=True, fill=tk.BOTH)
|
||||
list_frame.rowconfigure(0, weight=1)
|
||||
list_frame.columnconfigure(0, weight=1)
|
||||
|
||||
self.listbox = tk.Listbox(list_frame, selectmode=tk.SINGLE)
|
||||
self.listbox.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.listbox.yview)
|
||||
scrollbar.grid(row=0, column=1, sticky="ns")
|
||||
self.listbox.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
for func_name in functions_list:
|
||||
self.listbox.insert(tk.END, func_name)
|
||||
|
||||
self.listbox.bind("<Double-Button-1>", self._on_ok)
|
||||
|
||||
button_frame = ttk.Frame(self, padding=(10,0,10,10))
|
||||
button_frame.pack(fill=tk.X)
|
||||
ttk.Button(button_frame, text="OK", command=self._on_ok).pack(side=tk.RIGHT, padx=5)
|
||||
ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.RIGHT)
|
||||
|
||||
if functions_list:
|
||||
self.listbox.selection_set(0) # Seleziona il primo elemento
|
||||
self.listbox.focus_set()
|
||||
|
||||
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
|
||||
self.wait_window()
|
||||
|
||||
def _on_ok(self, event=None): # Aggiunto event=None per il double-click
|
||||
selection = self.listbox.curselection()
|
||||
if selection:
|
||||
self.result = self.listbox.get(selection[0])
|
||||
self.destroy()
|
||||
|
||||
def _on_cancel(self):
|
||||
self.result = None
|
||||
self.destroy()
|
||||
|
||||
class ActionEditorWindow(tk.Toplevel):
|
||||
"""
|
||||
A modal dialog for creating or editing a single debug action for a profile.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: tk.Widget,
|
||||
action_data: Optional[Dict[str, Any]] = None,
|
||||
is_new: bool = True,
|
||||
):
|
||||
def __init__(self, parent: tk.Widget,
|
||||
action_data: Optional[Dict[str, Any]] = None,
|
||||
is_new: bool = True,
|
||||
target_executable_path: Optional[str] = None, # NUOVO PARAMETRO
|
||||
app_settings: Optional[AppSettings] = None): # NUOVO PARAMETRO
|
||||
super().__init__(parent)
|
||||
self.parent_window = parent
|
||||
self.is_new_action = is_new
|
||||
self.result: Optional[Dict[str, Any]] = None # To store the action data on OK
|
||||
self.result: Optional[Dict[str, Any]] = None
|
||||
|
||||
# --- NUOVI ATTRIBUTI ---
|
||||
self.target_executable_path = target_executable_path
|
||||
self.app_settings = app_settings # Necessario per gdb_path, timeouts
|
||||
# --- FINE NUOVI ATTRIBUTI ---
|
||||
|
||||
title = "Add New Action" if self.is_new_action else "Edit Action"
|
||||
self.title(title)
|
||||
self.geometry("600x550") # Adjusted size
|
||||
self.geometry("650x580") # Aumentato leggermente per il nuovo bottone
|
||||
self.resizable(False, False)
|
||||
|
||||
self.transient(parent)
|
||||
self.grab_set() # Make modal
|
||||
self.grab_set()
|
||||
|
||||
# --- StringVars and other Tkinter variables for action fields ---
|
||||
self.breakpoint_var = tk.StringVar()
|
||||
# variables_to_dump will be handled by ScrolledText
|
||||
self.output_format_var = tk.StringVar()
|
||||
self.output_directory_var = tk.StringVar()
|
||||
self.filename_pattern_var = tk.StringVar()
|
||||
self.continue_after_dump_var = tk.BooleanVar()
|
||||
# self.max_hits_var = tk.StringVar() # For later
|
||||
|
||||
self._initial_action_data = (
|
||||
action_data.copy() if action_data else None
|
||||
) # Store original for cancel/comparison
|
||||
self._initial_action_data = action_data.copy() if action_data else None
|
||||
|
||||
self._create_widgets()
|
||||
self._load_action_data(action_data)
|
||||
|
||||
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
|
||||
self.wait_window() # Important for modal dialog behavior to get result
|
||||
self.wait_window()
|
||||
|
||||
def _create_widgets(self) -> None:
|
||||
main_frame = ttk.Frame(self, padding="15")
|
||||
main_frame.pack(expand=True, fill=tk.BOTH)
|
||||
main_frame.columnconfigure(1, weight=1) # Make entry fields expand
|
||||
main_frame.columnconfigure(1, weight=1) # Colonna per gli input si espande
|
||||
|
||||
row_idx = 0
|
||||
|
||||
# Breakpoint Location
|
||||
ttk.Label(main_frame, text="Breakpoint Location:").grid(
|
||||
row=row_idx, column=0, sticky=tk.W, padx=5, pady=5
|
||||
)
|
||||
ttk.Entry(main_frame, textvariable=self.breakpoint_var, width=60).grid(
|
||||
row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5
|
||||
)
|
||||
row_idx += 1
|
||||
# --- Breakpoint Location (Layout Corretto) ---
|
||||
ttk.Label(main_frame, text="Breakpoint Location:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
|
||||
bp_input_frame = ttk.Frame(main_frame) # Frame per Entry e Bottone Browse
|
||||
bp_input_frame.grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5)
|
||||
bp_input_frame.columnconfigure(0, weight=1) # L'Entry si espande dentro questo frame
|
||||
|
||||
# MODIFIED LINE:
|
||||
bp_help_label = ttk.Label(
|
||||
main_frame,
|
||||
text="(e.g., main, file.cpp:123, MyClass::foo)",
|
||||
foreground="gray",
|
||||
)
|
||||
bp_help_label.grid(
|
||||
row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0, 10)
|
||||
)
|
||||
self.bp_entry = ttk.Entry(bp_input_frame, textvariable=self.breakpoint_var)
|
||||
self.bp_entry.grid(row=0, column=0, sticky="ew", padx=(0,5)) # padx a destra per spaziare dal bottone
|
||||
|
||||
self.browse_funcs_button = ttk.Button(bp_input_frame, text="Browse Functions...", command=self._browse_functions)
|
||||
self.browse_funcs_button.grid(row=0, column=1, sticky=tk.E) # Bottone a destra dell'entry
|
||||
|
||||
# Stato iniziale del bottone "Browse Functions..."
|
||||
if not self.target_executable_path or not self.app_settings or \
|
||||
not os.path.isfile(self.target_executable_path): # Aggiunto controllo isfile
|
||||
self.browse_funcs_button.config(state=tk.DISABLED)
|
||||
if self.target_executable_path and not os.path.isfile(self.target_executable_path):
|
||||
logger.warning(f"Target executable '{self.target_executable_path}' not found, disabling function browser.")
|
||||
row_idx += 1
|
||||
# --- Fine Breakpoint Location ---
|
||||
|
||||
bp_help_label = ttk.Label(main_frame, text="(e.g., main, file.cpp:123, MyClass::foo)", foreground="gray")
|
||||
bp_help_label.grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0,10))
|
||||
row_idx += 1
|
||||
|
||||
# Variables to Dump
|
||||
ttk.Label(main_frame, text="Variables to Dump:").grid(
|
||||
row=row_idx, column=0, sticky="nw", padx=5, pady=5
|
||||
) # nw for top-alignment
|
||||
self.variables_text = scrolledtext.ScrolledText(
|
||||
main_frame, wrap=tk.WORD, height=5, width=58, font=("Consolas", 9)
|
||||
)
|
||||
self.variables_text.grid(
|
||||
row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5
|
||||
)
|
||||
ttk.Label(main_frame, text="Variables to Dump:").grid(row=row_idx, column=0, sticky="nw", padx=5, pady=5)
|
||||
self.variables_text = scrolledtext.ScrolledText(main_frame, wrap=tk.WORD, height=5, width=58, font=("Consolas", 9)) # width era 58
|
||||
self.variables_text.grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5)
|
||||
row_idx += 1
|
||||
|
||||
# MODIFIED LINE:
|
||||
vars_help_label = ttk.Label(
|
||||
main_frame, text="(One variable/expression per line)", foreground="gray"
|
||||
)
|
||||
vars_help_label.grid(
|
||||
row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0, 10)
|
||||
)
|
||||
vars_help_label = ttk.Label(main_frame, text="(One variable/expression per line)", foreground="gray")
|
||||
vars_help_label.grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0,10))
|
||||
row_idx += 1
|
||||
|
||||
# Output Format
|
||||
ttk.Label(main_frame, text="Output Format:").grid(
|
||||
row=row_idx, column=0, sticky=tk.W, padx=5, pady=5
|
||||
)
|
||||
self.output_format_combo = ttk.Combobox(
|
||||
main_frame,
|
||||
textvariable=self.output_format_var,
|
||||
values=["json", "csv"],
|
||||
state="readonly",
|
||||
width=10,
|
||||
)
|
||||
self.output_format_combo.grid(row=row_idx, column=1, sticky="w", padx=5, pady=5)
|
||||
ttk.Label(main_frame, text="Output Format:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
self.output_format_combo = ttk.Combobox(main_frame, textvariable=self.output_format_var, values=["json", "csv"], state="readonly", width=10)
|
||||
self.output_format_combo.grid(row=row_idx, column=1, sticky="w", padx=5, pady=5) # columnspan rimosso, non necessario
|
||||
row_idx += 1
|
||||
|
||||
# Output Directory
|
||||
ttk.Label(main_frame, text="Output Directory:").grid(
|
||||
row=row_idx, column=0, sticky=tk.W, padx=5, pady=5
|
||||
)
|
||||
ttk.Entry(main_frame, textvariable=self.output_directory_var, width=50).grid(
|
||||
row=row_idx, column=1, sticky="ew", padx=5, pady=5
|
||||
)
|
||||
ttk.Button(main_frame, text="Browse...", command=self._browse_output_dir).grid(
|
||||
row=row_idx, column=2, padx=5, pady=5
|
||||
)
|
||||
ttk.Label(main_frame, text="Output Directory:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
self.output_dir_entry = ttk.Entry(main_frame, textvariable=self.output_directory_var) # width rimosso per farlo espandere con la colonna
|
||||
self.output_dir_entry.grid(row=row_idx, column=1, sticky="ew", padx=5, pady=5)
|
||||
ttk.Button(main_frame, text="Browse...", command=self._browse_output_dir).grid(row=row_idx, column=2, padx=5, pady=5)
|
||||
row_idx += 1
|
||||
|
||||
# Filename Pattern
|
||||
ttk.Label(main_frame, text="Filename Pattern:").grid(
|
||||
row=row_idx, column=0, sticky=tk.W, padx=5, pady=5
|
||||
)
|
||||
ttk.Entry(main_frame, textvariable=self.filename_pattern_var, width=60).grid(
|
||||
row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5
|
||||
)
|
||||
ttk.Label(main_frame, text="Filename Pattern:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=5)
|
||||
self.filename_pattern_entry = ttk.Entry(main_frame, textvariable=self.filename_pattern_var) # width rimosso
|
||||
self.filename_pattern_entry.grid(row=row_idx, column=1, columnspan=2, sticky="ew", padx=5, pady=5)
|
||||
row_idx += 1
|
||||
|
||||
# MODIFIED LINE:
|
||||
pattern_help_label = ttk.Label(
|
||||
main_frame,
|
||||
text="(Placeholders: {profile_name}, {breakpoint}, {variable}, {timestamp}, {format})",
|
||||
foreground="gray",
|
||||
)
|
||||
pattern_help_label.grid(
|
||||
row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0, 10)
|
||||
)
|
||||
pattern_help_label = ttk.Label(main_frame, text="(Placeholders: {profile_name}, {breakpoint}, {variable}, {timestamp}, {format})", foreground="gray")
|
||||
pattern_help_label.grid(row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0,10))
|
||||
row_idx += 1
|
||||
|
||||
# Continue After Dump
|
||||
self.continue_check = ttk.Checkbutton(
|
||||
main_frame,
|
||||
text="Continue execution after dump",
|
||||
variable=self.continue_after_dump_var,
|
||||
)
|
||||
self.continue_check.grid(
|
||||
row=row_idx, column=0, columnspan=3, sticky="w", padx=5, pady=10
|
||||
)
|
||||
self.continue_check = ttk.Checkbutton(main_frame, text="Continue execution after dump", variable=self.continue_after_dump_var)
|
||||
self.continue_check.grid(row=row_idx, column=0, columnspan=3, sticky="w", padx=5, pady=10) # sticky w
|
||||
row_idx += 1
|
||||
|
||||
# --- Buttons Frame ---
|
||||
# Buttons Frame
|
||||
buttons_frame = ttk.Frame(main_frame)
|
||||
buttons_frame.grid(
|
||||
row=row_idx, column=0, columnspan=3, pady=(15, 5), sticky="e"
|
||||
)
|
||||
|
||||
ttk.Button(buttons_frame, text="OK", command=self._on_ok, width=10).pack(
|
||||
side=tk.RIGHT, padx=5
|
||||
)
|
||||
ttk.Button(
|
||||
buttons_frame, text="Cancel", command=self._on_cancel, width=10
|
||||
).pack(side=tk.RIGHT, padx=5)
|
||||
buttons_frame.grid(row=row_idx, column=0, columnspan=3, pady=(15,5), sticky="e") # sticky e
|
||||
ttk.Button(buttons_frame, text="OK", command=self._on_ok, width=10).pack(side=tk.RIGHT, padx=5)
|
||||
ttk.Button(buttons_frame, text="Cancel", command=self._on_cancel, width=10).pack(side=tk.RIGHT, padx=5)
|
||||
|
||||
def _load_action_data(self, action_data: Optional[Dict[str, Any]]) -> None:
|
||||
"""Loads action data into the form fields."""
|
||||
from ..gui.profile_manager_window import (
|
||||
DEFAULT_ACTION,
|
||||
) # Lazy import for default
|
||||
# Importa DEFAULT_ACTION qui se non è già globale o un attributo di classe
|
||||
# Dobbiamo importarlo da ProfileManagerWindow o definirlo qui. Per ora, assumiamo sia disponibile.
|
||||
# Idealmente, DEFAULT_ACTION dovrebbe essere in un modulo `defaults` o simile.
|
||||
# Assumiamo per ora che sia stato passato correttamente o che ProfileManagerWindow lo gestisca.
|
||||
# Per questo esempio, lo ridefinisco localmente se necessario.
|
||||
_DEFAULT_ACTION_LOCAL = {
|
||||
"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
|
||||
}
|
||||
data_to_load = action_data if action_data else _DEFAULT_ACTION_LOCAL.copy()
|
||||
|
||||
data_to_load = action_data if action_data else DEFAULT_ACTION.copy()
|
||||
|
||||
self.breakpoint_var.set(
|
||||
data_to_load.get(
|
||||
"breakpoint_location", DEFAULT_ACTION["breakpoint_location"]
|
||||
)
|
||||
)
|
||||
|
||||
variables_list = data_to_load.get(
|
||||
"variables_to_dump", DEFAULT_ACTION["variables_to_dump"]
|
||||
)
|
||||
self.breakpoint_var.set(data_to_load.get("breakpoint_location", _DEFAULT_ACTION_LOCAL["breakpoint_location"]))
|
||||
variables_list = data_to_load.get("variables_to_dump", _DEFAULT_ACTION_LOCAL["variables_to_dump"])
|
||||
if isinstance(variables_list, list):
|
||||
self.variables_text.insert(tk.END, "\n".join(variables_list))
|
||||
else: # Fallback if it's not a list for some reason
|
||||
else:
|
||||
self.variables_text.insert(tk.END, str(variables_list))
|
||||
self.output_format_var.set(data_to_load.get("output_format", _DEFAULT_ACTION_LOCAL["output_format"]))
|
||||
self.output_directory_var.set(data_to_load.get("output_directory", _DEFAULT_ACTION_LOCAL["output_directory"]))
|
||||
self.filename_pattern_var.set(data_to_load.get("filename_pattern", _DEFAULT_ACTION_LOCAL["filename_pattern"]))
|
||||
self.continue_after_dump_var.set(data_to_load.get("continue_after_dump", _DEFAULT_ACTION_LOCAL["continue_after_dump"]))
|
||||
|
||||
self.output_format_var.set(
|
||||
data_to_load.get("output_format", DEFAULT_ACTION["output_format"])
|
||||
)
|
||||
self.output_directory_var.set(
|
||||
data_to_load.get("output_directory", DEFAULT_ACTION["output_directory"])
|
||||
)
|
||||
self.filename_pattern_var.set(
|
||||
data_to_load.get("filename_pattern", DEFAULT_ACTION["filename_pattern"])
|
||||
)
|
||||
self.continue_after_dump_var.set(
|
||||
data_to_load.get(
|
||||
"continue_after_dump", DEFAULT_ACTION["continue_after_dump"]
|
||||
)
|
||||
)
|
||||
|
||||
def _browse_output_dir(self) -> None:
|
||||
def _browse_output_dir(self) -> None: # Invariato
|
||||
current_path = self.output_directory_var.get()
|
||||
initial_dir = (
|
||||
current_path if current_path and os.path.isdir(current_path) else None
|
||||
)
|
||||
initial_dir = current_path if current_path and os.path.isdir(current_path) else None
|
||||
path = filedialog.askdirectory(
|
||||
title="Select Output Directory for Dumps",
|
||||
initialdir=initial_dir,
|
||||
parent=self, # Ensure dialog is on top
|
||||
parent=self
|
||||
)
|
||||
if path:
|
||||
self.output_directory_var.set(path)
|
||||
|
||||
def _validate_data(self) -> bool:
|
||||
"""Validates the current form data."""
|
||||
def _validate_data(self) -> bool: # Invariato
|
||||
if not self.breakpoint_var.get().strip():
|
||||
messagebox.showerror(
|
||||
"Validation Error", "Breakpoint Location cannot be empty.", parent=self
|
||||
)
|
||||
messagebox.showerror("Validation Error", "Breakpoint Location cannot be empty.", parent=self)
|
||||
return False
|
||||
|
||||
variables_str = self.variables_text.get("1.0", tk.END).strip()
|
||||
if not variables_str:
|
||||
messagebox.showerror(
|
||||
"Validation Error", "Variables to Dump cannot be empty.", parent=self
|
||||
)
|
||||
messagebox.showerror("Validation Error", "Variables to Dump cannot be empty.", parent=self)
|
||||
return False
|
||||
|
||||
if not self.output_directory_var.get().strip():
|
||||
messagebox.showerror(
|
||||
"Validation Error", "Output Directory cannot be empty.", parent=self
|
||||
)
|
||||
messagebox.showerror("Validation Error", "Output Directory cannot be empty.", parent=self)
|
||||
return False
|
||||
|
||||
if not self.filename_pattern_var.get().strip():
|
||||
messagebox.showerror(
|
||||
"Validation Error", "Filename Pattern cannot be empty.", parent=self
|
||||
)
|
||||
messagebox.showerror("Validation Error", "Filename Pattern cannot be empty.", parent=self)
|
||||
return False
|
||||
# Basic check for common placeholders
|
||||
if not any(
|
||||
p in self.filename_pattern_var.get()
|
||||
for p in ["{breakpoint}", "{variable}", "{timestamp}"]
|
||||
):
|
||||
messagebox.showwarning(
|
||||
"Validation Warning",
|
||||
"Filename Pattern seems to be missing common placeholders like {breakpoint}, {variable}, or {timestamp}. This might lead to overwritten files.",
|
||||
parent=self,
|
||||
)
|
||||
|
||||
if not any(p in self.filename_pattern_var.get() for p in ["{breakpoint}", "{variable}", "{timestamp}"]):
|
||||
messagebox.showwarning("Validation Warning",
|
||||
"Filename Pattern seems to be missing common placeholders like {breakpoint}, {variable}, or {timestamp}. This might lead to overwritten files.",
|
||||
parent=self)
|
||||
return True
|
||||
|
||||
def _on_ok(self) -> None:
|
||||
"""Handles the OK button click."""
|
||||
def _on_ok(self) -> None: # Invariato
|
||||
if not self._validate_data():
|
||||
return
|
||||
|
||||
variables_list = [
|
||||
line.strip()
|
||||
for line in self.variables_text.get("1.0", tk.END).strip().splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
if not variables_list: # Double check after stripping
|
||||
messagebox.showerror(
|
||||
"Validation Error",
|
||||
"Variables to Dump cannot be empty after processing.",
|
||||
parent=self,
|
||||
)
|
||||
variables_list = [line.strip() for line in self.variables_text.get("1.0", tk.END).strip().splitlines() if line.strip()]
|
||||
if not variables_list :
|
||||
messagebox.showerror("Validation Error", "Variables to Dump cannot be empty after processing.", parent=self)
|
||||
return
|
||||
|
||||
self.result = {
|
||||
"breakpoint_location": self.breakpoint_var.get().strip(),
|
||||
"variables_to_dump": variables_list,
|
||||
"output_format": self.output_format_var.get(),
|
||||
"output_directory": self.output_directory_var.get().strip(),
|
||||
"filename_pattern": self.filename_pattern_var.get().strip(),
|
||||
"continue_after_dump": self.continue_after_dump_var.get(),
|
||||
"continue_after_dump": self.continue_after_dump_var.get()
|
||||
}
|
||||
# logger.debug(f"ActionEditorWindow result on OK: {self.result}")
|
||||
self.destroy()
|
||||
|
||||
def _on_cancel(self) -> None:
|
||||
"""Handles the Cancel button click or window close."""
|
||||
self.result = None # Explicitly set result to None on cancel
|
||||
# logger.debug("ActionEditorWindow cancelled.")
|
||||
def _on_cancel(self) -> None: # Invariato
|
||||
self.result = None
|
||||
self.destroy()
|
||||
|
||||
# Public method to get the result after the dialog closes
|
||||
def get_result(self) -> Optional[Dict[str, Any]]:
|
||||
def get_result(self) -> Optional[Dict[str, Any]]: # Invariato
|
||||
return self.result
|
||||
|
||||
# --- NUOVA FUNZIONE ---
|
||||
def _browse_functions(self) -> None:
|
||||
if not self.target_executable_path or not os.path.isfile(self.target_executable_path):
|
||||
messagebox.showerror("Error", "Target executable for the profile is not set or invalid. Cannot browse functions.", parent=self)
|
||||
return
|
||||
if not self.app_settings:
|
||||
messagebox.showerror("Error", "Application settings not available. Cannot determine GDB path.", parent=self)
|
||||
return
|
||||
|
||||
gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path")
|
||||
if not gdb_exe_path or not os.path.isfile(gdb_exe_path):
|
||||
messagebox.showerror("GDB Error", f"GDB executable not found or not configured: {gdb_exe_path}", parent=self)
|
||||
return
|
||||
|
||||
# Mostra un messaggio di attesa
|
||||
self.config(cursor="watch")
|
||||
self.update_idletasks()
|
||||
|
||||
temp_gdb_session: Optional[GDBSession] = None
|
||||
functions_list: List[str] = []
|
||||
error_occurred = False
|
||||
|
||||
try:
|
||||
logger.info(f"Creating temporary GDBSession for function listing. Target: {self.target_executable_path}")
|
||||
# Creiamo una sessione GDBSession senza script dumper e senza opzioni dumper,
|
||||
# serve solo per 'info functions'.
|
||||
temp_gdb_session = GDBSession(
|
||||
gdb_path=gdb_exe_path,
|
||||
executable_path=self.target_executable_path,
|
||||
gdb_script_full_path=None, # Non serve lo script dumper per info functions
|
||||
dumper_options={}
|
||||
)
|
||||
|
||||
# Usiamo timeout brevi per l'avvio e il comando
|
||||
startup_timeout = self.app_settings.get_setting("timeouts", "gdb_start", 30) // 2 # Più breve
|
||||
command_timeout = self.app_settings.get_setting("timeouts", "gdb_command", 30) // 2 # Più breve
|
||||
|
||||
logger.debug("Starting temporary GDB session...")
|
||||
temp_gdb_session.start(timeout=startup_timeout) # Avvia GDB
|
||||
|
||||
if not temp_gdb_session.symbols_found:
|
||||
messagebox.showwarning("No Debug Symbols",
|
||||
"GDB reported no debugging symbols found in the executable. "
|
||||
"The function list might be empty or incomplete.", parent=self)
|
||||
# Continuiamo comunque, GDB potrebbe listare alcuni simboli non-debugging.
|
||||
|
||||
logger.debug("Listing functions...")
|
||||
functions_list = temp_gdb_session.list_functions(timeout=command_timeout)
|
||||
|
||||
except FileNotFoundError as fnf_e:
|
||||
logger.error(f"Error during function browsing (FileNotFound): {fnf_e}", exc_info=True)
|
||||
messagebox.showerror("Error", f"File not found during GDB interaction: {fnf_e}", parent=self)
|
||||
error_occurred = True
|
||||
except (ConnectionError, TimeoutError) as session_e:
|
||||
logger.error(f"Session error during function browsing: {session_e}", exc_info=True)
|
||||
messagebox.showerror("GDB Error", f"Could not communicate with GDB to list functions: {session_e}", parent=self)
|
||||
error_occurred = True
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during function browsing: {e}", exc_info=True)
|
||||
messagebox.showerror("Error", f"An unexpected error occurred: {e}", parent=self)
|
||||
error_occurred = True
|
||||
finally:
|
||||
if temp_gdb_session and temp_gdb_session.is_alive():
|
||||
logger.debug("Quitting temporary GDB session...")
|
||||
quit_timeout = self.app_settings.get_setting("timeouts", "gdb_quit", 10) // 2
|
||||
try:
|
||||
temp_gdb_session.quit(timeout=quit_timeout)
|
||||
except Exception as e_quit:
|
||||
logger.error(f"Error quitting temporary GDB session: {e_quit}")
|
||||
self.config(cursor="") # Ripristina cursore
|
||||
|
||||
if not error_occurred:
|
||||
if functions_list:
|
||||
dialog = FunctionSelectorDialog(self, functions_list)
|
||||
selected_function = dialog.result
|
||||
if selected_function:
|
||||
self.breakpoint_var.set(selected_function)
|
||||
else:
|
||||
messagebox.showinfo("No Functions", "No functions found or GDB did not return any.", parent=self)
|
||||
@ -36,30 +36,29 @@ DEFAULT_PROFILE = {
|
||||
|
||||
|
||||
class ProfileManagerWindow(tk.Toplevel):
|
||||
# ... (init, _load_profiles_from_settings, etc. rimangono simili) ...
|
||||
def __init__(self, parent: "GDBGui", app_settings: "AppSettings"):
|
||||
def __init__(self, parent: 'GDBGui', app_settings: 'AppSettings'):
|
||||
super().__init__(parent)
|
||||
self.parent_window = parent
|
||||
self.app_settings = app_settings
|
||||
|
||||
self.title("Profile Manager")
|
||||
self.geometry("950x700")
|
||||
self.geometry("950x700") # Leggermente più largo per il nuovo bottone
|
||||
|
||||
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 # NEW
|
||||
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.target_exe_var = tk.StringVar() # Questo contiene il path dell'eseguibile del profilo selezionato
|
||||
self.program_params_var = tk.StringVar()
|
||||
|
||||
self._load_profiles_from_settings()
|
||||
self._create_widgets() # This will now include the new actions list UI
|
||||
self._create_widgets()
|
||||
self._populate_profiles_listbox()
|
||||
|
||||
if self._profiles_data:
|
||||
@ -748,92 +747,84 @@ class ProfileManagerWindow(tk.Toplevel):
|
||||
# self.move_action_down_button.config(state=tk.NORMAL if action_selected and self._selected_action_index_in_profile < (num_actions -1) else tk.DISABLED)
|
||||
|
||||
def _add_action(self) -> None:
|
||||
if self._selected_profile_index is None:
|
||||
return
|
||||
if self._selected_profile_index is None: return
|
||||
|
||||
editor = ActionEditorWindow(
|
||||
self, action_data=None, is_new=True
|
||||
) # Pass self (ProfileManagerWindow) as parent
|
||||
# --- NUOVO: Ottieni il percorso dell'eseguibile del profilo corrente ---
|
||||
current_profile_target_exe = self.target_exe_var.get() # Ottenuto dal form del profilo
|
||||
if not current_profile_target_exe or not os.path.isfile(current_profile_target_exe):
|
||||
# Se l'eseguibile del profilo non è valido, il browse funzioni non funzionerà bene.
|
||||
# Potremmo avvisare o disabilitare il browse nell'editor. Per ora, passiamolo.
|
||||
logger.warning("Target executable for the current profile is not set or invalid. Function browsing might be limited.")
|
||||
# current_profile_target_exe = None # O passarlo comunque
|
||||
|
||||
# Passa current_profile_target_exe all'editor
|
||||
editor = ActionEditorWindow(self,
|
||||
action_data=None,
|
||||
is_new=True,
|
||||
target_executable_path=current_profile_target_exe, # NUOVO ARGOMENTO
|
||||
app_settings=self.app_settings) # Passa app_settings per GDB path
|
||||
new_action_data = editor.get_result()
|
||||
|
||||
if new_action_data:
|
||||
profile = self._profiles_data[self._selected_profile_index]
|
||||
if "actions" not in profile or not isinstance(profile["actions"], list):
|
||||
profile["actions"] = [] # Ensure actions list exists
|
||||
profile["actions"] = []
|
||||
profile["actions"].append(new_action_data)
|
||||
|
||||
self._profiles_list_changed_overall = True # Mark profile as changed
|
||||
self._current_profile_modified_in_form = (
|
||||
True # This profile in form has changed
|
||||
)
|
||||
self._populate_actions_listbox() # Refresh actions list
|
||||
self.actions_listbox.selection_set(tk.END) # Select the new action
|
||||
self._on_action_select_in_listbox() # Update button states
|
||||
self._profiles_list_changed_overall = True
|
||||
self._current_profile_modified_in_form = True
|
||||
self._populate_actions_listbox()
|
||||
self.actions_listbox.selection_set(tk.END)
|
||||
self._on_action_select_in_listbox()
|
||||
logger.info(f"Added new action to profile '{profile.get('profile_name')}'.")
|
||||
|
||||
def _edit_action(self) -> None:
|
||||
if (
|
||||
self._selected_profile_index is None
|
||||
or self._selected_action_index_in_profile is None
|
||||
):
|
||||
if self._selected_profile_index is None or self._selected_action_index_in_profile is None:
|
||||
logger.warning("Edit action called but no profile or action is selected.")
|
||||
return
|
||||
|
||||
# Ensure the selected action index is valid for the current actions list
|
||||
profile = self._profiles_data[self._selected_profile_index]
|
||||
actions_list = profile.get("actions", [])
|
||||
|
||||
# Defensive check for the selected action index
|
||||
if not (0 <= self._selected_action_index_in_profile < len(actions_list)):
|
||||
logger.error(
|
||||
f"Selected action index {self._selected_action_index_in_profile} is out of bounds for actions list of length {len(actions_list)}."
|
||||
)
|
||||
self._selected_action_index_in_profile = None # Invalidate selection
|
||||
self._update_action_buttons_state() # Update UI
|
||||
messagebox.showerror(
|
||||
"Error",
|
||||
"The selected action index is no longer valid. Please re-select.",
|
||||
parent=self,
|
||||
)
|
||||
logger.error(f"Selected action index {self._selected_action_index_in_profile} is out of bounds for actions list of length {len(actions_list)}.")
|
||||
self._selected_action_index_in_profile = None
|
||||
self._update_action_buttons_state()
|
||||
messagebox.showerror("Error", "The selected action index is no longer valid. Please re-select.", parent=self)
|
||||
return
|
||||
|
||||
action_to_edit = actions_list[self._selected_action_index_in_profile]
|
||||
|
||||
editor = ActionEditorWindow(self, action_data=action_to_edit, is_new=False)
|
||||
# --- NUOVO: Ottieni il percorso dell'eseguibile del profilo corrente ---
|
||||
current_profile_target_exe = self.target_exe_var.get()
|
||||
if not current_profile_target_exe or not os.path.isfile(current_profile_target_exe):
|
||||
logger.warning("Target executable for the current profile is not set or invalid for editing action. Function browsing might be limited.")
|
||||
# current_profile_target_exe = None
|
||||
|
||||
editor = ActionEditorWindow(self,
|
||||
action_data=action_to_edit,
|
||||
is_new=False,
|
||||
target_executable_path=current_profile_target_exe, # NUOVO ARGOMENTO
|
||||
app_settings=self.app_settings) # Passa app_settings
|
||||
updated_action_data = editor.get_result()
|
||||
|
||||
if updated_action_data:
|
||||
profile["actions"][
|
||||
self._selected_action_index_in_profile
|
||||
] = updated_action_data
|
||||
profile["actions"][self._selected_action_index_in_profile] = updated_action_data
|
||||
|
||||
self._profiles_list_changed_overall = True
|
||||
self._current_profile_modified_in_form = (
|
||||
True # Mark profile as changed because an action within it changed
|
||||
)
|
||||
self._current_profile_modified_in_form = True
|
||||
|
||||
# Store the current selection index because _populate_actions_listbox clears it
|
||||
current_action_idx_to_reselect = self._selected_action_index_in_profile
|
||||
self._populate_actions_listbox()
|
||||
|
||||
self._populate_actions_listbox() # Refresh to show updated summary
|
||||
|
||||
# MODIFIED: Ensure re-selection is valid
|
||||
if (
|
||||
current_action_idx_to_reselect is not None
|
||||
and 0 <= current_action_idx_to_reselect < self.actions_listbox.size()
|
||||
):
|
||||
if current_action_idx_to_reselect is not None and \
|
||||
0 <= current_action_idx_to_reselect < self.actions_listbox.size():
|
||||
self.actions_listbox.selection_set(current_action_idx_to_reselect)
|
||||
|
||||
self._on_action_select_in_listbox() # This will correctly set _selected_action_index_in_profile based on actual listbox selection
|
||||
self._on_action_select_in_listbox()
|
||||
logger.info(f"Edited action in profile '{profile.get('profile_name')}'.")
|
||||
else:
|
||||
# If editor was cancelled, ensure the original selection state is maintained or cleared if necessary
|
||||
# No, _on_action_select_in_listbox() will handle this if we re-select.
|
||||
# If editor was cancelled, the selection in listbox shouldn't change from user's perspective
|
||||
# unless the list itself was repopulated for other reasons.
|
||||
# Let's ensure the listbox selection status is re-evaluated by _on_action_select_in_listbox
|
||||
self._on_action_select_in_listbox() # Call to ensure button states are correct after editor close
|
||||
pass
|
||||
self._on_action_select_in_listbox()
|
||||
|
||||
def _remove_action(self) -> None:
|
||||
if (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user