651 lines
35 KiB
Python
651 lines
35 KiB
Python
# File: cpp_python_debug/gui/main_window.py
|
|
# Provides the Tkinter GUI for interacting with the GDB session.
|
|
|
|
import tkinter as tk
|
|
from tkinter import filedialog, messagebox, ttk, scrolledtext
|
|
import logging
|
|
import os # For path manipulation
|
|
import json # For pretty-printing JSON in the GUI
|
|
import appdirs
|
|
|
|
# Relative imports for modules within the same package
|
|
from ..core.gdb_controller import GDBSession
|
|
from ..core.output_formatter import save_to_json, save_to_csv
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
CONFIG_FILE_NAME = "gdb_debug_gui_settings.json"
|
|
|
|
class GDBGui(tk.Tk):
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
self.gui_log_handler = None
|
|
|
|
self.title("GDB Debug GUI (Python API Enhanced)")
|
|
self.geometry("850x700") # Increased size for better layout
|
|
|
|
self.gdb_session = None
|
|
self.last_dumped_data = None # Stores the result from dump_variable_to_json
|
|
self.program_started_once = False # Tracks if 'run' has been called
|
|
|
|
# Tkinter StringVars for input fields
|
|
self.gdb_path_var = tk.StringVar()
|
|
self.exe_path_var = tk.StringVar()
|
|
self.gdb_script_path_var = tk.StringVar() # For the path to gdb_dumper.py
|
|
self.breakpoint_var = tk.StringVar(value="main") # Default breakpoint
|
|
self.variable_var = tk.StringVar()
|
|
self.params_var = tk.StringVar() # For program parameters
|
|
|
|
# Usiamo appdirs per trovare un percorso appropriato per i dati dell'utente
|
|
# Questo è cross-platform.
|
|
self.config_dir = appdirs.user_config_dir("GDBDebugGui", "CppPythonDebug") # Appname, Appauthor
|
|
if not os.path.exists(self.config_dir):
|
|
try:
|
|
os.makedirs(self.config_dir, exist_ok=True)
|
|
except OSError as e:
|
|
logger.error(f"Could not create config directory {self.config_dir}: {e}")
|
|
# Fallback to current directory if user config dir fails (less ideal)
|
|
self.config_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
self.config_filepath = os.path.join(self.config_dir, CONFIG_FILE_NAME)
|
|
logger.info(f"Settings will be loaded/saved from: {self.config_filepath}")
|
|
|
|
|
|
self._find_default_gdb_dumper_script() # Questo imposta un default se non ci sono settings
|
|
self._load_settings() # Carica le impostazioni *dopo* i default iniziali
|
|
|
|
self._find_default_gdb_dumper_script()
|
|
self._create_widgets()
|
|
self._setup_logging_redirect_to_gui()
|
|
|
|
self.protocol("WM_DELETE_WINDOW", self._on_closing_window) # Handle window close button
|
|
|
|
def _load_settings(self):
|
|
"""Loads settings from the JSON configuration file."""
|
|
logger.info(f"Attempting to load settings from {self.config_filepath}")
|
|
if not os.path.exists(self.config_filepath):
|
|
logger.info("Settings file not found. Using default/initial values.")
|
|
return
|
|
|
|
try:
|
|
with open(self.config_filepath, 'r', encoding='utf-8') as f:
|
|
settings = json.load(f)
|
|
|
|
self.gdb_path_var.set(settings.get("gdb_path", self.gdb_path_var.get()))
|
|
self.exe_path_var.set(settings.get("exe_path", self.exe_path_var.get()))
|
|
# Per gdb_script_path, diamo priorità al valore salvato rispetto a quello auto-rilevato,
|
|
# ma se è vuoto nel file di settings e _find_default... ne ha trovato uno, manteniamo quello auto-rilevato.
|
|
saved_script_path = settings.get("gdb_script_path")
|
|
if saved_script_path: # Se c'è un valore salvato, usalo
|
|
self.gdb_script_path_var.set(saved_script_path)
|
|
elif not self.gdb_script_path_var.get(): # Se non c'è salvato e non c'è auto-rilevato, lascialo vuoto o prendi dal settings se è lì
|
|
self.gdb_script_path_var.set(settings.get("gdb_script_path", ""))
|
|
|
|
|
|
self.breakpoint_var.set(settings.get("breakpoint", self.breakpoint_var.get()))
|
|
self.variable_var.set(settings.get("variable", self.variable_var.get()))
|
|
self.params_var.set(settings.get("params", self.params_var.get()))
|
|
# Potremmo anche salvare/caricare le dimensioni/posizione della finestra
|
|
# window_geometry = settings.get("window_geometry")
|
|
# if window_geometry:
|
|
# try: self.geometry(window_geometry)
|
|
# except tk.TclError: logger.warning(f"Invalid window geometry in settings: {window_geometry}")
|
|
|
|
logger.info("Settings loaded successfully.")
|
|
except json.JSONDecodeError:
|
|
logger.error(f"Error decoding JSON from settings file: {self.config_filepath}. Using defaults.", exc_info=True)
|
|
except Exception as e:
|
|
logger.error(f"Failed to load settings from {self.config_filepath}: {e}. Using defaults.", exc_info=True)
|
|
|
|
|
|
def _save_settings(self):
|
|
"""Saves current settings to the JSON configuration file."""
|
|
logger.info(f"Attempting to save settings to {self.config_filepath}")
|
|
settings = {
|
|
"gdb_path": self.gdb_path_var.get(),
|
|
"exe_path": self.exe_path_var.get(),
|
|
"gdb_script_path": self.gdb_script_path_var.get(),
|
|
"breakpoint": self.breakpoint_var.get(),
|
|
"variable": self.variable_var.get(),
|
|
"params": self.params_var.get(),
|
|
# "window_geometry": self.geometry() # Salva dimensioni/posizione finestra
|
|
}
|
|
try:
|
|
# Assicurati che la directory di configurazione esista (potrebbe essere la prima esecuzione)
|
|
if not os.path.exists(self.config_dir):
|
|
os.makedirs(self.config_dir, exist_ok=True)
|
|
|
|
with open(self.config_filepath, 'w', encoding='utf-8') as f:
|
|
json.dump(settings, f, indent=4) # indent=4 per leggibilità
|
|
logger.info("Settings saved successfully.")
|
|
except Exception as e:
|
|
logger.error(f"Failed to save settings to {self.config_filepath}: {e}", exc_info=True)
|
|
# Opzionalmente, informa l'utente con un messagebox se il salvataggio fallisce
|
|
# messagebox.showwarning("Settings Error", f"Could not save settings to:\n{self.config_filepath}\n\nError: {e}")
|
|
|
|
|
|
def _find_default_gdb_dumper_script(self):
|
|
"""
|
|
Tries to find the gdb_dumper.py script relative to this package's structure.
|
|
Sets self.gdb_script_path_var if found.
|
|
"""
|
|
try:
|
|
# Assumes gdb_dumper.py is in cpp_python_debug/core/gdb_dumper.py
|
|
# __file__ is gui/main_window.py
|
|
# So, ../core/gdb_dumper.py from here
|
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
script_path = os.path.join(current_dir, "..", "core", "gdb_dumper.py")
|
|
if os.path.exists(script_path):
|
|
self.gdb_script_path_var.set(os.path.abspath(script_path))
|
|
logger.info(f"Found default GDB dumper script at: {script_path}")
|
|
else:
|
|
logger.warning(f"Default GDB dumper script not found at expected location: {script_path}")
|
|
except Exception as e:
|
|
logger.error(f"Error trying to find default GDB dumper script: {e}", exc_info=True)
|
|
|
|
|
|
def _create_widgets(self):
|
|
"""Creates and lays out the GUI widgets."""
|
|
main_frame = ttk.Frame(self, padding="10")
|
|
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
|
self.columnconfigure(0, weight=1)
|
|
self.rowconfigure(0, weight=1) # Allow main_frame to expand
|
|
|
|
# --- Configuration Frame ---
|
|
config_frame = ttk.LabelFrame(main_frame, text="GDB Configuration", padding="10")
|
|
config_frame.grid(row=0, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
|
|
config_frame.columnconfigure(1, weight=1) # Allow entry fields to expand
|
|
|
|
row_idx = 0
|
|
# GDB Path
|
|
ttk.Label(config_frame, text="GDB Executable:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2)
|
|
ttk.Entry(config_frame, textvariable=self.gdb_path_var).grid(row=row_idx, column=1, sticky=(tk.W, tk.E), padx=5, pady=2)
|
|
ttk.Button(config_frame, text="Browse...", command=self._browse_gdb_exe).grid(row=row_idx, column=2, padx=5, pady=2)
|
|
row_idx += 1
|
|
|
|
# Target Executable Path
|
|
ttk.Label(config_frame, text="Target Executable:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2)
|
|
ttk.Entry(config_frame, textvariable=self.exe_path_var).grid(row=row_idx, column=1, sticky=(tk.W, tk.E), padx=5, pady=2)
|
|
ttk.Button(config_frame, text="Browse...", command=self._browse_target_exe).grid(row=row_idx, column=2, padx=5, pady=2)
|
|
row_idx += 1
|
|
|
|
# GDB Dumper Script Path
|
|
ttk.Label(config_frame, text="GDB Dumper Script:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2)
|
|
ttk.Entry(config_frame, textvariable=self.gdb_script_path_var).grid(row=row_idx, column=1, sticky=(tk.W, tk.E), padx=5, pady=2)
|
|
ttk.Button(config_frame, text="Browse...", command=self._browse_gdb_script).grid(row=row_idx, column=2, padx=5, pady=2)
|
|
row_idx += 1
|
|
|
|
# Program Parameters
|
|
ttk.Label(config_frame, text="Program Parameters:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2)
|
|
ttk.Entry(config_frame, textvariable=self.params_var).grid(row=row_idx, column=1, columnspan=2, sticky=(tk.W, tk.E), padx=5, pady=2)
|
|
row_idx += 1
|
|
|
|
# Breakpoint
|
|
ttk.Label(config_frame, text="Breakpoint Location:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2)
|
|
ttk.Entry(config_frame, textvariable=self.breakpoint_var).grid(row=row_idx, column=1, columnspan=2, sticky=(tk.W, tk.E), padx=5, pady=2)
|
|
row_idx += 1
|
|
|
|
bp_help_text = "Examples: main, myfile.cpp:123, MyClass::myMethod, src/utils.cpp:105"
|
|
ttk.Label(config_frame, text=bp_help_text, foreground="gray", font=("TkDefaultFont", 8)).grid(
|
|
row=row_idx, column=1, columnspan=2, sticky=tk.W, padx=7, pady=(0,5) # padx=7 per allineare con Entry
|
|
)
|
|
row_idx += 1
|
|
|
|
# Variable to Dump
|
|
ttk.Label(config_frame, text="Variable/Expression:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2)
|
|
ttk.Entry(config_frame, textvariable=self.variable_var).grid(row=row_idx, column=1, columnspan=2, sticky=(tk.W, tk.E), padx=5, pady=2)
|
|
|
|
# --- Control Frame ---
|
|
control_frame = ttk.LabelFrame(main_frame, text="Session Control", padding="10")
|
|
control_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10)
|
|
|
|
# Using a sub-frame for button alignment might be cleaner if more buttons are added
|
|
button_flow_frame = ttk.Frame(control_frame)
|
|
button_flow_frame.pack(fill=tk.X, expand=True)
|
|
|
|
self.start_gdb_button = ttk.Button(button_flow_frame, text="1. Start GDB Session", command=self._start_gdb_session_action)
|
|
self.start_gdb_button.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.X, expand=True)
|
|
|
|
self.set_bp_button = ttk.Button(button_flow_frame, text="2. Set Breakpoint", command=self._set_gdb_breakpoint_action, state=tk.DISABLED)
|
|
self.set_bp_button.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.X, expand=True)
|
|
|
|
self.run_button = ttk.Button(button_flow_frame, text="3. Run Program", command=self._run_or_continue_gdb_action, state=tk.DISABLED)
|
|
self.run_button.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.X, expand=True)
|
|
|
|
self.dump_var_button = ttk.Button(button_flow_frame, text="4. Dump Variable (JSON)", command=self._dump_gdb_variable_action, state=tk.DISABLED)
|
|
self.dump_var_button.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.X, expand=True)
|
|
|
|
self.stop_gdb_button = ttk.Button(button_flow_frame, text="Stop GDB Session", command=self._stop_gdb_session_action, state=tk.DISABLED)
|
|
self.stop_gdb_button.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.X, expand=True)
|
|
|
|
# --- Output and Log Tabs ---
|
|
output_notebook = ttk.Notebook(main_frame)
|
|
output_notebook.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=10)
|
|
main_frame.rowconfigure(2, weight=1) # Allow notebook to expand vertically
|
|
|
|
# GDB Raw Output Tab
|
|
self.gdb_raw_output_text = scrolledtext.ScrolledText(output_notebook, wrap=tk.WORD, height=10, state=tk.DISABLED, font=("Consolas", 9))
|
|
output_notebook.add(self.gdb_raw_output_text, text="GDB Raw Output")
|
|
|
|
# Parsed JSON Output Tab
|
|
self.parsed_json_output_text = scrolledtext.ScrolledText(output_notebook, wrap=tk.WORD, height=10, state=tk.DISABLED, font=("Consolas", 9))
|
|
output_notebook.add(self.parsed_json_output_text, text="Parsed JSON Output")
|
|
|
|
# Application Log Tab
|
|
self.app_log_text = scrolledtext.ScrolledText(output_notebook, wrap=tk.WORD, height=10, state=tk.DISABLED, font=("Consolas", 9))
|
|
output_notebook.add(self.app_log_text, text="Application Log")
|
|
|
|
# --- Save Frame ---
|
|
save_frame = ttk.LabelFrame(main_frame, text="Save Dumped Data", padding="10")
|
|
save_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
|
|
|
|
self.save_json_button = ttk.Button(save_frame, text="Save as JSON", command=lambda: self._save_dumped_data("json"), state=tk.DISABLED)
|
|
self.save_json_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
|
|
self.save_csv_button = ttk.Button(save_frame, text="Save as CSV", command=lambda: self._save_dumped_data("csv"), state=tk.DISABLED)
|
|
self.save_csv_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
|
|
# Status Bar (optional, but good for brief messages)
|
|
self.status_var = tk.StringVar(value="Ready.")
|
|
status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
|
|
status_bar.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(5,0), ipady=2)
|
|
|
|
def _setup_logging_redirect_to_gui(self):
|
|
"""Configures a logging handler to display log messages in the GUI's app_log_text widget."""
|
|
# self.app_log_text DEVE essere creato prima di chiamare questa funzione
|
|
if not hasattr(self, 'app_log_text') or not self.app_log_text:
|
|
logger.error("app_log_text widget not available for GUI logging setup.")
|
|
return
|
|
|
|
self.gui_log_handler = ScrolledTextLogHandler(self.app_log_text) # Memorizza l'handler
|
|
formatter = logging.Formatter('%(asctime)s [%(levelname)-7s] %(module)s: %(message)s', datefmt='%H:%M:%S')
|
|
self.gui_log_handler.setFormatter(formatter)
|
|
self.gui_log_handler.setLevel(logging.INFO)
|
|
|
|
logging.getLogger().addHandler(self.gui_log_handler)
|
|
# Optionally, set the root logger level if not already set (e.g., in __main__.py)
|
|
# logging.getLogger().setLevel(logging.DEBUG) # Captures everything from DEBUG up
|
|
|
|
# --- Browse Methods ---
|
|
def _browse_file(self, title: str, target_var: tk.StringVar, filetypes=None):
|
|
path = filedialog.askopenfilename(title=title, filetypes=filetypes or [("All files", "*.*")])
|
|
if path:
|
|
target_var.set(path)
|
|
|
|
def _browse_gdb_exe(self):
|
|
self._browse_file("Select GDB Executable", self.gdb_path_var, [("Executable files", "*.exe"), ("All files", "*.*")])
|
|
|
|
def _browse_target_exe(self):
|
|
self._browse_file("Select Target Application Executable", self.exe_path_var, [("Executable files", "*.exe"), ("All files", "*.*")])
|
|
|
|
def _browse_gdb_script(self):
|
|
self._browse_file("Select GDB Python Dumper Script", self.gdb_script_path_var, [("Python files", "*.py"), ("All files", "*.*")])
|
|
|
|
|
|
# --- GUI Update Methods ---
|
|
def _update_gdb_raw_output(self, text: str, append: bool = True):
|
|
self.gdb_raw_output_text.config(state=tk.NORMAL)
|
|
if append:
|
|
self.gdb_raw_output_text.insert(tk.END, str(text) + "\n")
|
|
else:
|
|
self.gdb_raw_output_text.delete("1.0", tk.END)
|
|
self.gdb_raw_output_text.insert("1.0", str(text))
|
|
self.gdb_raw_output_text.see(tk.END)
|
|
self.gdb_raw_output_text.config(state=tk.DISABLED)
|
|
|
|
def _update_parsed_json_output(self, data_to_display: any):
|
|
self.parsed_json_output_text.config(state=tk.NORMAL)
|
|
self.parsed_json_output_text.delete("1.0", tk.END)
|
|
if data_to_display is None:
|
|
self.parsed_json_output_text.insert("1.0", "<No data to display>")
|
|
elif isinstance(data_to_display, (dict, list)):
|
|
try:
|
|
pretty_json = json.dumps(data_to_display, indent=2, ensure_ascii=False)
|
|
self.parsed_json_output_text.insert("1.0", pretty_json)
|
|
except Exception as e:
|
|
logger.error(f"Error pretty-printing JSON for GUI: {e}")
|
|
self.parsed_json_output_text.insert("1.0", f"Error displaying JSON: {e}\nRaw data: {str(data_to_display)}")
|
|
else: # If it's a simple string or other type (e.g., error message)
|
|
self.parsed_json_output_text.insert("1.0", str(data_to_display))
|
|
self.parsed_json_output_text.see("1.0") # Scroll to top
|
|
self.parsed_json_output_text.config(state=tk.DISABLED)
|
|
|
|
def _update_status_bar(self, message: str, is_error: bool = False):
|
|
self.status_var.set(message)
|
|
# Optionally change color for errors, but ttk.Label styling is limited without themes
|
|
# self.status_bar.config(foreground="red" if is_error else "black")
|
|
|
|
def _handle_gdb_operation_error(self, operation_name: str, error_details: any):
|
|
"""Handles errors from GDB operations, logs them, and shows a message."""
|
|
error_message = f"Error during GDB operation '{operation_name}': {error_details}"
|
|
logger.error(error_message)
|
|
self._update_gdb_raw_output(f"ERROR: {error_message}\n", append=True)
|
|
self._update_status_bar(f"Error: {operation_name} failed.", is_error=True)
|
|
messagebox.showerror("GDB Operation Error", error_message)
|
|
# Consider more specific state resets based on the error if needed
|
|
|
|
# --- GDB Action Methods (called by buttons) ---
|
|
def _start_gdb_session_action(self):
|
|
gdb_exe = self.gdb_path_var.get()
|
|
target_exe = self.exe_path_var.get()
|
|
gdb_script = self.gdb_script_path_var.get() # Path to gdb_dumper.py
|
|
|
|
if not gdb_exe or not target_exe:
|
|
messagebox.showerror("Input Error", "GDB executable and Target executable paths are required.")
|
|
return
|
|
|
|
if self.gdb_session and self.gdb_session.is_alive():
|
|
messagebox.showwarning("Session Active", "A GDB session is already active. Please stop it first.")
|
|
return
|
|
|
|
self._update_status_bar("Starting GDB session...")
|
|
self._update_gdb_raw_output("Attempting to start GDB session...\n", append=False)
|
|
self._update_parsed_json_output(None) # Clear parsed output
|
|
try:
|
|
self.gdb_session = GDBSession(gdb_exe, target_exe, gdb_script_full_path=gdb_script)
|
|
self.gdb_session.start() # This now also sources the script
|
|
|
|
self._update_gdb_raw_output(f"GDB session started for '{target_exe}'.\n")
|
|
if gdb_script and self.gdb_session.gdb_script_sourced_successfully:
|
|
self._update_gdb_raw_output("GDB dumper script sourced successfully.\n", append=True)
|
|
self._update_status_bar("GDB session active. Dumper script loaded.")
|
|
elif gdb_script:
|
|
self._update_gdb_raw_output("GDB dumper script specified but may have failed to load (check GDB Raw Output and logs).\n", append=True)
|
|
self._update_status_bar("GDB session active. Dumper script issues (check logs).", is_error=True)
|
|
else:
|
|
self._update_gdb_raw_output("No GDB dumper script specified. JSON dump via script unavailable.\n", append=True)
|
|
self._update_status_bar("GDB session active. No dumper script.")
|
|
|
|
# Update button states
|
|
self.start_gdb_button.config(state=tk.DISABLED)
|
|
self.set_bp_button.config(state=tk.NORMAL)
|
|
self.run_button.config(state=tk.DISABLED, text="3. Run Program") # Enabled after BP
|
|
self.dump_var_button.config(state=tk.DISABLED) # Enabled after hitting BP
|
|
self.stop_gdb_button.config(state=tk.NORMAL)
|
|
self.program_started_once = False
|
|
self.last_dumped_data = None
|
|
self._disable_save_buttons()
|
|
|
|
except (FileNotFoundError, ConnectionError, TimeoutError) as e:
|
|
self._handle_gdb_operation_error("start session", e)
|
|
self.gdb_session = None # Ensure session is None on failure
|
|
self._reset_gui_to_stopped_state() # Make sure GUI reflects this
|
|
except Exception as e: # Catch-all for other unexpected errors
|
|
self._handle_gdb_operation_error("start session (unexpected)", e)
|
|
self.gdb_session = None
|
|
self._reset_gui_to_stopped_state()
|
|
|
|
def _set_gdb_breakpoint_action(self):
|
|
if not self.gdb_session or not self.gdb_session.is_alive():
|
|
messagebox.showerror("Error", "GDB session is not active.")
|
|
return
|
|
|
|
bp_location = self.breakpoint_var.get()
|
|
if not bp_location:
|
|
messagebox.showerror("Input Error", "Breakpoint location cannot be empty.")
|
|
return
|
|
|
|
self._update_status_bar(f"Setting breakpoint at '{bp_location}'...")
|
|
try:
|
|
output = self.gdb_session.set_breakpoint(bp_location)
|
|
self._update_gdb_raw_output(output, append=True)
|
|
if "Breakpoint" in output and "not defined" not in output: # Basic check for success
|
|
self.set_bp_button.config(text=f"BP: {bp_location} (Set)")
|
|
self.run_button.config(state=tk.NORMAL)
|
|
self._update_status_bar(f"Breakpoint set at '{bp_location}'. Ready to run.")
|
|
else:
|
|
self._update_status_bar(f"Issue setting breakpoint at '{bp_location}'. Check GDB output.", is_error=True)
|
|
# self.set_bp_button.config(text="2. Set Breakpoint") # Optionally reset text
|
|
except (ConnectionError, TimeoutError) as e:
|
|
self._handle_gdb_operation_error(f"set breakpoint '{bp_location}'", e)
|
|
except Exception as e:
|
|
self._handle_gdb_operation_error(f"set breakpoint '{bp_location}' (unexpected)", e)
|
|
|
|
|
|
def _run_or_continue_gdb_action(self):
|
|
if not self.gdb_session or not self.gdb_session.is_alive():
|
|
messagebox.showerror("Error", "GDB session is not active.")
|
|
return
|
|
|
|
self._update_parsed_json_output(None) # Clear previous dump
|
|
self._disable_save_buttons()
|
|
|
|
try:
|
|
output = ""
|
|
if not self.program_started_once:
|
|
params_str = self.params_var.get()
|
|
self._update_status_bar(f"Running program with params: '{params_str}'...")
|
|
self._update_gdb_raw_output(f"Executing: run {params_str}\n", append=True)
|
|
output = self.gdb_session.run_program(params_str)
|
|
else:
|
|
self._update_status_bar("Continuing program execution...")
|
|
self._update_gdb_raw_output("Executing: continue\n", append=True)
|
|
output = self.gdb_session.continue_execution()
|
|
|
|
self._update_gdb_raw_output(output, append=True)
|
|
|
|
# Analyze output to determine state (hit breakpoint, exited, crashed)
|
|
if "Breakpoint" in output or re.search(r"Hit Breakpoint \d+", output, re.IGNORECASE):
|
|
self._update_status_bar("Breakpoint hit. Ready to dump variables.")
|
|
self.dump_var_button.config(state=tk.NORMAL if self.gdb_session.gdb_script_sourced_successfully else tk.DISABLED)
|
|
self.program_started_once = True
|
|
self.run_button.config(text="3. Continue")
|
|
elif "Program exited normally" in output or "exited with code" in output:
|
|
self._update_status_bar("Program exited.")
|
|
self.dump_var_button.config(state=tk.DISABLED)
|
|
self.run_button.config(text="3. Run Program (Restart)")
|
|
self.program_started_once = False # Allow re-run
|
|
elif "received signal" in output.lower() or "segmentation fault" in output.lower():
|
|
self._update_status_bar("Program received a signal (e.g., crash). Check GDB output.", is_error=True)
|
|
self.dump_var_button.config(state=tk.NORMAL if self.gdb_session.gdb_script_sourced_successfully else tk.DISABLED) # Still might want to dump
|
|
self.program_started_once = True # It did start
|
|
self.run_button.config(text="3. Continue (Risky)")
|
|
else: # Ambiguous state, but assume it's running or waiting
|
|
self._update_status_bar("Program running or in unknown state.")
|
|
self.dump_var_button.config(state=tk.NORMAL if self.gdb_session.gdb_script_sourced_successfully else tk.DISABLED)
|
|
self.program_started_once = True
|
|
self.run_button.config(text="3. Continue")
|
|
|
|
except (ConnectionError, TimeoutError) as e:
|
|
self._handle_gdb_operation_error("run/continue", e)
|
|
except Exception as e:
|
|
self._handle_gdb_operation_error("run/continue (unexpected)", e)
|
|
|
|
|
|
def _dump_gdb_variable_action(self):
|
|
if not self.gdb_session or not self.gdb_session.is_alive():
|
|
messagebox.showerror("Error", "GDB session is not active.")
|
|
return
|
|
if not self.gdb_session.gdb_script_sourced_successfully:
|
|
messagebox.showwarning("Dumper Script Error",
|
|
"GDB dumper script is not available or failed to load. JSON dump cannot proceed.")
|
|
return
|
|
|
|
var_expr = self.variable_var.get()
|
|
if not var_expr:
|
|
messagebox.showerror("Input Error", "Variable/Expression to dump cannot be empty.")
|
|
return
|
|
|
|
self._update_status_bar(f"Dumping '{var_expr}' to JSON...")
|
|
self._update_gdb_raw_output(f"Attempting JSON dump of: {var_expr}\n", append=True)
|
|
try:
|
|
dumped_data = self.gdb_session.dump_variable_to_json(var_expr)
|
|
self.last_dumped_data = dumped_data # Store for saving
|
|
|
|
# Display in the "Parsed JSON Output" tab
|
|
self._update_parsed_json_output(dumped_data)
|
|
|
|
# Append raw GDB output related to this dump if any was logged by the controller
|
|
# (The controller's dump_variable_to_json now logs full GDB output on certain errors)
|
|
# if isinstance(dumped_data, dict) and "_gdb_tool_error" in dumped_data and "raw_gdb_output" in dumped_data:
|
|
# self._update_gdb_raw_output(f"--- Raw GDB output for failed dump of '{var_expr}' ---\n{dumped_data['raw_gdb_output']}\n", append=True)
|
|
|
|
|
|
if isinstance(dumped_data, dict) and "_gdb_tool_error" in dumped_data:
|
|
error_msg = dumped_data.get("details", dumped_data["_gdb_tool_error"])
|
|
self._update_status_bar(f"Error dumping '{var_expr}': {error_msg}", is_error=True)
|
|
self._disable_save_buttons()
|
|
elif dumped_data is not None: # Success
|
|
self._update_status_bar(f"Successfully dumped '{var_expr}'. Ready to save.")
|
|
self._enable_save_buttons_if_data()
|
|
else: # Should not happen if dump_variable_to_json always returns a dict
|
|
self._update_status_bar(f"Dump of '{var_expr}' returned no data.", is_error=True)
|
|
self._disable_save_buttons()
|
|
|
|
except (ConnectionError, TimeoutError) as e: # Errors from GDBSession.send_cmd if not caught internally
|
|
self._handle_gdb_operation_error(f"dump variable '{var_expr}'", e)
|
|
self.last_dumped_data = None
|
|
self._disable_save_buttons()
|
|
self._update_parsed_json_output({"error": str(e)})
|
|
except Exception as e:
|
|
self._handle_gdb_operation_error(f"dump variable '{var_expr}' (unexpected)", e)
|
|
self.last_dumped_data = None
|
|
self._disable_save_buttons()
|
|
self._update_parsed_json_output({"error": str(e)})
|
|
|
|
def _reset_gui_to_stopped_state(self):
|
|
"""Resets GUI elements to the state before a GDB session is started."""
|
|
self.start_gdb_button.config(state=tk.NORMAL)
|
|
self.set_bp_button.config(state=tk.DISABLED, text="2. Set Breakpoint")
|
|
self.run_button.config(state=tk.DISABLED, text="3. Run Program")
|
|
self.dump_var_button.config(state=tk.DISABLED)
|
|
self.stop_gdb_button.config(state=tk.DISABLED)
|
|
self._disable_save_buttons()
|
|
self.program_started_once = False
|
|
self.last_dumped_data = None
|
|
self._update_status_bar("GDB session stopped or not active.")
|
|
# self._update_parsed_json_output(None) # Optionally clear parsed output on stop
|
|
|
|
def _stop_gdb_session_action(self):
|
|
if self.gdb_session and self.gdb_session.is_alive():
|
|
self._update_status_bar("Stopping GDB session...")
|
|
try:
|
|
# If program was started, try to kill it first (optional, quit should handle it)
|
|
if self.program_started_once:
|
|
kill_output = self.gdb_session.kill_program()
|
|
self._update_gdb_raw_output(f"Kill command output:\n{kill_output}\n", append=True)
|
|
|
|
self.gdb_session.quit() # GDBSession.quit handles actual process termination
|
|
self._update_gdb_raw_output("GDB session quit command sent.\n", append=True)
|
|
except Exception as e:
|
|
self._handle_gdb_operation_error("stop session", e)
|
|
finally:
|
|
self.gdb_session = None # Clear the session object
|
|
self._reset_gui_to_stopped_state()
|
|
else:
|
|
messagebox.showinfo("Info", "GDB session is not active or already stopped.")
|
|
self._reset_gui_to_stopped_state() # Ensure GUI is reset
|
|
|
|
# --- Save Data Methods ---
|
|
def _enable_save_buttons_if_data(self):
|
|
if self.last_dumped_data and not (isinstance(self.last_dumped_data, dict) and "_gdb_tool_error" in self.last_dumped_data):
|
|
self.save_json_button.config(state=tk.NORMAL)
|
|
# CSV saving is reasonable if data is a list of dicts, a single dict, or even simple list/value
|
|
self.save_csv_button.config(state=tk.NORMAL)
|
|
else:
|
|
self._disable_save_buttons()
|
|
|
|
def _disable_save_buttons(self):
|
|
self.save_json_button.config(state=tk.DISABLED)
|
|
self.save_csv_button.config(state=tk.DISABLED)
|
|
|
|
def _save_dumped_data(self, format_type: str):
|
|
if self.last_dumped_data is None or \
|
|
(isinstance(self.last_dumped_data, dict) and "_gdb_tool_error" in self.last_dumped_data):
|
|
messagebox.showwarning("No Data", "No valid data has been dumped to save.")
|
|
return
|
|
|
|
file_ext = f".{format_type.lower()}"
|
|
file_types = [(f"{format_type.upper()} files", f"*{file_ext}"), ("All files", "*.*")]
|
|
|
|
filepath = filedialog.asksaveasfilename(
|
|
defaultextension=file_ext,
|
|
filetypes=file_types,
|
|
title=f"Save Dumped Data as {format_type.upper()}"
|
|
)
|
|
if not filepath: return # User cancelled
|
|
|
|
self._update_status_bar(f"Saving data as {format_type.upper()} to {os.path.basename(filepath)}...")
|
|
try:
|
|
if format_type == "json":
|
|
save_to_json(self.last_dumped_data, filepath)
|
|
elif format_type == "csv":
|
|
# Prepare data for CSV: output_formatter expects list of dicts primarily
|
|
data_for_csv = self.last_dumped_data
|
|
if isinstance(data_for_csv, dict) and not isinstance(data_for_csv, list): # Single struct/object
|
|
data_for_csv = [data_for_csv]
|
|
elif not isinstance(data_for_csv, list): # Single primitive value
|
|
data_for_csv = [{"value": data_for_csv}]
|
|
elif isinstance(data_for_csv, list) and data_for_csv and not all(isinstance(item, dict) for item in data_for_csv):
|
|
# List of primitives
|
|
data_for_csv = [{"value": item} for item in data_for_csv]
|
|
|
|
save_to_csv(data_for_csv, filepath) # output_formatter.save_to_csv handles empty list
|
|
|
|
messagebox.showinfo("Save Successful", f"Data saved to:\n{filepath}")
|
|
self._update_status_bar(f"Data saved to {os.path.basename(filepath)}.")
|
|
except Exception as e:
|
|
logger.error(f"Error saving data as {format_type} to {filepath}: {e}", exc_info=True)
|
|
messagebox.showerror("Save Error", f"Failed to save data to {filepath}:\n{e}")
|
|
self._update_status_bar(f"Error saving data as {format_type}.", is_error=True)
|
|
|
|
# --- Window Closing Handler ---
|
|
def _on_closing_window(self):
|
|
"""Handles window close event: save settings, confirm quit if session active."""
|
|
logger.info("Window closing sequence initiated.")
|
|
self._save_settings()
|
|
|
|
should_destroy = True # Flag per decidere se distruggere la finestra
|
|
|
|
if self.gdb_session and self.gdb_session.is_alive():
|
|
if messagebox.askokcancel("Quit GDB Session", "A GDB session is active. Stop it and exit?"):
|
|
logger.info("User chose to stop active GDB session and exit.")
|
|
self._stop_gdb_session_action() # Questo dovrebbe gestire la terminazione della sessione GDB
|
|
else:
|
|
logger.info("User cancelled exit while GDB session is active.")
|
|
should_destroy = False # Non distruggere se l'utente annulla
|
|
|
|
if should_destroy:
|
|
logger.info("Proceeding with window destruction.")
|
|
# --- Rimuovi l'handler della GUI prima di distruggere i widget ---
|
|
if self.gui_log_handler:
|
|
logger.debug("Removing GUI log handler.")
|
|
logging.getLogger().removeHandler(self.gui_log_handler)
|
|
self.gui_log_handler = None # Rimuovi il riferimento
|
|
# --- Fine rimozione handler ---
|
|
self.destroy() # Distrugge la finestra e i suoi widget
|
|
logger.info("Tkinter window destroyed.") # Questo log andrà solo alla console
|
|
else:
|
|
logger.info("Window destruction aborted by user.")
|
|
|
|
|
|
class ScrolledTextLogHandler(logging.Handler):
|
|
"""A logging handler that directs messages to a Tkinter ScrolledText widget."""
|
|
def __init__(self, text_widget: scrolledtext.ScrolledText):
|
|
super().__init__()
|
|
self.text_widget = text_widget
|
|
self._active = True # Flag per indicare se l'handler è attivo
|
|
|
|
def emit(self, record):
|
|
if not self._active or not hasattr(self.text_widget, 'winfo_exists') or not self.text_widget.winfo_exists():
|
|
# Se l'handler è stato disattivato o il widget non esiste più, non fare nulla
|
|
# (o invia a un logger di fallback come print)
|
|
# print(f"GUI Logger (inactive/widget destroyed): {self.format(record)}")
|
|
return
|
|
|
|
try:
|
|
log_entry = self.format(record)
|
|
self.text_widget.config(state=tk.NORMAL)
|
|
self.text_widget.insert(tk.END, log_entry + "\n")
|
|
self.text_widget.see(tk.END)
|
|
self.text_widget.config(state=tk.DISABLED)
|
|
except tk.TclError as e:
|
|
# Cattura TclError se, nonostante i controlli, il widget è sparito
|
|
# tra winfo_exists() e l'operazione successiva (race condition rara ma possibile)
|
|
print(f"TclError in ScrolledTextLogHandler (widget likely destroyed): {e} - Record: {self.format(record)}")
|
|
self._active = False # Disattiva per evitare ulteriori errori
|
|
|
|
def close(self): # Metodo standard degli handler di logging
|
|
self._active = False
|
|
super().close() |