This commit is contained in:
VALLONGOL 2025-05-23 09:23:10 +02:00
parent f16862ce29
commit ec89134d2c
2 changed files with 585 additions and 181 deletions

View File

@ -0,0 +1,243 @@
# File: cpp_python_debug/core/profile_executor.py
# Manages the automated execution of a debug profile.
import logging
import os
import time # For timestamping if we log data directly for now
from typing import Dict, Any, Optional, Callable
from .gdb_controller import GDBSession
from .config_manager import AppSettings
# from .output_formatter import save_to_json, save_to_csv # For later
logger = logging.getLogger(__name__)
class ProfileExecutor:
"""
Orchestrates the execution of a debug profile, interacting with GDBSession.
"""
def __init__(self,
profile_data: Dict[str, Any],
app_settings: AppSettings,
status_update_callback: Optional[Callable[[str], None]] = None,
gdb_output_callback: Optional[Callable[[str], None]] = None,
json_output_callback: Optional[Callable[[Any], None]] = None
):
"""
Initializes the ProfileExecutor.
Args:
profile_data: The dictionary containing the profile configuration.
app_settings: The application settings instance.
status_update_callback: Callback to update status in GUI.
gdb_output_callback: Callback to send GDB raw output to GUI.
json_output_callback: Callback to send parsed JSON data to GUI.
"""
self.profile = profile_data
self.app_settings = app_settings
self.gdb_session: Optional[GDBSession] = None
self.is_running: bool = False
self._stop_requested: bool = False
self.status_updater = status_update_callback if status_update_callback else lambda msg: logger.info(f"Status: {msg}")
self.gdb_output_writer = gdb_output_callback if gdb_output_callback else lambda msg: logger.debug(f"GDB Output: {msg}")
self.json_data_handler = json_output_callback if json_output_callback else lambda data: logger.debug(f"JSON Data: {str(data)[:200]}")
logger.info(f"ProfileExecutor initialized for profile: '{self.profile.get('profile_name', 'Unnamed Profile')}'")
def _get_setting(self, category: str, key: str, default: Optional[Any] = None) -> Any:
"""Helper to get settings via app_settings."""
return self.app_settings.get_setting(category, key, default)
def _get_dumper_options(self) -> Dict[str, Any]:
"""Helper to get dumper options."""
return self.app_settings.get_category_settings("dumper_options", {})
def run(self) -> None:
"""
Starts the automated execution of the debug profile.
This method will run synchronously for now for simplicity in this first step.
Later, it might need to run in a separate thread to keep the GUI responsive.
"""
profile_name = self.profile.get("profile_name", "Unnamed Profile")
self.status_updater(f"Starting profile: '{profile_name}'...")
self.is_running = True
self._stop_requested = False
gdb_exe = self._get_setting("general", "gdb_executable_path")
target_exe = self.profile.get("target_executable")
gdb_script_path = self._get_setting("general", "gdb_dumper_script_path")
if not target_exe or not os.path.exists(target_exe):
self.status_updater(f"Error: Target executable '{target_exe}' not found for profile '{profile_name}'.")
logger.error(f"Target executable '{target_exe}' not found for profile '{profile_name}'.")
self.is_running = False
return
try:
self.gdb_session = GDBSession(
gdb_path=gdb_exe,
executable_path=target_exe,
gdb_script_full_path=gdb_script_path,
dumper_options=self._get_dumper_options()
)
startup_timeout = self._get_setting("timeouts", "gdb_start", 30)
self.status_updater(f"Profile '{profile_name}': Spawning GDB for '{os.path.basename(target_exe)}'...")
self.gdb_session.start(timeout=startup_timeout)
self.gdb_output_writer(f"GDB session started for profile '{profile_name}'.\n")
if gdb_script_path and self.gdb_session.gdb_script_sourced_successfully:
self.gdb_output_writer(f"GDB dumper script '{os.path.basename(gdb_script_path)}' sourced successfully.\n")
elif gdb_script_path:
self.gdb_output_writer(f"Warning: GDB dumper script '{os.path.basename(gdb_script_path)}' failed to load.\n")
# --- Simplified Execution Logic for the first action and first variable ---
actions = self.profile.get("actions", [])
if not actions:
self.status_updater(f"Profile '{profile_name}' has no actions defined. Stopping.")
logger.warning(f"Profile '{profile_name}' has no actions.")
self._cleanup_session()
return
# For now, just process the first action
first_action = actions[0]
bp_location = first_action.get("breakpoint_location")
vars_to_dump = first_action.get("variables_to_dump", [])
continue_after = first_action.get("continue_after_dump", True)
if not bp_location:
self.status_updater(f"Profile '{profile_name}': First action has no breakpoint. Stopping.")
logger.error(f"Profile '{profile_name}': First action missing breakpoint.")
self._cleanup_session()
return
if not vars_to_dump:
self.status_updater(f"Profile '{profile_name}': First action at '{bp_location}' has no variables to dump. Setting BP and running.")
# We might still want to run to the breakpoint even if no vars are dumped.
# For now, let's proceed if there's a BP.
# Set breakpoint
self.status_updater(f"Profile '{profile_name}': Setting breakpoint at '{bp_location}'...")
cmd_timeout = self._get_setting("timeouts", "gdb_command", 30)
bp_output = self.gdb_session.set_breakpoint(bp_location, timeout=cmd_timeout)
self.gdb_output_writer(bp_output)
if "Breakpoint" not in bp_output and "pending" not in bp_output.lower():
self.status_updater(f"Error: Failed to set breakpoint '{bp_location}'. Check GDB output.")
logger.error(f"Failed to set breakpoint '{bp_location}' for profile '{profile_name}'. Output: {bp_output}")
self._cleanup_session()
return
# Run program
program_params = self.profile.get("program_parameters", "")
self.status_updater(f"Profile '{profile_name}': Running program '{os.path.basename(target_exe)} {program_params}'...")
run_timeout = self._get_setting("timeouts", "program_run_continue", 120)
run_output = self.gdb_session.run_program(program_params, timeout=run_timeout)
self.gdb_output_writer(run_output)
if self._stop_requested:
self.status_updater(f"Profile '{profile_name}' execution stopped by user request.")
self._cleanup_session()
return
# Check if breakpoint was hit
if "Breakpoint" in run_output or \
(hasattr(self.gdb_session.child, 'before') and "Breakpoint" in self.gdb_session.child.before): # Check 'before' as well
self.status_updater(f"Profile '{profile_name}': Hit breakpoint at '{bp_location}'.")
if vars_to_dump:
var_to_dump = vars_to_dump[0] # Just the first one for now
self.status_updater(f"Profile '{profile_name}': Dumping variable '{var_to_dump}'...")
dump_timeout = self._get_setting("timeouts", "dump_variable", 60)
if not self.gdb_session.gdb_script_sourced_successfully:
msg = f"Profile '{profile_name}': GDB Dumper script not available/loaded. Cannot dump '{var_to_dump}'."
self.status_updater(msg)
logger.warning(msg)
self.json_data_handler({"_profile_executor_error": msg})
else:
dumped_json = self.gdb_session.dump_variable_to_json(var_to_dump, timeout=dump_timeout)
self.json_data_handler(dumped_json) # Send to GUI/logger
self.gdb_output_writer(f"Dumped '{var_to_dump}': {str(dumped_json)[:200]}...\n") # Also to GDB raw output for now
if isinstance(dumped_json, dict) and "_gdb_tool_error" in dumped_json:
self.status_updater(f"Error dumping '{var_to_dump}': {dumped_json.get('details', '')}")
else:
self.status_updater(f"Successfully dumped '{var_to_dump}'.")
# Placeholder for saving to file later
logger.info(f"Profile '{profile_name}' - Dumped data for '{var_to_dump}': {str(dumped_json)[:100]}...")
if continue_after:
if self._stop_requested:
self.status_updater(f"Profile '{profile_name}' execution stopped by user request before continue.")
self._cleanup_session()
return
self.status_updater(f"Profile '{profile_name}': Continuing execution...")
continue_output = self.gdb_session.continue_execution(timeout=run_timeout)
self.gdb_output_writer(continue_output)
# Rudimentary: check for program exit after continue
if "Program exited normally" in continue_output or "exited with code" in continue_output:
self.status_updater(f"Profile '{profile_name}': Program exited after continue.")
else:
self.status_updater(f"Profile '{profile_name}': Program continued. Further automatic steps not yet implemented.")
else:
self.status_updater(f"Profile '{profile_name}': Execution paused at '{bp_location}' as per profile action (continue_after_dump is false).")
elif "Program exited normally" in run_output or "exited with code" in run_output:
self.status_updater(f"Profile '{profile_name}': Program exited before hitting breakpoint '{bp_location}'.")
else:
self.status_updater(f"Profile '{profile_name}': Program did not hit breakpoint '{bp_location}' as expected. Output: {run_output[:100]}...")
except FileNotFoundError as fnf_e:
err_msg = f"Error running profile '{profile_name}': File not found - {fnf_e}"
self.status_updater(err_msg)
logger.error(err_msg, exc_info=True)
except (ConnectionError, TimeoutError) as session_e:
err_msg = f"Session error running profile '{profile_name}': {type(session_e).__name__} - {session_e}"
self.status_updater(err_msg)
logger.error(err_msg, exc_info=True)
except Exception as e:
err_msg = f"Unexpected error running profile '{profile_name}': {type(e).__name__} - {e}"
self.status_updater(err_msg)
logger.critical(err_msg, exc_info=True)
finally:
self._cleanup_session()
self.status_updater(f"Profile '{profile_name}' execution finished.")
self.is_running = False
def request_stop(self) -> None:
"""Requests the profile execution to stop gracefully."""
self.status_updater("Stop requested for current profile execution...")
self._stop_requested = True
# If GDB session is in a blocking expect, this won't have immediate effect
# until GDB returns control. For true async stop, gdb_session would need interrupt.
def _cleanup_session(self) -> None:
"""Cleans up the GDB session."""
if self.gdb_session and self.gdb_session.is_alive():
self.status_updater("Cleaning up GDB session...")
quit_timeout = self._get_setting("timeouts", "gdb_quit", 10)
# If program might be running, try to kill it first, then quit
if self.gdb_session.child and self.gdb_session.child.isalive(): # Check if child process exists
# A more robust check would be to see if GDB thinks a program is loaded/running
# For now, assume we might need to kill if a breakpoint was hit or run was issued.
try:
kill_timeout = self._get_setting("timeouts", "kill_program", 20)
# Only kill if it's likely the inferior is running.
# This is tricky. GDB might not have an inferior if it exited.
# For simplicity now, just attempt quit. A more robust kill would be needed
# if the program is left running and blocks quit.
# kill_output = self.gdb_session.kill_program(timeout=kill_timeout)
# self.gdb_output_writer(f"Kill attempt during cleanup: {kill_output}\n")
pass # Skip kill for now to avoid complexity if program already exited
except Exception as e_kill:
logger.warning(f"Exception during kill in cleanup: {e_kill}")
self.gdb_session.quit(timeout=quit_timeout)
self.gdb_output_writer("GDB session quit during cleanup.\n")
self.gdb_session = None
logger.info("ProfileExecutor GDB session cleaned up.")

View File

@ -1,5 +1,5 @@
# File: cpp_python_debug/gui/main_window.py # File: cpp_python_debug/gui/main_window.py
# Provides the Tkinter GUI for interacting with the GDB session. # Provides the Tkinter GUI for interacting with the GDB session and automated profiles.
import tkinter as tk import tkinter as tk
from tkinter import filedialog, messagebox, ttk, scrolledtext, Menu from tkinter import filedialog, messagebox, ttk, scrolledtext, Menu
@ -7,13 +7,17 @@ import logging
import os import os
import json # For pretty-printing JSON in the GUI import json # For pretty-printing JSON in the GUI
import re import re
import threading # For running profile executor in a separate thread
from typing import Optional, Dict, Any
# Relative imports for modules within the same package # Relative imports for modules within the same package
from ..core.gdb_controller import GDBSession from ..core.gdb_controller import GDBSession
from ..core.output_formatter import save_to_json, save_to_csv from ..core.output_formatter import save_to_json, save_to_csv
from ..core.config_manager import AppSettings from ..core.config_manager import AppSettings
from .config_window import ConfigWindow from ..core.profile_executor import ProfileExecutor # NEW IMPORT
from .profile_manager_window import ProfileManagerWindow from .config_window import ConfigWindow
from .profile_manager_window import ProfileManagerWindow
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -22,21 +26,19 @@ class GDBGui(tk.Tk):
super().__init__() super().__init__()
self.app_settings = AppSettings() self.app_settings = AppSettings()
self.gui_log_handler = None self.gui_log_handler = None
self.title(f"GDB Debug GUI - Settings: {os.path.basename(self.app_settings.config_filepath)}") self.title(f"GDB Debug GUI - Settings: {os.path.basename(self.app_settings.config_filepath)}")
self.geometry(self.app_settings.get_setting("gui", "main_window_geometry", "850x650")) # Adjusted height slightly self.geometry(self.app_settings.get_setting("gui", "main_window_geometry", "850x650"))
self.gdb_session = None self.gdb_session: Optional[GDBSession] = None # For manual GDB session
self.last_dumped_data = None self.last_dumped_data: Any = None # For manual dump
self.program_started_once = False self.program_started_once: bool = False # For manual run/continue logic
# MODIFIED: StringVars for status display of critical paths
self.gdb_exe_status_var = tk.StringVar(value="GDB: Checking...") self.gdb_exe_status_var = tk.StringVar(value="GDB: Checking...")
self.gdb_dumper_status_var = tk.StringVar(value="Dumper Script: Checking...") self.gdb_dumper_status_var = tk.StringVar(value="Dumper Script: Checking...")
# Tkinter StringVars for other input fields remain # StringVars for manual debug input fields
self.exe_path_var = tk.StringVar( self.exe_path_var = tk.StringVar(
value=self.app_settings.get_setting("general", "last_target_executable_path", "") value=self.app_settings.get_setting("general", "last_target_executable_path", "")
) )
@ -50,153 +52,104 @@ class GDBGui(tk.Tk):
value=self.app_settings.get_setting("general", "default_program_parameters", "") value=self.app_settings.get_setting("general", "default_program_parameters", "")
) )
self._create_menus() # For Automated Profile Execution Tab
self._create_widgets() # Widgets creation first self.profile_executor_instance: Optional[ProfileExecutor] = None
self._setup_logging_redirect_to_gui() self.available_profiles_map: Dict[str, Dict[str, Any]] = {} # Maps display name to profile data
self.profile_exec_status_var = tk.StringVar(value="Select a profile to run.") # Status for auto execution
# MODIFIED: Initial check of critical configurations self._create_menus()
self._check_critical_configs_and_update_gui() self._create_widgets()
self._setup_logging_redirect_to_gui()
self._check_critical_configs_and_update_gui()
self._load_and_populate_profiles_for_automation_tab()
self.protocol("WM_DELETE_WINDOW", self._on_closing_window) self.protocol("WM_DELETE_WINDOW", self._on_closing_window)
# REMOVED: _initialize_gdb_dumper_script_path (handled by AppSettings and ConfigWindow)
def _create_menus(self): def _create_menus(self):
self.menubar = Menu(self) self.menubar = Menu(self)
self.config(menu=self.menubar) self.config(menu=self.menubar)
options_menu = Menu(self.menubar, tearoff=0) options_menu = Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label="Options", menu=options_menu) self.menubar.add_cascade(label="Options", menu=options_menu)
options_menu.add_command(label="Configure Application...", command=self._open_config_window) options_menu.add_command(label="Configure Application...", command=self._open_config_window)
options_menu.add_separator() options_menu.add_separator()
options_menu.add_command(label="Exit", command=self._on_closing_window) options_menu.add_command(label="Exit", command=self._on_closing_window)
profiles_menu = Menu(self.menubar, tearoff=0) profiles_menu = Menu(self.menubar, tearoff=0)
self.menubar.add_cascade(label="Profiles", menu=profiles_menu) self.menubar.add_cascade(label="Profiles", menu=profiles_menu)
profiles_menu.add_command(label="Manage Profiles...", command=self._open_profile_manager_window) profiles_menu.add_command(label="Manage Profiles...", command=self._open_profile_manager_window)
# Dynamic list of profiles for quick run could be added here later
def _open_profile_manager_window(self):
logger.info("Opening Profile Manager window.")
# Pass 'self' as parent and self.app_settings
profile_win = ProfileManagerWindow(self, self.app_settings)
self.wait_window(profile_win) # Makes it modal
logger.info("Profile Manager window closed.")
def _open_config_window(self): def _open_config_window(self):
logger.debug("Opening configuration window.") logger.debug("Opening configuration window.")
config_win = ConfigWindow(self, self.app_settings) config_win = ConfigWindow(self, self.app_settings)
self.wait_window(config_win) # Makes the config window modal self.wait_window(config_win)
logger.debug("Configuration window closed.") logger.debug("Configuration window closed.")
self._check_critical_configs_and_update_gui() # Refresh status and button states self._check_critical_configs_and_update_gui()
def _open_profile_manager_window(self):
logger.info("Opening Profile Manager window.")
profile_win = ProfileManagerWindow(self, self.app_settings)
self.wait_window(profile_win)
logger.info("Profile Manager window closed.")
self._load_and_populate_profiles_for_automation_tab() # Refresh profiles list
def _check_critical_configs_and_update_gui(self): def _check_critical_configs_and_update_gui(self):
"""
Checks critical GDB and Dumper script configurations and updates GUI elements accordingly.
Enables/disables session control buttons based on configuration validity.
"""
logger.info("Checking critical configurations (GDB executable and Dumper script).") logger.info("Checking critical configurations (GDB executable and Dumper script).")
gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path") gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path")
dumper_script_path = self.app_settings.get_setting("general", "gdb_dumper_script_path") dumper_script_path = self.app_settings.get_setting("general", "gdb_dumper_script_path")
gdb_ok = False gdb_ok = False
dumper_ok = False # Dumper is optional, but its status is still relevant if gdb_exe_path and os.path.isfile(gdb_exe_path):
# Check GDB executable
if gdb_exe_path and os.path.isfile(gdb_exe_path): # Check if it's a file, more robust than just exists
self.gdb_exe_status_var.set(f"GDB: {os.path.basename(gdb_exe_path)} (OK)") self.gdb_exe_status_var.set(f"GDB: {os.path.basename(gdb_exe_path)} (OK)")
gdb_ok = True gdb_ok = True
elif gdb_exe_path: # Path is set but not a valid file elif gdb_exe_path:
self.gdb_exe_status_var.set(f"GDB: '{gdb_exe_path}' (Not Found/Invalid!)") self.gdb_exe_status_var.set(f"GDB: '{gdb_exe_path}' (Not Found/Invalid!)")
else: # Path not set else:
self.gdb_exe_status_var.set("GDB: Not Configured! Please set in Options > Configure.") self.gdb_exe_status_var.set("GDB: Not Configured! Please set in Options > Configure.")
# Check GDB Dumper script (optional)
if dumper_script_path and os.path.isfile(dumper_script_path): if dumper_script_path and os.path.isfile(dumper_script_path):
self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(dumper_script_path)} (OK)") self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(dumper_script_path)} (OK)")
dumper_ok = True # Not strictly necessary for dumper_ok to be true for session start elif dumper_script_path:
elif dumper_script_path: # Path is set but not a valid file
self.gdb_dumper_status_var.set(f"Dumper: '{dumper_script_path}' (Not Found/Invalid!)") self.gdb_dumper_status_var.set(f"Dumper: '{dumper_script_path}' (Not Found/Invalid!)")
else: # Path not set else:
self.gdb_dumper_status_var.set("Dumper: Not Configured (Optional).") self.gdb_dumper_status_var.set("Dumper: Not Configured (Optional).")
dumper_ok = True # Considered "OK" in the sense that it's not misconfigured if empty
# Update button states
if gdb_ok: if gdb_ok:
self.start_gdb_button.config(state=tk.NORMAL) # Only enable manual start if no profile is running
# Other buttons remain disabled until GDB session starts if not (self.profile_executor_instance and self.profile_executor_instance.is_running):
if self.gdb_session and self.gdb_session.is_alive(): self.start_gdb_button.config(state=tk.NORMAL)
# This case should ideally not happen here as _reset_gui_to_stopped_state # Other manual buttons depend on GDB session state
# would have been called if GDB session was active and then config changed. else:
# But as a safeguard: self._reset_gui_to_stopped_state() # This will disable all manual session buttons
self.set_bp_button.config(state=tk.NORMAL) self.start_gdb_button.config(state=tk.DISABLED)
# self.run_button state depends on breakpoint
# self.dump_var_button state depends on hitting breakpoint & dumper script
self.stop_gdb_button.config(state=tk.NORMAL)
else: # No active session, reset dependent buttons
self.set_bp_button.config(state=tk.DISABLED)
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)
else: # GDB not configured or invalid
self._reset_gui_to_stopped_state() # This will disable all session buttons
self.start_gdb_button.config(state=tk.DISABLED) # Explicitly ensure start is disabled
# The dump_var_button's state also depends on whether the dumper script is OK
# and if a breakpoint is hit. This is handled in _run_or_continue_gdb_action.
# Here, we primarily care about enabling the GDB start.
# Update main window title to reflect config file
self.title(f"GDB Debug GUI - Settings: {os.path.basename(self.app_settings.config_filepath)}") self.title(f"GDB Debug GUI - Settings: {os.path.basename(self.app_settings.config_filepath)}")
def _create_widgets(self): def _create_widgets(self):
main_frame = ttk.Frame(self, padding="10") main_frame = ttk.Frame(self, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.columnconfigure(0, weight=1) # Permette a main_frame di espandersi orizzontalmente self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1) # Permette a main_frame di espandersi verticalmente self.rowconfigure(0, weight=1)
# Configura le righe di main_frame per l'espansione main_frame.rowconfigure(0, weight=0) # Config Status
# row 0: config_status_frame (non espandibile verticalmente) main_frame.rowconfigure(1, weight=1) # Mode Notebook
# row 1: mode_notebook (moderatamente espandibile o fisso) main_frame.rowconfigure(2, weight=3) # Output/Log Notebook
# row 2: output_log_notebook (massima espansione verticale) main_frame.rowconfigure(3, weight=0) # Status Bar
# row 3: status_bar (non espandibile verticalmente) main_frame.columnconfigure(0, weight=1)
main_frame.rowconfigure(0, weight=0)
main_frame.rowconfigure(1, weight=1) # Diamo un po' di peso al notebook delle modalità
main_frame.rowconfigure(2, weight=3) # Diamo più peso al notebook dei log per espansione
main_frame.rowconfigure(3, weight=0)
main_frame.columnconfigure(0, weight=1) # Permette espansione orizzontale dei figli
# --- 1. Frame Stato Configurazione Critica (IN ALTO) ---
self._create_config_status_widgets(main_frame) self._create_config_status_widgets(main_frame)
# --- 2. Notebook per Modalità di Debug (Manuale / Automatico) ---
self._create_mode_notebook_widgets(main_frame) self._create_mode_notebook_widgets(main_frame)
# --- 3. Notebook per Output e Log (IN BASSO, SOPRA LA STATUS BAR) ---
self._create_output_log_widgets(main_frame) self._create_output_log_widgets(main_frame)
# --- 4. Barra di Stato (IN FONDO) ---
self._create_status_bar(main_frame) self._create_status_bar(main_frame)
def _create_config_status_widgets(self, parent_frame: ttk.Frame): def _create_config_status_widgets(self, parent_frame: ttk.Frame):
"""Crea i widget per lo stato della configurazione critica, tutti su una riga.""" config_status_frame = ttk.LabelFrame(parent_frame, text="Critical Configuration Status", padding=(10, 5, 10, 10))
config_status_frame = ttk.LabelFrame(parent_frame, text="Critical Configuration Status", padding=(10, 5, 10, 10)) # Aggiustato padding
config_status_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5, padx=0) config_status_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5, padx=0)
# Configura le colonne per distribuire lo spazio config_status_frame.columnconfigure(1, weight=1)
# Colonna 0: Label "GDB Executable:" config_status_frame.columnconfigure(3, weight=1)
# Colonna 1: Status GDB (espandibile)
# Colonna 2: Label "GDB Dumper Script:"
# Colonna 3: Status Dumper (espandibile)
# Colonna 4: Bottone "Open Configuration..."
config_status_frame.columnconfigure(0, weight=0) # Label fissa
config_status_frame.columnconfigure(1, weight=1) # Status espandibile
config_status_frame.columnconfigure(2, weight=0) # Label fissa
config_status_frame.columnconfigure(3, weight=1) # Status espandibile
config_status_frame.columnconfigure(4, weight=0) # Bottone fisso
# Riga 0
ttk.Label(config_status_frame, text="GDB:").grid(row=0, column=0, sticky=tk.W, padx=(5,0), pady=5) ttk.Label(config_status_frame, text="GDB:").grid(row=0, column=0, sticky=tk.W, padx=(5,0), pady=5)
self.gdb_exe_status_label = ttk.Label(config_status_frame, textvariable=self.gdb_exe_status_var, relief="sunken", padding=(5,2), anchor=tk.W) self.gdb_exe_status_label = ttk.Label(config_status_frame, textvariable=self.gdb_exe_status_var, relief="sunken", padding=(5,2), anchor=tk.W)
self.gdb_exe_status_label.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0,10), pady=5) self.gdb_exe_status_label.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0,10), pady=5)
@ -210,27 +163,21 @@ class GDBGui(tk.Tk):
) )
def _create_mode_notebook_widgets(self, parent_frame: ttk.Frame): def _create_mode_notebook_widgets(self, parent_frame: ttk.Frame):
"""Crea il Notebook per le modalità di debug (Manuale, Automatico)."""
mode_notebook = ttk.Notebook(parent_frame) mode_notebook = ttk.Notebook(parent_frame)
# MODIFIED: sticky per espandere in tutte le direzioni, padx per allineare
mode_notebook.grid(row=1, column=0, columnspan=1, sticky='nsew', pady=5, padx=0) mode_notebook.grid(row=1, column=0, columnspan=1, sticky='nsew', pady=5, padx=0)
# Tab: Manual Debug
manual_debug_frame = ttk.Frame(mode_notebook, padding="5") manual_debug_frame = ttk.Frame(mode_notebook, padding="5")
mode_notebook.add(manual_debug_frame, text="Manual Debug") mode_notebook.add(manual_debug_frame, text="Manual Debug")
self._populate_manual_debug_tab(manual_debug_frame) self._populate_manual_debug_tab(manual_debug_frame)
# Tab: Automatic Debug (Placeholder) self.automated_exec_frame = ttk.Frame(mode_notebook, padding="10")
automatic_debug_frame = ttk.Frame(mode_notebook, padding="10") mode_notebook.add(self.automated_exec_frame, text="Automated Profile Execution")
mode_notebook.add(automatic_debug_frame, text="Automatic Debug") self._populate_automated_execution_tab(self.automated_exec_frame)
ttk.Label(automatic_debug_frame, text="Automated profile execution controls will be here.").pack(padx=5, pady=5)
def _populate_manual_debug_tab(self, parent_tab_frame: ttk.Frame): def _populate_manual_debug_tab(self, parent_tab_frame: ttk.Frame):
"""Popola la scheda "Manual Debug" con i controlli necessari.""" parent_tab_frame.columnconfigure(0, weight=1)
parent_tab_frame.columnconfigure(0, weight=1) # Permetti al contenuto di espandersi
# Frame per Target & Debug Session Settings (precedentemente runtime_config_frame)
manual_target_settings_frame = ttk.LabelFrame(parent_tab_frame, text="Target & Debug Session Settings", padding="10") manual_target_settings_frame = ttk.LabelFrame(parent_tab_frame, text="Target & Debug Session Settings", padding="10")
manual_target_settings_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5) manual_target_settings_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5)
manual_target_settings_frame.columnconfigure(1, weight=1) manual_target_settings_frame.columnconfigure(1, weight=1)
@ -257,12 +204,11 @@ class GDBGui(tk.Tk):
ttk.Label(manual_target_settings_frame, text="Variable/Expression:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2) ttk.Label(manual_target_settings_frame, text="Variable/Expression:").grid(row=row_idx, column=0, sticky=tk.W, padx=5, pady=2)
ttk.Entry(manual_target_settings_frame, textvariable=self.variable_var).grid(row=row_idx, column=1, columnspan=2, sticky=(tk.W, tk.E), padx=5, pady=2) ttk.Entry(manual_target_settings_frame, textvariable=self.variable_var).grid(row=row_idx, column=1, columnspan=2, sticky=(tk.W, tk.E), padx=5, pady=2)
# Frame per Session Control (precedentemente control_frame)
manual_session_control_frame = ttk.LabelFrame(parent_tab_frame, text="Session Control", padding="10") manual_session_control_frame = ttk.LabelFrame(parent_tab_frame, text="Session Control", padding="10")
manual_session_control_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(10,5)) manual_session_control_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(10,5))
button_flow_frame = ttk.Frame(manual_session_control_frame) # Contenitore per i bottoni button_flow_frame = ttk.Frame(manual_session_control_frame)
button_flow_frame.pack(fill=tk.X, expand=True) # Permetti ai bottoni di distribuirsi button_flow_frame.pack(fill=tk.X, expand=True)
self.start_gdb_button = ttk.Button(button_flow_frame, text="1. Start GDB", command=self._start_gdb_session_action, state=tk.DISABLED) self.start_gdb_button = ttk.Button(button_flow_frame, text="1. Start GDB", command=self._start_gdb_session_action, state=tk.DISABLED)
self.start_gdb_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True) self.start_gdb_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
@ -279,7 +225,6 @@ class GDBGui(tk.Tk):
self.stop_gdb_button = ttk.Button(button_flow_frame, text="Stop GDB", command=self._stop_gdb_session_action, state=tk.DISABLED) self.stop_gdb_button = ttk.Button(button_flow_frame, text="Stop GDB", command=self._stop_gdb_session_action, state=tk.DISABLED)
self.stop_gdb_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True) self.stop_gdb_button.pack(side=tk.LEFT, padx=2, pady=5, fill=tk.X, expand=True)
# Frame per Save Dumped Data (precedentemente save_frame)
manual_save_data_frame = ttk.LabelFrame(parent_tab_frame, text="Save Dumped Data", padding="10") manual_save_data_frame = ttk.LabelFrame(parent_tab_frame, text="Save Dumped Data", padding="10")
manual_save_data_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=5) manual_save_data_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=5)
@ -289,13 +234,76 @@ class GDBGui(tk.Tk):
self.save_csv_button = ttk.Button(manual_save_data_frame, text="Save as CSV", command=lambda: self._save_dumped_data("csv"), state=tk.DISABLED) self.save_csv_button = ttk.Button(manual_save_data_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) self.save_csv_button.pack(side=tk.LEFT, padx=5, pady=5)
def _populate_automated_execution_tab(self, parent_tab_frame: ttk.Frame) -> None:
"""Populates the tab for automated profile execution."""
parent_tab_frame.columnconfigure(0, weight=1) # Consenti al frame principale del tab di espandersi
# Frame per Profile Selection e Control, ora con una griglia più complessa
auto_control_frame = ttk.LabelFrame(parent_tab_frame, text="Profile Execution Control", padding="10")
auto_control_frame.grid(row=0, column=0, sticky="ew", pady=5)
# Configura le colonne del auto_control_frame per il layout desiderato:
# Colonna 0: Label "Select Profile:"
# Colonna 1: Combobox (espandibile)
# Colonna 2: Bottone Run
# Colonna 3: Bottone Stop
auto_control_frame.columnconfigure(0, weight=0) # Label fissa
auto_control_frame.columnconfigure(1, weight=1) # Combobox espandibile
auto_control_frame.columnconfigure(2, weight=0) # Bottone Run fisso
auto_control_frame.columnconfigure(3, weight=0) # Bottone Stop fisso
# Riga 0: Label, Combobox, Run Button, Stop Button
ttk.Label(auto_control_frame, text="Select Profile:").grid(row=0, column=0, padx=(5,2), pady=5, sticky="w")
self.profile_selection_combo = ttk.Combobox(auto_control_frame, state="readonly", width=35, # Larghezza leggermente ridotta
textvariable=tk.StringVar())
self.profile_selection_combo.grid(row=0, column=1, padx=(0,5), pady=5, sticky="ew")
self.run_profile_button = ttk.Button(auto_control_frame, text="Run Profile", command=self._run_selected_profile_action, state=tk.DISABLED)
self.run_profile_button.grid(row=0, column=2, padx=(0,2), pady=5, sticky="ew")
self.stop_profile_button = ttk.Button(auto_control_frame, text="Stop Profile", command=self._stop_current_profile_action, state=tk.DISABLED)
self.stop_profile_button.grid(row=0, column=3, padx=(0,5), pady=5, sticky="ew")
# Riga 1: Status label (sotto i controlli)
ttk.Label(auto_control_frame, textvariable=self.profile_exec_status_var, relief=tk.SUNKEN, anchor=tk.W, padding=3).grid(
row=1, column=0, columnspan=4, sticky="ew", padx=5, pady=(10,5), ipady=2 # columnspan=4 per coprire tutte le colonne
)
def _load_and_populate_profiles_for_automation_tab(self) -> None:
self.available_profiles_map.clear()
profiles = self.app_settings.get_profiles()
profile_display_names = []
for profile_item in profiles: # Renamed to avoid conflict with menubar 'profiles_menu'
name = profile_item.get("profile_name")
if name:
self.available_profiles_map[name] = profile_item
profile_display_names.append(name)
sorted_names = sorted(profile_display_names)
self.profile_selection_combo['values'] = sorted_names
# Safely get the textvariable and set its value
combo_text_var = self.profile_selection_combo.cget("textvariable")
if isinstance(combo_text_var, str): # If it's a string name of a tk variable
# This case is less common if we assign a tk.StringVar() directly
# For safety, one might re-fetch or ensure it's a StringVar instance.
# For now, assuming direct StringVar assignment or it works.
pass
if sorted_names:
self.profile_selection_combo.set(sorted_names[0]) # Set current value using set()
self.run_profile_button.config(state=tk.NORMAL)
self.profile_exec_status_var.set(f"Ready to run profile: {self.profile_selection_combo.get()}")
else:
self.profile_selection_combo.set("")
self.run_profile_button.config(state=tk.DISABLED)
self.profile_exec_status_var.set("No profiles. Create one via 'Profiles > Manage Profiles'.")
def _create_output_log_widgets(self, parent_frame: ttk.Frame): def _create_output_log_widgets(self, parent_frame: ttk.Frame):
"""Crea il Notebook per i log e l'output JSON."""
output_log_notebook = ttk.Notebook(parent_frame) output_log_notebook = ttk.Notebook(parent_frame)
# MODIFIED: sticky per espandere, padx per allineare
output_log_notebook.grid(row=2, column=0, columnspan=1, sticky='nsew', pady=(5,0), padx=0) output_log_notebook.grid(row=2, column=0, columnspan=1, sticky='nsew', pady=(5,0), padx=0)
# Le ScrolledText ora vengono create qui e aggiunte al notebook
self.gdb_raw_output_text = scrolledtext.ScrolledText(output_log_notebook, wrap=tk.WORD, height=10, state=tk.DISABLED, font=("Consolas", 9)) self.gdb_raw_output_text = scrolledtext.ScrolledText(output_log_notebook, wrap=tk.WORD, height=10, state=tk.DISABLED, font=("Consolas", 9))
output_log_notebook.add(self.gdb_raw_output_text, text="GDB Raw Output") output_log_notebook.add(self.gdb_raw_output_text, text="GDB Raw Output")
@ -306,10 +314,8 @@ class GDBGui(tk.Tk):
output_log_notebook.add(self.app_log_text, text="Application Log") output_log_notebook.add(self.app_log_text, text="Application Log")
def _create_status_bar(self, parent_frame: ttk.Frame): def _create_status_bar(self, parent_frame: ttk.Frame):
"""Crea la barra di stato."""
self.status_var = tk.StringVar(value="Ready. Configure GDB via Options menu if needed.") self.status_var = tk.StringVar(value="Ready. Configure GDB via Options menu if needed.")
status_bar = ttk.Label(parent_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) status_bar = ttk.Label(parent_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
# MODIFIED: sticky per espandere orizzontalmente, padx per allineare
status_bar.grid(row=3, column=0, columnspan=1, sticky=(tk.W, tk.E), pady=(5,0), ipady=2, padx=0) status_bar.grid(row=3, column=0, columnspan=1, sticky=(tk.W, tk.E), pady=(5,0), ipady=2, padx=0)
def _setup_logging_redirect_to_gui(self): def _setup_logging_redirect_to_gui(self):
@ -328,16 +334,17 @@ class GDBGui(tk.Tk):
path = filedialog.askopenfilename( path = filedialog.askopenfilename(
title=title, title=title,
filetypes=filetypes or [("All files", "*.*")], filetypes=filetypes or [("All files", "*.*")],
initialdir=initial_dir initialdir=initial_dir,
parent=self
) )
if path: if path:
target_var.set(path) target_var.set(path)
# REMOVED: _browse_gdb_exe and _browse_gdb_script (handled in ConfigWindow) def _browse_target_exe(self):
def _browse_target_exe(self): # This one remains as target exe is configured in main GUI self._browse_file("Select Target Application Executable", self.exe_path_var, [("Executable files", ("*.exe","*")), ("All files", "*.*")])
self._browse_file("Select Target Application Executable", self.exe_path_var, [("Executable files", "*.exe"), ("All files", "*.*")])
def _update_gdb_raw_output(self, text: str, append: bool = True): # No changes def _update_gdb_raw_output(self, text: str, append: bool = True):
if not hasattr(self, 'gdb_raw_output_text') or not self.gdb_raw_output_text.winfo_exists(): return
self.gdb_raw_output_text.config(state=tk.NORMAL) self.gdb_raw_output_text.config(state=tk.NORMAL)
if append: self.gdb_raw_output_text.insert(tk.END, str(text) + "\n") if append: self.gdb_raw_output_text.insert(tk.END, str(text) + "\n")
else: else:
@ -346,7 +353,8 @@ class GDBGui(tk.Tk):
self.gdb_raw_output_text.see(tk.END) self.gdb_raw_output_text.see(tk.END)
self.gdb_raw_output_text.config(state=tk.DISABLED) self.gdb_raw_output_text.config(state=tk.DISABLED)
def _update_parsed_json_output(self, data_to_display: any): # No changes def _update_parsed_json_output(self, data_to_display: any):
if not hasattr(self, 'parsed_json_output_text') or not self.parsed_json_output_text.winfo_exists(): return
self.parsed_json_output_text.config(state=tk.NORMAL) self.parsed_json_output_text.config(state=tk.NORMAL)
self.parsed_json_output_text.delete("1.0", tk.END) 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>") if data_to_display is None: self.parsed_json_output_text.insert("1.0", "<No data to display>")
@ -361,17 +369,22 @@ class GDBGui(tk.Tk):
self.parsed_json_output_text.see("1.0") self.parsed_json_output_text.see("1.0")
self.parsed_json_output_text.config(state=tk.DISABLED) self.parsed_json_output_text.config(state=tk.DISABLED)
def _update_status_bar(self, message: str, is_error: bool = False): # No changes def _update_status_bar(self, message: str, is_error: bool = False):
self.status_var.set(message) if hasattr(self, 'status_var'): self.status_var.set(message)
def _handle_gdb_operation_error(self, operation_name: str, error_details: any): # No changes def _handle_gdb_operation_error(self, operation_name: str, error_details: any):
error_message = f"Error during GDB operation '{operation_name}': {error_details}" error_message = f"Error during GDB operation '{operation_name}': {error_details}"
logger.error(error_message) logger.error(error_message)
self._update_gdb_raw_output(f"ERROR: {error_message}\n", append=True) self._update_gdb_raw_output(f"ERROR: {error_message}\n", append=True)
self._update_status_bar(f"Error: {operation_name} failed.", is_error=True) self._update_status_bar(f"Error: {operation_name} failed.", is_error=True)
messagebox.showerror("GDB Operation Error", error_message) if self.winfo_exists(): # Check if window still exists before showing messagebox
messagebox.showerror("GDB Operation Error", error_message, parent=self)
def _start_gdb_session_action(self): def _start_gdb_session_action(self):
if self.profile_executor_instance and self.profile_executor_instance.is_running:
messagebox.showwarning("Profile Running", "An automated profile is running. Please stop it first.", parent=self)
return
gdb_exe = self.app_settings.get_setting("general", "gdb_executable_path") gdb_exe = self.app_settings.get_setting("general", "gdb_executable_path")
target_exe = self.exe_path_var.get() target_exe = self.exe_path_var.get()
gdb_script = self.app_settings.get_setting("general", "gdb_dumper_script_path") gdb_script = self.app_settings.get_setting("general", "gdb_dumper_script_path")
@ -395,7 +408,6 @@ class GDBGui(tk.Tk):
gdb_script = None gdb_script = None
self.gdb_dumper_status_var.set(f"Dumper: '{self.app_settings.get_setting('general', 'gdb_dumper_script_path')}' (Not Found!)") self.gdb_dumper_status_var.set(f"Dumper: '{self.app_settings.get_setting('general', 'gdb_dumper_script_path')}' (Not Found!)")
if self.gdb_session and self.gdb_session.is_alive(): if self.gdb_session and self.gdb_session.is_alive():
messagebox.showwarning("Session Active", "A GDB session is already active. Please stop it first.", parent=self) messagebox.showwarning("Session Active", "A GDB session is already active. Please stop it first.", parent=self)
return return
@ -404,23 +416,16 @@ class GDBGui(tk.Tk):
self._update_gdb_raw_output("Attempting to start GDB session...\n", append=False) self._update_gdb_raw_output("Attempting to start GDB session...\n", append=False)
self._update_parsed_json_output(None) self._update_parsed_json_output(None)
try: try:
startup_timeout = self.app_settings.get_setting("timeouts", "gdb_start", 30) # Added default for safety startup_timeout = self.app_settings.get_setting("timeouts", "gdb_start", 30)
# MODIFIED: Correctly get the dumper_options dictionary
current_dumper_options = self.app_settings.get_category_settings("dumper_options", {}) current_dumper_options = self.app_settings.get_category_settings("dumper_options", {})
logger.debug(f"Attempting to create GDBSession with dumper_options: type={type(current_dumper_options)}, value='{str(current_dumper_options)[:200]}...'")
self.gdb_session = GDBSession( self.gdb_session = GDBSession(
gdb_path=gdb_exe, gdb_path=gdb_exe,
executable_path=target_exe, executable_path=target_exe,
gdb_script_full_path=gdb_script, gdb_script_full_path=gdb_script,
dumper_options=current_dumper_options dumper_options=current_dumper_options
) )
logger.debug("GDBSession instance created. Calling start()...")
self.gdb_session.start(timeout=startup_timeout) self.gdb_session.start(timeout=startup_timeout)
logger.debug("GDBSession start() returned.")
self._update_gdb_raw_output(f"GDB session started for '{os.path.basename(target_exe)}'.\n") self._update_gdb_raw_output(f"GDB session started for '{os.path.basename(target_exe)}'.\n")
if gdb_script and self.gdb_session.gdb_script_sourced_successfully: if gdb_script and self.gdb_session.gdb_script_sourced_successfully:
@ -431,13 +436,14 @@ class GDBGui(tk.Tk):
self._update_gdb_raw_output(f"Warning: GDB dumper script '{os.path.basename(gdb_script)}' specified but failed to load.\n", append=True) self._update_gdb_raw_output(f"Warning: GDB dumper script '{os.path.basename(gdb_script)}' specified but failed to load.\n", append=True)
self._update_status_bar(f"GDB active. Dumper script issues (check logs).", is_error=True) self._update_status_bar(f"GDB active. Dumper script issues (check logs).", is_error=True)
self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Load Failed!)") self.gdb_dumper_status_var.set(f"Dumper: {os.path.basename(gdb_script)} (Load Failed!)")
messagebox.showwarning("Dumper Script Issue", if self.winfo_exists():
messagebox.showwarning("Dumper Script Issue",
f"The GDB dumper script '{os.path.basename(gdb_script)}' may have failed to load.\n" f"The GDB dumper script '{os.path.basename(gdb_script)}' may have failed to load.\n"
"JSON dumping might be affected. Check logs.", parent=self) "JSON dumping might be affected. Check logs.", parent=self)
else: else:
self._update_gdb_raw_output("No GDB dumper script. JSON dump via script unavailable.\n", append=True) self._update_gdb_raw_output("No GDB dumper script. JSON dump via script unavailable.\n", append=True)
self._update_status_bar("GDB session active. No dumper script.") self._update_status_bar("GDB session active. No dumper script.")
if not self.app_settings.get_setting("general", "gdb_dumper_script_path"): # Check specific setting for this message if not self.app_settings.get_setting("general", "gdb_dumper_script_path"):
self.gdb_dumper_status_var.set("Dumper: Not Configured (Optional).") self.gdb_dumper_status_var.set("Dumper: Not Configured (Optional).")
self.start_gdb_button.config(state=tk.DISABLED) self.start_gdb_button.config(state=tk.DISABLED)
@ -445,12 +451,15 @@ class GDBGui(tk.Tk):
self.run_button.config(state=tk.DISABLED, text="3. Run Program") self.run_button.config(state=tk.DISABLED, text="3. Run Program")
self.dump_var_button.config(state=tk.DISABLED) self.dump_var_button.config(state=tk.DISABLED)
self.stop_gdb_button.config(state=tk.NORMAL) self.stop_gdb_button.config(state=tk.NORMAL)
self.run_profile_button.config(state=tk.DISABLED) # Disable profile run when manual session starts
self.profile_selection_combo.config(state=tk.DISABLED)
self.program_started_once = False self.program_started_once = False
self.last_dumped_data = None self.last_dumped_data = None
self._disable_save_buttons() self._disable_save_buttons()
except (FileNotFoundError, ConnectionError, TimeoutError) as e_specific: except (FileNotFoundError, ConnectionError, TimeoutError) as e_specific:
logger.error(f"Specific error during GDB start: {type(e_specific).__name__}: {e_specific}", exc_info=False)
self._handle_gdb_operation_error("start session", e_specific) self._handle_gdb_operation_error("start session", e_specific)
self.gdb_session = None self.gdb_session = None
self._reset_gui_to_stopped_state() self._reset_gui_to_stopped_state()
@ -459,7 +468,6 @@ class GDBGui(tk.Tk):
self._handle_gdb_operation_error("start session (unexpected from main_window catch-all)", e) self._handle_gdb_operation_error("start session (unexpected from main_window catch-all)", e)
self.gdb_session = None self.gdb_session = None
self._reset_gui_to_stopped_state() self._reset_gui_to_stopped_state()
# _check_critical_configs_and_update_gui()
def _set_gdb_breakpoint_action(self): def _set_gdb_breakpoint_action(self):
if not self.gdb_session or not self.gdb_session.is_alive(): if not self.gdb_session or not self.gdb_session.is_alive():
@ -502,7 +510,9 @@ class GDBGui(tk.Tk):
output = "" output = ""
run_timeout = self.app_settings.get_setting("timeouts", "program_run_continue") run_timeout = self.app_settings.get_setting("timeouts", "program_run_continue")
dumper_script_path = self.app_settings.get_setting("general", "gdb_dumper_script_path") dumper_script_path = self.app_settings.get_setting("general", "gdb_dumper_script_path")
dumper_is_valid = dumper_script_path and os.path.isfile(dumper_script_path) and self.gdb_session.gdb_script_sourced_successfully dumper_is_valid_and_loaded = dumper_script_path and os.path.isfile(dumper_script_path) and \
self.gdb_session and self.gdb_session.gdb_script_sourced_successfully
if not self.program_started_once: if not self.program_started_once:
params_str = self.params_var.get() params_str = self.params_var.get()
@ -516,7 +526,7 @@ class GDBGui(tk.Tk):
self._update_gdb_raw_output(output, append=True) self._update_gdb_raw_output(output, append=True)
dump_button_state = tk.DISABLED dump_button_state = tk.DISABLED
if dumper_is_valid: # Only enable dump if dumper is OK if dumper_is_valid_and_loaded:
dump_button_state = tk.NORMAL dump_button_state = tk.NORMAL
if "Breakpoint" in output or re.search(r"Hit Breakpoint \d+", output, re.IGNORECASE): if "Breakpoint" in output or re.search(r"Hit Breakpoint \d+", output, re.IGNORECASE):
@ -531,7 +541,7 @@ class GDBGui(tk.Tk):
self.program_started_once = False self.program_started_once = False
elif "received signal" in output.lower() or "segmentation fault" in output.lower(): elif "received signal" in output.lower() or "segmentation fault" in output.lower():
self._update_status_bar("Program signal/crash. Check GDB output.", is_error=True) self._update_status_bar("Program signal/crash. Check GDB output.", is_error=True)
self.dump_var_button.config(state=dump_button_state) # Still might want to dump self.dump_var_button.config(state=dump_button_state)
self.program_started_once = True self.program_started_once = True
self.run_button.config(text="3. Continue (Risky)") self.run_button.config(text="3. Continue (Risky)")
else: else:
@ -550,11 +560,12 @@ class GDBGui(tk.Tk):
return return
dumper_script_path = self.app_settings.get_setting("general", "gdb_dumper_script_path") dumper_script_path = self.app_settings.get_setting("general", "gdb_dumper_script_path")
if not dumper_script_path or not os.path.isfile(dumper_script_path) or not self.gdb_session.gdb_script_sourced_successfully: if not dumper_script_path or not os.path.isfile(dumper_script_path) or \
not self.gdb_session.gdb_script_sourced_successfully:
messagebox.showwarning("Dumper Script Error", messagebox.showwarning("Dumper Script Error",
"GDB dumper script is not available, not found, or failed to load.\n" "GDB dumper script is not available, not found, or failed to load.\n"
"JSON dump cannot proceed. Check configuration and logs.", parent=self) "JSON dump cannot proceed. Check configuration and logs.", parent=self)
self._check_critical_configs_and_update_gui() # Re-check to update dumper status display self._check_critical_configs_and_update_gui()
return return
var_expr = self.variable_var.get() var_expr = self.variable_var.get()
@ -590,11 +601,10 @@ class GDBGui(tk.Tk):
self.last_dumped_data = None; self._disable_save_buttons(); self._update_parsed_json_output({"error": str(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): def _reset_gui_to_stopped_state(self):
# Determine if GDB path is currently valid to correctly set start_gdb_button state
gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path") gdb_exe_path = self.app_settings.get_setting("general", "gdb_executable_path")
gdb_is_configured_correctly = gdb_exe_path and os.path.isfile(gdb_exe_path) gdb_is_configured_correctly = gdb_exe_path and os.path.isfile(gdb_exe_path)
if gdb_is_configured_correctly: if gdb_is_configured_correctly and not (self.profile_executor_instance and self.profile_executor_instance.is_running):
self.start_gdb_button.config(state=tk.NORMAL) self.start_gdb_button.config(state=tk.NORMAL)
else: else:
self.start_gdb_button.config(state=tk.DISABLED) self.start_gdb_button.config(state=tk.DISABLED)
@ -604,20 +614,36 @@ class GDBGui(tk.Tk):
self.dump_var_button.config(state=tk.DISABLED) self.dump_var_button.config(state=tk.DISABLED)
self.stop_gdb_button.config(state=tk.DISABLED) self.stop_gdb_button.config(state=tk.DISABLED)
# Enable profile controls if GDB is ok and no profile is running
if gdb_is_configured_correctly and not (self.profile_executor_instance and self.profile_executor_instance.is_running):
if self.profile_selection_combo.get(): # If a profile is selected in combobox
self.run_profile_button.config(state=tk.NORMAL)
else:
self.run_profile_button.config(state=tk.DISABLED)
self.profile_selection_combo.config(state="readonly")
else: # If GDB not ok, or profile is running
self.run_profile_button.config(state=tk.DISABLED)
if self.profile_executor_instance and self.profile_executor_instance.is_running:
self.profile_selection_combo.config(state=tk.DISABLED)
else:
self.profile_selection_combo.config(state="readonly")
self._disable_save_buttons() self._disable_save_buttons()
self.program_started_once = False self.program_started_once = False
self.last_dumped_data = None self.last_dumped_data = None
self._update_status_bar("GDB session stopped or not active.") if not (self.profile_executor_instance and self.profile_executor_instance.is_running):
# REMOVED: self._check_critical_configs_and_update_gui() # This was the source of recursion self._update_status_bar("GDB session stopped or not active.")
# No need to call _check_critical_configs_and_update_gui() here as it caused recursion.
# It's called at specific points like startup or after config changes.
def _stop_gdb_session_action(self): # For manual session
def _stop_gdb_session_action(self):
if self.gdb_session and self.gdb_session.is_alive(): if self.gdb_session and self.gdb_session.is_alive():
self._update_status_bar("Stopping GDB session...") self._update_status_bar("Stopping GDB session...")
try: try:
kill_timeout = self.app_settings.get_setting("timeouts", "kill_program") kill_timeout = self.app_settings.get_setting("timeouts", "kill_program")
quit_timeout = self.app_settings.get_setting("timeouts", "gdb_quit") quit_timeout = self.app_settings.get_setting("timeouts", "gdb_quit")
if self.program_started_once: if self.program_started_once: # Only kill if program was actually run
kill_output = self.gdb_session.kill_program(timeout=kill_timeout) kill_output = self.gdb_session.kill_program(timeout=kill_timeout)
self._update_gdb_raw_output(f"Kill command output:\n{kill_output}\n", append=True) self._update_gdb_raw_output(f"Kill command output:\n{kill_output}\n", append=True)
self.gdb_session.quit(timeout=quit_timeout) self.gdb_session.quit(timeout=quit_timeout)
@ -626,30 +652,33 @@ class GDBGui(tk.Tk):
self._handle_gdb_operation_error("stop session", e) self._handle_gdb_operation_error("stop session", e)
finally: finally:
self.gdb_session = None self.gdb_session = None
self._reset_gui_to_stopped_state() # This will also call _check_critical_configs self._reset_gui_to_stopped_state()
self._load_and_populate_profiles_for_automation_tab() # Re-enable profile UI potentially
else: else:
messagebox.showinfo("Info", "GDB session is not active or already stopped.", parent=self) # messagebox.showinfo("Info", "GDB session is not active or already stopped.", parent=self)
self._reset_gui_to_stopped_state() # Ensure GUI is reset and buttons updated self._reset_gui_to_stopped_state()
self._load_and_populate_profiles_for_automation_tab()
def _enable_save_buttons_if_data(self): # No changes
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): 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) self.save_json_button.config(state=tk.NORMAL)
self.save_csv_button.config(state=tk.NORMAL) self.save_csv_button.config(state=tk.NORMAL)
else: self._disable_save_buttons() else: self._disable_save_buttons()
def _disable_save_buttons(self): # No changes def _disable_save_buttons(self):
self.save_json_button.config(state=tk.DISABLED) self.save_json_button.config(state=tk.DISABLED)
self.save_csv_button.config(state=tk.DISABLED) self.save_csv_button.config(state=tk.DISABLED)
def _save_dumped_data(self, format_type: str): # No changes beyond parent=self for messageboxes def _save_dumped_data(self, format_type: str):
if self.last_dumped_data is None or \ if self.last_dumped_data is None or \
(isinstance(self.last_dumped_data, dict) and "_gdb_tool_error" in self.last_dumped_data): (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.", parent=self) messagebox.showwarning("No Data", "No valid data has been dumped to save.", parent=self)
return return
file_ext = f".{format_type.lower()}"; file_types = [(f"{format_type.upper()} files", f"*{file_ext}"), ("All files", "*.*")] file_ext = f".{format_type.lower()}"; file_types = [(f"{format_type.upper()} files", f"*{file_ext}"), ("All files", "*.*")]
var_name_suggestion = self.variable_var.get().replace(" ", "_").replace("*", "ptr").replace("->", "_") var_name_suggestion = self.variable_var.get().replace(" ", "_").replace("*", "ptr").replace("->", "_").replace(":", "_")
default_filename = f"{var_name_suggestion}_dump{file_ext}" if var_name_suggestion else f"gdb_dump{file_ext}" default_filename = f"{var_name_suggestion}_dump{file_ext}" if var_name_suggestion else f"gdb_dump{file_ext}"
filepath = filedialog.asksaveasfilename(defaultextension=file_ext, filetypes=file_types, title=f"Save Dumped Data as {format_type.upper()}", initialfile=default_filename) filepath = filedialog.asksaveasfilename(defaultextension=file_ext, filetypes=file_types, title=f"Save Dumped Data as {format_type.upper()}", initialfile=default_filename, parent=self)
if not filepath: return if not filepath: return
self._update_status_bar(f"Saving data as {format_type.upper()} to {os.path.basename(filepath)}...") self._update_status_bar(f"Saving data as {format_type.upper()} to {os.path.basename(filepath)}...")
try: try:
@ -668,13 +697,136 @@ class GDBGui(tk.Tk):
messagebox.showerror("Save Error", f"Failed to save data to {filepath}:\n{e}", parent=self) messagebox.showerror("Save Error", f"Failed to save data to {filepath}:\n{e}", parent=self)
self._update_status_bar(f"Error saving data as {format_type}.", is_error=True) self._update_status_bar(f"Error saving data as {format_type}.", is_error=True)
def _gui_status_update(self, message: str) -> None:
if hasattr(self, 'profile_exec_status_var') and self.profile_exec_status_var.get() : # Check if widget exists
self.profile_exec_status_var.set(message)
logger.info(f"ProfileExec Status: {message}")
def _gui_gdb_output_update(self, message: str) -> None:
self._update_gdb_raw_output(message, append=True)
def _gui_json_data_update(self, data: Any) -> None:
self._update_parsed_json_output(data)
def _run_selected_profile_action(self) -> None:
selected_profile_name = self.profile_selection_combo.get()
if not selected_profile_name:
messagebox.showwarning("No Profile Selected", "Please select a profile to run.", parent=self)
return
if self.profile_executor_instance and self.profile_executor_instance.is_running:
messagebox.showwarning("Profile Running", "A profile is already running. Please stop it first.", parent=self)
return
if self.gdb_session and self.gdb_session.is_alive(): # Manual session active
messagebox.showerror("GDB Session Active", "A manual GDB session is active. Please stop it first via 'Manual Debug' tab.", parent=self)
return
profile_data = self.available_profiles_map.get(selected_profile_name)
if not profile_data:
messagebox.showerror("Error", f"Could not find data for profile '{selected_profile_name}'.", parent=self)
return
self.start_gdb_button.config(state=tk.DISABLED)
self.set_bp_button.config(state=tk.DISABLED)
self.run_button.config(state=tk.DISABLED)
self.dump_var_button.config(state=tk.DISABLED)
self.stop_gdb_button.config(state=tk.DISABLED)
self.run_profile_button.config(state=tk.DISABLED)
self.stop_profile_button.config(state=tk.NORMAL)
self.profile_selection_combo.config(state=tk.DISABLED)
try: # Disable menu items
self.menubar.entryconfig("Profiles", state=tk.DISABLED)
self.menubar.entryconfig("Options", state=tk.DISABLED)
except tk.TclError: pass # In case menubar or entries are not yet fully available
self.profile_executor_instance = ProfileExecutor(
profile_data,
self.app_settings,
status_update_callback=self._gui_status_update,
gdb_output_callback=self._gui_gdb_output_update,
json_output_callback=self._gui_json_data_update
)
self.profile_exec_status_var.set(f"Starting profile '{selected_profile_name}' in background...")
# Clear previous outputs before starting a new profile run
self._update_gdb_raw_output("", append=False)
self._update_parsed_json_output(None)
executor_thread = threading.Thread(target=self._profile_executor_thread_target, daemon=True)
executor_thread.start()
def _profile_executor_thread_target(self):
if self.profile_executor_instance:
try:
self.profile_executor_instance.run()
finally:
if self.winfo_exists(): # Check if main window still exists
self.after(0, self._on_profile_execution_finished)
def _on_profile_execution_finished(self):
if not self.winfo_exists(): return # Window might have been destroyed
# Get final status from executor instance if it's still around and set it.
# The instance might have already updated profile_exec_status_var directly.
final_status_message = "Profile execution finished."
if self.profile_executor_instance and hasattr(self.profile_executor_instance, 'status_updater'):
# This is a bit indirect; ideally, status_updater already set the final message
# or executor.run() returns a final status. For now, we take the current var value.
current_status = self.profile_exec_status_var.get()
if "Error:" in current_status or "failed" in current_status.lower():
final_status_message = f"Profile finished with issues: {current_status}"
elif current_status and not current_status.startswith("Starting profile"): # If not default starting msg
final_status_message = f"Profile run completed. Last status: {current_status}"
self.profile_exec_status_var.set(final_status_message)
# Re-enable UI components
self.run_profile_button.config(state=tk.NORMAL if self.profile_selection_combo.get() else tk.DISABLED)
self.stop_profile_button.config(state=tk.DISABLED)
self.profile_selection_combo.config(state="readonly")
try:
self.menubar.entryconfig("Profiles", state=tk.NORMAL)
self.menubar.entryconfig("Options", state=tk.NORMAL)
except tk.TclError: pass
self._check_critical_configs_and_update_gui() # This will re-evaluate manual GDB buttons state
self.profile_executor_instance = None
def _stop_current_profile_action(self) -> None:
if self.profile_executor_instance and self.profile_executor_instance.is_running:
self.profile_exec_status_var.set("Requesting profile stop...")
self.profile_executor_instance.request_stop()
self.stop_profile_button.config(state=tk.DISABLED)
else:
self.profile_exec_status_var.set("No profile currently running to stop.")
def _on_closing_window(self): def _on_closing_window(self):
logger.info("Window closing sequence initiated.") logger.info("Window closing sequence initiated.")
# MODIFIED: Only save settings that are still managed by main_window's UI active_profile_stop_requested = False
if self.profile_executor_instance and self.profile_executor_instance.is_running:
response = messagebox.askyesnocancel("Profile Running",
"An automated profile is currently running.\n"
"Do you want to stop it and exit?",
default=messagebox.CANCEL, parent=self)
if response is True:
self._stop_current_profile_action()
active_profile_stop_requested = True
# Give a small grace period for the thread to notice the stop request.
# This is not a perfect solution for immediate shutdown.
# A more robust solution would involve joining the thread with a timeout,
# or a more direct way to interrupt gdb_session.
# For now, we'll proceed after requesting stop.
logger.info("Requested stop for active profile. Proceeding with shutdown.")
elif response is None:
logger.info("User cancelled exit while automated profile is running.")
return
# If False (No), user wants to exit without stopping.
self.app_settings.set_setting("gui", "main_window_geometry", self.geometry()) self.app_settings.set_setting("gui", "main_window_geometry", self.geometry())
# GDB exe and dumper script paths are no longer in main_window StringVars directly tied to AppSettings here.
# They are managed via ConfigWindow.
self.app_settings.set_setting("general", "last_target_executable_path", self.exe_path_var.get()) self.app_settings.set_setting("general", "last_target_executable_path", self.exe_path_var.get())
self.app_settings.set_setting("general", "default_breakpoint", self.breakpoint_var.get()) self.app_settings.set_setting("general", "default_breakpoint", self.breakpoint_var.get())
self.app_settings.set_setting("general", "default_variable_to_dump", self.variable_var.get()) self.app_settings.set_setting("general", "default_variable_to_dump", self.variable_var.get())
@ -682,31 +834,40 @@ class GDBGui(tk.Tk):
save_success = self.app_settings.save_settings() save_success = self.app_settings.save_settings()
if not save_success: if not save_success:
messagebox.showwarning("Settings Error", "Could not save application settings. Check logs.", parent=self) if self.winfo_exists(): messagebox.showwarning("Settings Error", "Could not save application settings. Check logs.", parent=self)
should_destroy = True should_destroy = True
if self.gdb_session and self.gdb_session.is_alive(): 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?", parent=self): if self.winfo_exists() and messagebox.askokcancel("Quit GDB Session", "A manual GDB session is active. Stop it and exit?", parent=self):
logger.info("User chose to stop active GDB session and exit.") logger.info("User chose to stop active manual GDB session and exit.")
self._stop_gdb_session_action() self._stop_gdb_session_action()
else: elif self.winfo_exists(): # User chose not to stop manual GDB
logger.info("User cancelled exit while GDB session is active.") logger.info("User cancelled exit while manual GDB session is active.")
should_destroy = False should_destroy = False
elif not self.winfo_exists(): # Window already gone
self._stop_gdb_session_action() # Try to stop anyway
if should_destroy: if should_destroy:
logger.info("Proceeding with window destruction.") logger.info("Proceeding with window destruction.")
if self.gui_log_handler: if self.gui_log_handler:
logger.debug("Removing GUI log handler.")
logging.getLogger().removeHandler(self.gui_log_handler) logging.getLogger().removeHandler(self.gui_log_handler)
self.gui_log_handler.close() self.gui_log_handler.close()
self.gui_log_handler = None self.gui_log_handler = None # Important to break reference
# Ensure the executor thread has a chance to finish if stop was requested
# This is still a bit tricky if the executor is deep in a blocking GDB call.
if active_profile_stop_requested:
# A join with timeout could be attempted on the thread here if we stored the thread.
# For now, rely on daemon=True and the request_stop flag.
logger.debug("Assuming profile executor thread will terminate due to stop request or daemon nature.")
self.destroy() self.destroy()
logger.info("Tkinter window destroyed.") logger.info("Tkinter window destroyed.")
else: else:
logger.info("Window destruction aborted by user.") logger.info("Window destruction aborted by user.")
class ScrolledTextLogHandler(logging.Handler): # No changes class ScrolledTextLogHandler(logging.Handler):
def __init__(self, text_widget: scrolledtext.ScrolledText): def __init__(self, text_widget: scrolledtext.ScrolledText):
super().__init__() super().__init__()
self.text_widget = text_widget self.text_widget = text_widget
@ -719,12 +880,12 @@ class ScrolledTextLogHandler(logging.Handler): # No changes
self.text_widget.insert(tk.END, log_entry + "\n") self.text_widget.insert(tk.END, log_entry + "\n")
self.text_widget.see(tk.END) self.text_widget.see(tk.END)
self.text_widget.config(state=tk.DISABLED) self.text_widget.config(state=tk.DISABLED)
except tk.TclError as e: except tk.TclError as e: # Handle widget destroyed error
print(f"ScrolledTextLogHandler TclError (widget likely destroyed): {e} - Record: {self.format(record)}") # print(f"ScrolledTextLogHandler TclError (widget likely destroyed): {e} - Record: {self.format(record)}")
self._active = False self._active = False # Stop trying to use a dead widget
except Exception as e: except Exception as e:
print(f"ScrolledTextLogHandler unexpected error: {e} - Record: {self.format(record)}") # print(f"ScrolledTextLogHandler unexpected error: {e} - Record: {self.format(record)}")
self._active = False self._active = False
def close(self): def close(self):
self._active = False self._active = False
super().close() super().close()