diff --git a/cpp_python_debug/__main__.py b/cpp_python_debug/__main__.py index efbbba9..b7715ef 100644 --- a/cpp_python_debug/__main__.py +++ b/cpp_python_debug/__main__.py @@ -4,55 +4,86 @@ import logging import sys # For stream handler to output to console +import os # For path manipulation and log file deletion # Use relative import to get the GDBGui class from the gui subpackage from .gui.main_window import GDBGui -# --- Application Constants (Optional) --- +# --- Application Constants --- APP_NAME = "Cpp-Python GDB Debug Helper" APP_VERSION = "1.1.0" # Example version +# --- PERCORSO DEL FILE DI LOG DEL DUMPER --- +# Questo percorso deve essere coerente con come GDB_DUMPER_LOG_PATH è definito in gdb_dumper.py +GDB_DUMPER_LOG_TO_DELETE = None # Inizializza a None +try: + # Se __file__ è definito per __main__.py (di solito lo è) + if '__file__' in globals(): + # Assumendo la struttura: + # cpp_python_debug/ + # |-- __main__.py (questo file, os.path.dirname(__file__) è cpp_python_debug/) + # |-- core/ + # |-- gdb_dumper.py (e gdb_dumper_debug.log viene creato qui) + # |-- ... + # |-- gui/ + # |-- ... + main_script_dir = os.path.dirname(os.path.abspath(__file__)) + gdb_dumper_script_dir = os.path.join(main_script_dir, "core") + GDB_DUMPER_LOG_TO_DELETE = os.path.join(gdb_dumper_script_dir, "gdb_dumper_debug.log") + else: + # Fallback se __file__ non è definito (raro per __main__.py) + # In questo caso, assumiamo che gdb_dumper.py scriva nella home dell'utente + # come suo fallback. + GDB_DUMPER_LOG_TO_DELETE = os.path.join(os.path.expanduser("~"), "gdb_dumper_debug.log") +except Exception: + # Se c'è un errore nel determinare il percorso, non tentare di cancellare. + # GDB_DUMPER_LOG_TO_DELETE rimarrà None. + pass + + def setup_global_logging(): """ Configures basic logging for the application. - Logs to console (stdout). - GUI will have its own handler to display logs within the application window. """ - # Define the root logger level. DEBUG will capture everything. - # Specific handlers can have their own, more restrictive levels. + # Definizione di log_level qui, così è nello scope corretto log_level = logging.DEBUG # Or logging.INFO for less verbosity in console - # Basic configuration for the root logger - # This will set up a default StreamHandler to sys.stderr if no handlers are added. - # We will add our own StreamHandler to sys.stdout for better control. logging.basicConfig(level=log_level, format='%(asctime)s - %(name)-25s - %(levelname)-8s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') - # If you want to ensure console output goes to stdout instead of stderr (default for basicConfig warnings/errors) - # and have more control over the console handler: - # 1. Get the root logger - root_logger = logging.getLogger() - # 2. Remove any default handlers basicConfig might have added (if any, depends on Python version behavior) - # For safety, clear existing handlers if you want to be sure only yours are present. + # Opzionale: Se si vuole un controllo più fine sull'handler della console (es. stdout invece di stderr) + # root_logger = logging.getLogger() + # # Rimuovi handler di default se presenti e se si vuole solo il proprio # for handler in root_logger.handlers[:]: # root_logger.removeHandler(handler) - - # 3. Add your custom console StreamHandler # console_handler = logging.StreamHandler(sys.stdout) # console_formatter = logging.Formatter('%(asctime)s - %(name)-25s - %(levelname)-8s - %(message)s', # datefmt='%Y-%m-%d %H:%M:%S') # console_handler.setFormatter(console_formatter) - # console_handler.setLevel(log_level) # Set level for this specific handler + # console_handler.setLevel(log_level) # root_logger.addHandler(console_handler) - # The ScrolledTextLogHandler in main_window.py will be added to the root logger - # when the GDBGui instance is created, so logs will also go to the GUI. - - logger = logging.getLogger(__name__) # Get a logger for this module + logger = logging.getLogger(__name__) # Logger per questo modulo (__main__) logger.info(f"Global logging initialized. Console log level: {logging.getLevelName(log_level)}.") logger.info(f"Starting {APP_NAME} v{APP_VERSION}") + # --- CANCELLA IL VECCHIO LOG DEL DUMPER --- + if GDB_DUMPER_LOG_TO_DELETE: # Controlla se il percorso è stato determinato + if os.path.exists(GDB_DUMPER_LOG_TO_DELETE): + try: + os.remove(GDB_DUMPER_LOG_TO_DELETE) + logger.info(f"Previous GDB dumper log deleted: {GDB_DUMPER_LOG_TO_DELETE}") + except Exception as e_remove: + logger.warning(f"Could not delete previous GDB dumper log ('{GDB_DUMPER_LOG_TO_DELETE}'): {e_remove}") + else: + logger.info(f"GDB dumper log not found (no previous log to delete): {GDB_DUMPER_LOG_TO_DELETE}") + else: + logger.warning("Path for GDB dumper log to delete could not be determined.") + # --- FINE CANCELLAZIONE --- + def main(): """ @@ -60,43 +91,35 @@ def main(): """ setup_global_logging() # Initialize logging first - # Get a logger for the main execution scope - # (using __name__ here would give 'cpp_python_debug.__main__') - app_logger = logging.getLogger(APP_NAME) # Or use a more generic name + app_logger = logging.getLogger(APP_NAME) # Logger per l'applicazione in generale try: app_logger.info("Creating GDBGui instance...") - app = GDBGui() # Create the main application window + app = GDBGui() app_logger.info("Starting Tkinter main event loop.") - app.mainloop() # Start the Tkinter event loop + app.mainloop() app_logger.info("Tkinter main event loop finished.") except Exception as e: - # Catch any unhandled exceptions during GUI initialization or runtime - # that might not be caught by Tkinter's own error handling. app_logger.critical(f"An unhandled critical error occurred: {e}", exc_info=True) - # Optionally, show a simple error dialog if Tkinter is still somewhat functional - # or if it fails before Tkinter is even up. - # For console-based launch, the log to console is key. try: + # Prova a mostrare un errore Tkinter se possibile import tkinter as tk from tkinter import messagebox - # Check if a root window exists or can be created - # This is a bit of a hack; ideally, errors are caught within the GUI logic. root_for_error = tk.Tk() - root_for_error.withdraw() # Hide the empty root window + root_for_error.withdraw() messagebox.showerror("Critical Application Error", f"A critical error occurred:\n{e}\n\nPlease check the logs for more details.") root_for_error.destroy() except Exception: - pass # If Tkinter itself is broken, can't show a Tkinter messagebox + # Se Tkinter stesso è rotto, non si può fare molto altro che loggare + pass finally: app_logger.info(f"{APP_NAME} is shutting down.") - logging.shutdown() # Cleanly close all logging handlers + logging.shutdown() # Chiude gli handler di logging if __name__ == "__main__": - # This allows the script to be run directly (e.g., python __main__.py) - # as well as via `python -m cpp_python_debug`. + # Permette l'esecuzione come script diretto o come modulo main() \ No newline at end of file diff --git a/cpp_python_debug/core/gdb_controller.py b/cpp_python_debug/core/gdb_controller.py index 8c9871e..b8e4695 100644 --- a/cpp_python_debug/core/gdb_controller.py +++ b/cpp_python_debug/core/gdb_controller.py @@ -6,6 +6,7 @@ import re import wexpect import logging import json # For parsing JSON output from the GDB script +import time # <<<<---- IMPORTANTE: AGGIUNTO PER IL DEBUG DI QUIT logger = logging.getLogger(__name__) @@ -15,16 +16,6 @@ class GDBSession: set breakpoints, run the target, and dump variables using a custom GDB Python script. """ def __init__(self, gdb_path: str, executable_path: str, gdb_script_full_path: str = None, timeout: int = 30): - """ - Initializes the GDBSession. - - Args: - gdb_path: Path to the GDB executable. - executable_path: Path to the target executable to debug. - gdb_script_full_path: Absolute path to the GDB Python dumper script (e.g., gdb_dumper.py). - If None, JSON dumping capabilities will be limited. - timeout: Default timeout for GDB command expectations. - """ if not os.path.exists(gdb_path): msg = f"GDB executable not found at: {gdb_path}" logger.error(msg) @@ -34,227 +25,149 @@ class GDBSession: logger.error(msg) raise FileNotFoundError(msg) if gdb_script_full_path and not os.path.exists(gdb_script_full_path): - # Log a warning instead of raising an error, to allow basic GDB usage - # if the script is missing for some reason. The GUI should inform the user. logger.warning(f"GDB Python dumper script not found at: {gdb_script_full_path}. Advanced JSON dumping will be unavailable.") - gdb_script_full_path = None # Treat as if not provided + gdb_script_full_path = None self.gdb_path = gdb_path self.executable_path = executable_path - self.gdb_script_path = gdb_script_full_path # Store the path to our dumper script + self.gdb_script_path = gdb_script_full_path self.timeout = timeout - self.child = None # The wexpect child process - self.gdb_prompt = "(gdb) " # Standard GDB prompt + self.child = None + self.gdb_prompt = "(gdb) " self.gdb_script_sourced_successfully = False logger.info(f"GDBSession initialized. GDB: '{gdb_path}', Executable: '{executable_path}', DumperScript: '{gdb_script_full_path}'") def start(self) -> None: - """ - Starts the GDB subprocess and waits for the initial prompt. - Also attempts to source the custom GDB Python script if provided. - """ command = f'"{self.gdb_path}" --nx --quiet "{self.executable_path}"' - # --nx: Do not execute commands from any .gdbinit files. - # --quiet: Do not print the introductory and copyright messages. logger.info(f"Spawning GDB process: {command}") try: self.child = wexpect.spawn(command, timeout=self.timeout, encoding='utf-8', errors='replace') - self.child.expect_exact(self.gdb_prompt, timeout=max(self.timeout, 10)) # Longer timeout for initial startup + self.child.expect_exact(self.gdb_prompt, timeout=max(self.timeout, 15)) # Increased timeout slightly logger.info("GDB started successfully and prompt received.") - - # Attempt to source the custom GDB Python script if a path was provided + + logger.info("Disabling GDB pagination ('set pagination off').") + # Inviamo il comando e ci aspettiamo un altro prompt. + # Usiamo un timeout più breve per questo comando di setup. + self.send_cmd("set pagination off", expect_prompt=True, timeout=max(5, self.timeout // 2)) + logger.info("GDB pagination disabled.") + if self.gdb_script_path: self._source_gdb_dumper_script() else: logger.info("No GDB dumper script path provided; skipping sourcing.") - except wexpect.TIMEOUT: error_msg = "Timeout waiting for GDB prompt on start." logger.error(error_msg) - # Try to get some output for diagnostics try: debug_output = self.child.read_nonblocking(size=2048, timeout=1) logger.error(f"GDB output before timeout: {debug_output}") - except Exception: - pass - raise TimeoutError(error_msg) # Use standard Python TimeoutError + except Exception: pass + raise TimeoutError(error_msg) except Exception as e: logger.error(f"Failed to spawn or initialize GDB: {e}", exc_info=True) - raise ConnectionError(f"Failed to spawn or initialize GDB: {e}") # Use standard ConnectionError + raise ConnectionError(f"Failed to spawn or initialize GDB: {e}") def _source_gdb_dumper_script(self) -> None: - """ - Internal method to send the 'source' command to GDB for the custom dumper script. - """ - if not self.gdb_script_path or not self.child: - return + if not self.gdb_script_path or not self.child: return normalized_script_path = self.gdb_script_path.replace('\\', '/') logger.info(f"Sourcing GDB Python script: {normalized_script_path}") try: - output_before_prompt = self.send_cmd(f'source "{normalized_script_path}"') + source_command = f'source {normalized_script_path}' # No quotes as per previous findings + logger.debug(f"Constructed source command: [{source_command}]") + output_before_prompt = self.send_cmd(source_command) - # --- LOGICA DI CONTROLLO ERRORI MIGLIORATA --- error_detected = False - # Controlla errori Python standard if "Traceback (most recent call last):" in output_before_prompt or \ "SyntaxError:" in output_before_prompt: logger.error(f"Python error detected while sourcing GDB script '{normalized_script_path}':\n{output_before_prompt}") error_detected = True - # Controlla errori GDB comuni come "No such file" o "Error:" all'inizio della riga - # GDB può avere output multiriga, quindi controlliamo ogni riga per certi pattern - # o la prima riga significativa. - # L'output "No such file or directory." è spesso preceduto dal path stesso. - # Esempio: '"path/to/script.py": No such file or directory.' - # Usiamo una regex per questo. - # Il messaggio di errore da GDB è "nomefile: messaggio errore." - # es: "C:/path/to/file.py: No such file or directory." - # es: "Error: an error message from gdb." - # es: "Script Error: some error message from a python script sourced." (improbabile con "source") - - # Pattern per "filename: No such file or directory." - # \S+ matches one or more non-whitespace characters (for the filename part) - no_such_file_pattern = re.compile(r""" - ^ # Inizio della riga (con re.MULTILINE) o inizio della stringa - "? # Opzionali virgolette all'inizio - \S+ # Il nome del file/percorso - "? # Opzionali virgolette alla fine - :\s* # Due punti seguito da zero o più spazi - No\s+such\s+file\s+or\s+directory # Il messaggio di errore - """, re.VERBOSE | re.MULTILINE) # re.MULTILINE per far funzionare ^ per ogni riga - - # Pattern per "Error: " all'inizio di una riga + no_such_file_pattern = re.compile(r"""^"?\S+"?:\s*No\s+such\s+file\s+or\s+directory""", re.VERBOSE | re.MULTILINE) gdb_error_pattern = re.compile(r"^Error:", re.MULTILINE) - if no_such_file_pattern.search(output_before_prompt): logger.error(f"GDB error 'No such file or directory' for script '{normalized_script_path}':\n{output_before_prompt}") error_detected = True elif gdb_error_pattern.search(output_before_prompt): - # Cattura "Error: " generico se non è un errore Python o "No such file" - # Questo potrebbe essere troppo generico, ma è un tentativo. - # Potrebbe essere meglio controllare la prima riga di output_before_prompt. first_line_output = output_before_prompt.splitlines()[0] if output_before_prompt.splitlines() else "" if first_line_output.strip().startswith("Error:"): logger.error(f"GDB 'Error:' detected while sourcing script '{normalized_script_path}':\n{output_before_prompt}") error_detected = True - if error_detected: self.gdb_script_sourced_successfully = False else: logger.info(f"GDB script '{normalized_script_path}' sourced (no critical errors detected in output).") self.gdb_script_sourced_successfully = True - # --- FINE LOGICA DI CONTROLLO ERRORI MIGLIORATA --- - except Exception as e: logger.error(f"Exception during 'source' command for GDB script '{normalized_script_path}': {e}", exc_info=True) self.gdb_script_sourced_successfully = False - def send_cmd(self, command: str, expect_prompt: bool = True, timeout: int = None) -> str: - """ - Sends a command to the GDB subprocess and returns its output. - # ... (docstring come prima) ... - """ if not self.child or not self.child.isalive(): logger.error("GDB session not started or is dead. Cannot send command.") raise ConnectionError("GDB session not active.") - effective_timeout = timeout if timeout is not None else self.timeout logger.debug(f"Sending GDB command: '{command}' with timeout: {effective_timeout}s") - try: self.child.sendline(command) if expect_prompt: index = self.child.expect_exact([self.gdb_prompt, wexpect.EOF, wexpect.TIMEOUT], timeout=effective_timeout) - output_before = self.child.before - - if index == 0: # Matched GDB prompt - # --- CORREZIONE QUI --- + if index == 0: logger.debug(f"GDB output for '{command}':\n{output_before.rstrip ('\r\n')}") - # --- FINE CORREZIONE --- return output_before - elif index == 1: # Matched EOF - logger.error(f"GDB exited unexpectedly (EOF) after command: {command}. Output: {output_before.rstrip('\r\n')}") # Aggiunto rstrip anche qui per pulizia log - self.child.close() + elif index == 1: + logger.error(f"GDB exited unexpectedly (EOF) after command: {command}. Output: {output_before.rstrip('\r\n')}") + self.child.close() raise wexpect.EOF(f"GDB exited unexpectedly after command: {command}") - elif index == 2: # Matched TIMEOUT - logger.error(f"Timeout ({effective_timeout}s) executing GDB command: {command}. Output so far: {output_before.rstrip ('\r\n')}") # Aggiunto rstrip anche qui + elif index == 2: + logger.error(f"Timeout ({effective_timeout}s) executing GDB command: {command}. Output so far: {output_before.rstrip ('\r\n')}") current_output = output_before - try: - current_output += self.child.read_nonblocking(size=4096, timeout=1) - except Exception: - pass - raise TimeoutError(f"Timeout executing GDB command: {command}. Partial output: {current_output.rstrip('\r\n')}") # Aggiunto rstrip anche qui + try: current_output += self.child.read_nonblocking(size=4096, timeout=0.5) # Shorter timeout for nonblocking read + except Exception: pass + raise TimeoutError(f"Timeout executing GDB command: {command}. Partial output: {current_output.rstrip('\r\n')}") return "" - except (wexpect.TIMEOUT, TimeoutError) as e: + except (wexpect.TIMEOUT, TimeoutError) as e: logger.error(f"Timeout during GDB command: {command} -> {e}", exc_info=True) raise TimeoutError(f"Timeout during GDB command: {command}") from e except wexpect.EOF as e: logger.error(f"GDB EOF during command: {command} -> {e}", exc_info=True) - self.child.close() + if self.child and self.child.isalive(): self.child.close() # Ensure close if possible raise except Exception as e: logger.error(f"Generic error during GDB command: {command} -> {e}", exc_info=True) raise ConnectionError(f"Error during GDB command '{command}': {e}") from e def set_breakpoint(self, location: str) -> str: - """Sets a breakpoint in GDB.""" logger.info(f"Setting breakpoint at: {location}") return self.send_cmd(f"break {location}") def run_program(self, params: str = "") -> str: - """Runs the program in GDB with optional parameters.""" run_command = "run" - if params: - run_command += f" {params.strip()}" + if params: run_command += f" {params.strip()}" logger.info(f"Running program in GDB: '{run_command}'") - # 'run' can take a long time and output varies (hit breakpoint, exit, crash) - # We expect the (gdb) prompt if it hits a breakpoint or after it exits normally. - # Timeout should be generous. return self.send_cmd(run_command, timeout=max(self.timeout, 120)) def continue_execution(self) -> str: - """Continues program execution in GDB.""" logger.info("Continuing program execution in GDB.") return self.send_cmd("continue", timeout=max(self.timeout, 120)) def dump_variable_to_json(self, var_name: str) -> dict: - """ - Dumps a variable using the 'dump_json' command (from the sourced GDB script) - and parses the JSON output. - - Args: - var_name: The name of the variable or expression to dump. - - Returns: - A Python dictionary parsed from the JSON output. - If an error occurs, returns a dictionary алкоголь error information. - """ if not self.gdb_script_sourced_successfully: logger.warning(f"GDB dumper script was not sourced successfully. Cannot dump '{var_name}' to JSON.") return {"_gdb_tool_error": "GDB dumper script not available or failed to load."} - logger.info(f"Dumping variable '{var_name}' to JSON using 'dump_json' GDB command.") try: - # The 'dump_json' command is expected to print delimiters START_JSON_OUTPUT and END_JSON_OUTPUT - # The timeout here might need to be quite generous for complex variables. raw_gdb_output = self.send_cmd(f"dump_json {var_name}", expect_prompt=True, timeout=max(self.timeout, 60)) - - # Extract the JSON string between the delimiters - # Using re.DOTALL to make '.' match newlines match = re.search(r"START_JSON_OUTPUT\s*([\s\S]*?)\s*END_JSON_OUTPUT", raw_gdb_output, re.DOTALL) - if match: json_str = match.group(1).strip() - logger.debug(f"JSON string received from GDB 'dump_json': {json_str[:500]}...") # Log start of JSON + logger.debug(f"JSON string received from GDB 'dump_json': {json_str[:500]}...") try: parsed_data = json.loads(json_str) - # Check if the parsed data itself indicates an error from the GDB script if isinstance(parsed_data, dict) and "gdb_script_error" in parsed_data: logger.error(f"Error reported by GDB dumper script for '{var_name}': {parsed_data['gdb_script_error']}") return parsed_data @@ -264,30 +177,25 @@ class GDBSession: else: logger.error(f"Delimiters START_JSON_OUTPUT/END_JSON_OUTPUT not found in 'dump_json' output for '{var_name}'.") logger.debug(f"Full GDB output for 'dump_json {var_name}':\n{raw_gdb_output}") - # Check if the raw output itself contains a GDB or Python script error message if "Traceback (most recent call last):" in raw_gdb_output or \ - "gdb.error:" in raw_gdb_output or "Error:" in raw_gdb_output.splitlines()[0]: + "gdb.error:" in raw_gdb_output or ("Error:" in raw_gdb_output.splitlines()[0] if raw_gdb_output.splitlines() else False) : return {"_gdb_tool_error": "Error during GDB 'dump_json' script execution", "raw_gdb_output": raw_gdb_output} return {"_gdb_tool_error": "JSON delimiters not found in GDB script output", "raw_gdb_output": raw_gdb_output} - - except TimeoutError: # Catch timeout specific to this command + except TimeoutError: logger.error(f"Timeout dumping variable '{var_name}' with 'dump_json'.") return {"_gdb_tool_error": f"Timeout during GDB 'dump_json {var_name}' command"} - except Exception as e: # Catch other errors from send_cmd or this method + except Exception as e: logger.error(f"Generic exception dumping variable '{var_name}' with 'dump_json': {e}", exc_info=True) return {"_gdb_tool_error": f"Generic exception during 'dump_json {var_name}': {str(e)}"} def kill_program(self) -> str: - """Kills the currently running program in GDB, if any.""" logger.info("Sending 'kill' command to GDB.") try: - # 'kill' might not always return the prompt immediately if the program - # wasn't running or if GDB needs time. - # A short timeout is usually sufficient. - return self.send_cmd("kill", expect_prompt=True, timeout=10) + # Increased timeout for kill, as it might take time for GDB to process + return self.send_cmd("kill", expect_prompt=True, timeout=max(self.timeout, 20)) except (TimeoutError, wexpect.EOF) as e: logger.warning(f"Exception during 'kill' (might be normal if program not running or GDB exits): {e}") - return f"" # Return some info + return f"" except ConnectionError: logger.warning("Cannot send 'kill', GDB session not active.") return "" @@ -295,51 +203,65 @@ class GDBSession: logger.error(f"Unexpected error during 'kill': {e}", exc_info=True) return f"" + # --- DEBUG VERSION of quit() --- def quit(self) -> None: - """Quits the GDB session gracefully.""" if self.child and self.child.isalive(): - logger.info("Quitting GDB session.") + logger.info("DEBUG: Attempting simplified GDB quit.") try: self.child.sendline("quit") - # GDB might ask "Quit anyway? (y or n)" if a program is running or has state. - # It might also just quit if nothing is active. - # It might also timeout if it's stuck. - patterns = [ - r"Quit anyway\?\s*\(y or n\)\s*", # GDB confirmation prompt - self.gdb_prompt, # GDB prompt (if it didn't ask for confirmation but didn't quit yet) - wexpect.EOF, # GDB exited - wexpect.TIMEOUT # Timeout - ] - index = self.child.expect(patterns, timeout=10) # Generous timeout for quit + logger.info("DEBUG: Sent 'quit'. Waiting to see GDB's reaction.") + + time.sleep(0.5) # Give GDB a moment to respond + + gdb_response_after_quit = "" + try: + gdb_response_after_quit = self.child.read_nonblocking(size=2048, timeout=2) + logger.info(f"DEBUG: GDB response after 'quit' command: {gdb_response_after_quit!r}") + except wexpect.TIMEOUT: + logger.info("DEBUG: Timeout reading GDB response after 'quit' command.") + except Exception as e_read_quit: + logger.info(f"DEBUG: Error reading GDB response after 'quit': {e_read_quit}") - if index == 0: # Matched "Quit anyway?" - logger.info("GDB asked for quit confirmation. Sending 'y'.") + if "Quit anyway?" in gdb_response_after_quit: # Check if confirmation is needed + logger.info("DEBUG: GDB asked for quit confirmation. Sending 'y'.") self.child.sendline("y") - # Expect EOF or another prompt briefly - self.child.expect([wexpect.EOF, self.gdb_prompt, wexpect.TIMEOUT], timeout=5) - elif index == 1: # Matched GDB prompt (shouldn't happen if quit is effective) - logger.warning("GDB prompt received after 'quit' command. Forcing close.") - elif index == 2: # Matched EOF - logger.info("GDB exited after 'quit' command (EOF).") - elif index == 3: # Matched TIMEOUT - logger.warning("Timeout waiting for GDB to quit. Forcing close.") - - except (wexpect.TIMEOUT, wexpect.EOF, TimeoutError) as e: # Catch our TimeoutError too - logger.warning(f"Exception during GDB quit sequence (EOF or Timeout is often normal here): {e}") - except Exception as e: # Other unexpected errors - logger.error(f"Error during GDB quit sequence: {e}", exc_info=True) - finally: - if self.child and self.child.isalive(): - logger.info("GDB still alive after quit sequence; forcing close.") - self.child.close(force=True) + time.sleep(0.5) # Give GDB a moment for 'y' + gdb_response_after_y = "" + try: + gdb_response_after_y = self.child.read_nonblocking(size=2048, timeout=2) + logger.info(f"DEBUG: GDB response after 'y': {gdb_response_after_y!r}") + except wexpect.TIMEOUT: + logger.info("DEBUG: Timeout reading GDB response after 'y'.") + except Exception as e_read_y: + logger.info(f"DEBUG: Error reading GDB response after 'y': {e_read_y}") + + # Check if GDB is still alive after attempting to quit + time.sleep(1) # Wait a bit more to ensure GDB has time to exit + if self.child.isalive(): + logger.warning("DEBUG: GDB is still alive after quit sequence. Attempting child.close().") + try: + self.child.close() # No 'force' argument + if self.child.isalive(): + logger.error("DEBUG: GDB still alive even after child.close(). Process may be orphaned.") + else: + logger.info("DEBUG: child.close() seems to have terminated GDB.") + except Exception as e_close: + logger.error(f"DEBUG: Exception during child.close(): {e_close}", exc_info=True) else: - logger.info("GDB process terminated or already closed.") - self.child = None - self.gdb_script_sourced_successfully = False # Reset flag + logger.info("DEBUG: GDB process appears to have exited successfully after quit sequence.") + + except Exception as e_quit_main: + logger.error(f"DEBUG: Exception in simplified quit's main try block: {e_quit_main}", exc_info=True) + if self.child and self.child.isalive(): # Final fallback + try: self.child.close() + except: pass # Ignore errors on this final close + finally: + self.child = None # Mark as no longer having a child process + self.gdb_script_sourced_successfully = False else: - logger.info("GDB session quit called, but no active child process.") - logger.info("GDB session resources released.") + logger.info("DEBUG: GDB session quit called, but no active child process or child already None.") + logger.info("DEBUG: GDB session resources (controller-side) released.") + def is_alive(self) -> bool: - """Checks if the GDB child process is alive.""" return self.child is not None and self.child.isalive() \ No newline at end of file diff --git a/cpp_python_debug/core/gdb_dumper.py b/cpp_python_debug/core/gdb_dumper.py index fe09411..8aedf60 100644 --- a/cpp_python_debug/core/gdb_dumper.py +++ b/cpp_python_debug/core/gdb_dumper.py @@ -1,236 +1,414 @@ # File: cpp_python_debug/core/gdb_dumper.py -# This script is intended to be sourced by GDB. -# It provides a command to dump GDB values as JSON. - import gdb import json -import traceback # For detailed error logging within the script +import traceback +import os # Per il percorso del file di log -# --- Configuration --- -# Maximum number of elements to serialize for an array/list-like structure -# to prevent excessively large JSON outputs or infinite loops with cyclic structures. +# --- Configurazione del Logging su File per gdb_dumper.py --- +# Determina un percorso per il file di log. +# Potrebbe essere nella directory temporanea dell'utente o vicino allo script. +# Per semplicità, usiamo una directory temporanea. Sii consapevole dei permessi. +try: + # Prova a usare la directory dello script GDB stesso (se ha permessi di scrittura) + # o una directory temporanea standard. + # __file__ potrebbe non essere definito se lo script è "incollato" in GDB in modi strani, + # ma per il sourcing normale dovrebbe funzionare. + if '__file__' in globals(): + GDB_DUMPER_LOG_DIR = os.path.dirname(os.path.abspath(__file__)) + else: # Fallback a una directory nota se __file__ non è definito + GDB_DUMPER_LOG_DIR = os.path.expanduser("~") # Home directory dell'utente + + GDB_DUMPER_LOG_PATH = os.path.join(GDB_DUMPER_LOG_DIR, "gdb_dumper_debug.log") + + # Funzione per scrivere nel log dedicato + def _dumper_log_write(message): + try: + # Usiamo 'a' per appendere, ma il file verrà cancellato all'avvio dell'app principale + with open(GDB_DUMPER_LOG_PATH, "a", encoding='utf-8') as f_log: + f_log.write(str(message) + "\n") + except Exception: + pass # Non far fallire lo script principale se il logging fallisce + + # Messaggio iniziale nel log quando lo script viene caricato/sorgente + _dumper_log_write("--- GDB Dumper Script Initialized/Sourced ---") + +except Exception as e_log_setup: + # Se la configurazione del log fallisce, il logging su file non funzionerà, + # ma lo script principale dovrebbe comunque provare a funzionare. + # Possiamo provare a stampare un errore sulla console GDB una volta, + # sperando che non interferisca troppo se è solo un messaggio. + gdb.write("ERROR: gdb_dumper.py failed to setup file logging: {}\n".format(e_log_setup)) + def _dumper_log_write(message): # Funzione NOP se il setup fallisce + pass +# --- Fine Configurazione Logging --- + + +# --- Configuration (MAX_*, etc. come prima) --- MAX_ARRAY_ELEMENTS = 100 -# Maximum depth for recursive serialization (e.g., nested structs) MAX_RECURSION_DEPTH = 10 -# Maximum length for strings read from memory MAX_STRING_LENGTH = 2048 - class EnhancedJsonEncoder(json.JSONEncoder): - """ - A custom JSON encoder to handle gdb.Value objects and other GDB-specific types. - It tries to serialize GDB values into corresponding Python types that can be - represented in JSON. - """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.current_depth = 0 # For tracking recursion depth + self.current_depth = 0 + self.visited_values = {} + + def _get_value_unique_key(self, gdb_val): + # ... (come prima) + if not gdb_val: return None + try: + address_str = "no_address" + if hasattr(gdb_val, 'address') and gdb_val.address is not None: + address_str = str(gdb_val.address) + original_type_str = str(gdb_val.type) + return f"{address_str}_{original_type_str}" + except gdb.error: return f"error_getting_key_for_{str(gdb_val)[:50]}" + + def _is_visitable_value(self, gdb_val): + # ... (come prima) + if not gdb_val: return False + try: + type_code = gdb_val.type.strip_typedefs().code + return type_code in [gdb.TYPE_CODE_PTR, gdb.TYPE_CODE_ARRAY, + gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION] + except gdb.error: return False + + # All'interno della classe EnhancedJsonEncoder: + + # All'interno della classe EnhancedJsonEncoder: def _serialize_value(self, gdb_val): """ - Recursive helper to serialize a gdb.Value, tracking depth. + Recursively serializes a gdb.Value, tracking depth and visited objects. """ - if self.current_depth > MAX_RECURSION_DEPTH: - return f"" + # _dumper_log_write(f"TRACE: _serialize_value - Depth: {self.current_depth}, Type: {gdb_val.type}") + if self.current_depth > MAX_RECURSION_DEPTH: + return "".format(MAX_RECURSION_DEPTH) + + unique_key = None + is_visitable = self._is_visitable_value(gdb_val) + if is_visitable: + unique_key = self._get_value_unique_key(gdb_val) + if unique_key and unique_key in self.visited_values: + if self.current_depth >= self.visited_values[unique_key]: + type_in_key = unique_key.split('_', 1)[1] if '_' in unique_key else "unknown_type" + addr_in_key = unique_key.split('_', 1)[0] + return "".format(type_in_key, addr_in_key) + self.current_depth += 1 + if is_visitable and unique_key: self.visited_values[unique_key] = self.current_depth + + serialized_val = None try: - # Handle cases where the value might have been optimized out by the compiler if gdb_val.is_optimized_out: serialized_val = "" else: - val_type = gdb_val.type.strip_typedefs() # Get the underlying type + val_type = gdb_val.type.strip_typedefs() type_code = val_type.code + original_type_str = str(gdb_val.type) + type_name_str = str(val_type.name) if val_type.name else "" - if type_code == gdb.TYPE_CODE_PTR: - serialized_val = self._handle_pointer(gdb_val, val_type) - elif type_code == gdb.TYPE_CODE_ARRAY: - serialized_val = self._handle_array(gdb_val, val_type) - elif type_code == gdb.TYPE_CODE_STRUCT or \ - type_code == gdb.TYPE_CODE_UNION or \ - (hasattr(val_type, 'fields') and callable(val_type.fields) and len(val_type.fields()) > 0): # Includes classes - serialized_val = self._handle_struct_or_class(gdb_val, val_type) - elif type_code == gdb.TYPE_CODE_ENUM: - serialized_val = str(gdb_val) # GDB usually gives the enum member name - elif type_code == gdb.TYPE_CODE_INT: - serialized_val = int(gdb_val) - elif type_code == gdb.TYPE_CODE_FLT: - serialized_val = float(gdb_val) - elif type_code == gdb.TYPE_CODE_BOOL: - serialized_val = bool(gdb_val) - elif str(val_type) == "void": # Special case for void, gdb_val might be an address - serialized_val = str(gdb_val) - else: - # Fallback for other types (e.g., function pointers, internal types) - # Try to convert to string; might be an address or symbolic representation - serialized_val = str(gdb_val) + is_potentially_string = "string" in original_type_str.lower() or \ + ("basic_string" in original_type_str.lower() and "<" in original_type_str) or \ + "string" in type_name_str.lower() or \ + ("basic_string" in type_name_str.lower() and "<" in type_name_str) + + if is_potentially_string: + _dumper_log_write(f"DEBUG_STRING_TRACE: _serialize_value processing possible string. Original Type: '{original_type_str}', Stripped Type Name: '{type_name_str}', Type Code: {type_code}") + + iterator_children_result = self._get_iterator_children(gdb_val) + + if iterator_children_result is not None: + serialized_val = iterator_children_result + else: + condition_is_std_string = False + if "::basic_string<" in type_name_str: + condition_is_std_string = True + elif type_name_str == "std::string" or type_name_str == "std::__cxx11::string": + condition_is_std_string = True + elif not condition_is_std_string and type_name_str.endswith("::string"): + condition_is_std_string = True + + if is_potentially_string: + _dumper_log_write(f"DEBUG_STRING_CONDITION_REVISED: For type_name_str: '{type_name_str}', condition_is_std_string result: {condition_is_std_string}") + + if condition_is_std_string: + _dumper_log_write(f"DEBUG_STRING_HANDLER: Entered 'std::string' block for: '{type_name_str}' (Attempting internal pointer access)") + _M_p_val = None + try: + # Tentativo di accedere a _M_dataplus['_M_p'] + if '_M_dataplus' in (f.name for f in gdb_val.type.fields()): + _M_dataplus_val = gdb_val['_M_dataplus'] + if '_M_p' in (f.name for f in _M_dataplus_val.type.fields()): + _M_p_val = _M_dataplus_val['_M_p'] + else: + _dumper_log_write(f"DEBUG_STRING_HANDLER: Field '_M_p' not found in _M_dataplus for '{type_name_str}'.") + # Tentativo di accedere a _M_p direttamente (per alcune implementazioni/SSO) + elif '_M_p' in (f.name for f in gdb_val.type.fields()): + _M_p_val = gdb_val['_M_p'] + else: + _dumper_log_write(f"DEBUG_STRING_HANDLER: Internal string pointer fields ('_M_dataplus' or direct '_M_p') not found for '{type_name_str}'.") + + if _M_p_val is not None: + _dumper_log_write(f"DEBUG_STRING_HANDLER: Found _M_p for '{type_name_str}'. Type of _M_p: {str(_M_p_val.type)}") + _M_p_type_stripped = _M_p_val.type.strip_typedefs() + if _M_p_type_stripped.code == gdb.TYPE_CODE_PTR: + _M_p_target_type = _M_p_type_stripped.target().strip_typedefs() + if _M_p_target_type.code == gdb.TYPE_CODE_INT and \ + ("char" in str(_M_p_target_type.name) if _M_p_target_type.name else False): + serialized_val = _M_p_val.string(encoding='utf-8', errors='replace') #, length=MAX_STRING_LENGTH) + _dumper_log_write(f"DEBUG_STRING_HANDLER: _M_p.string() successful, value (first 100 chars): {str(serialized_val)[:100]}") + else: + _dumper_log_write(f"DEBUG_STRING_HANDLER: _M_p for '{type_name_str}' is not a char pointer as expected. Target Type: {str(_M_p_target_type)}. Falling back to str(gdb_val).") + serialized_val = str(gdb_val) + else: + _dumper_log_write(f"DEBUG_STRING_HANDLER: _M_p for '{type_name_str}' is not a pointer. Type: {str(_M_p_val.type)}. Falling back to str(gdb_val).") + serialized_val = str(gdb_val) + else: # _M_p_val is None (non trovato) + _dumper_log_write(f"DEBUG_STRING_HANDLER: Could not access _M_p for '{type_name_str}'. This might be due to Small String Optimization (SSO). Falling back to str(gdb_val).") + serialized_val = str(gdb_val) # Fallback per SSO o se _M_p non è accessibile + + except Exception as e_str_internal: + _dumper_log_write(f"DEBUG_STRING_HANDLER: Error accessing internal string pointer for '{type_name_str}': {e_str_internal}\n{traceback.format_exc()}") + try: # Ultimo fallback + serialized_val = str(gdb_val) + _dumper_log_write(f"DEBUG_STRING_HANDLER: Fallback to str(gdb_val) after internal pointer error, value: {str(serialized_val)[:100]}") + except Exception as e_fallback_str: + _dumper_log_write(f"DEBUG_STRING_HANDLER: Fallback str(gdb_val) also failed: {e_fallback_str}") + serialized_val = "".format(str(e_str_internal)) + + elif type_code == gdb.TYPE_CODE_PTR: + serialized_val = self._handle_pointer(gdb_val, val_type) + elif type_code == gdb.TYPE_CODE_ARRAY: + serialized_val = self._handle_c_array(gdb_val, val_type) + elif type_code == gdb.TYPE_CODE_STRUCT or \ + type_code == gdb.TYPE_CODE_UNION: + if is_potentially_string and not condition_is_std_string: + _dumper_log_write(f"DEBUG_STRING_ERROR_REVISED_CHECK: String type ('{original_type_str}') still reached STRUCT/UNION! type_name_str: '{type_name_str}', condition_was: {condition_is_std_string}") + serialized_val = self._handle_struct_or_class(gdb_val, val_type, original_type_str) + elif type_code == gdb.TYPE_CODE_ENUM: + serialized_val = str(gdb_val) + elif type_code == gdb.TYPE_CODE_INT: + serialized_val = int(gdb_val) + elif type_code == gdb.TYPE_CODE_FLT: + serialized_val = float(gdb_val) + elif type_code == gdb.TYPE_CODE_BOOL: + serialized_val = bool(gdb_val) + elif str(val_type) == "void": + serialized_val = str(gdb_val) + else: + try: + serialized_val = str(gdb_val) + except Exception: + serialized_val = "".format(str(val_type)) except gdb.error as e_gdb: - # Error accessing GDB value (e.g., invalid memory) - serialized_val = f"" + _dumper_log_write(f"ERROR: GDB error in _serialize_value for type {gdb_val.type}: {e_gdb}") + serialized_val = "".format(str(e_gdb), str(gdb_val.type)) except Exception as e_py: - # Python error within this serialization logic - serialized_val = f"" + _dumper_log_write(f"ERROR: Python Traceback in _serialize_value for type {gdb_val.type}:\n{traceback.format_exc()}") + serialized_val = "".format(str(e_py), str(gdb_val.type)) self.current_depth -= 1 + if is_visitable and unique_key and unique_key in self.visited_values: + if self.visited_values[unique_key] == self.current_depth + 1: + del self.visited_values[unique_key] return serialized_val def default(self, o): if isinstance(o, gdb.Value): + if self.current_depth == 0: self.visited_values.clear() return self._serialize_value(o) - # Let the base class default method raise the TypeError for unsupported types return json.JSONEncoder.default(self, o) - def _handle_pointer(self, gdb_val, val_type): - """Handles gdb.TYPE_CODE_PTR.""" - if not gdb_val: # Null pointer + def _get_iterator_children(self, gdb_val_original): + type_name = "UnknownType" + try: + gdb_val_type_stripped = gdb_val_original.type.strip_typedefs() + type_name = str(gdb_val_type_stripped.name) if gdb_val_type_stripped.name else "" + + if "std::basic_string" in type_name: return None + + is_known_stl_vector = "std::vector" in type_name + is_known_stl_map = "std::map" in type_name or "std::unordered_map" in type_name + is_any_known_stl_container = is_known_stl_vector or is_known_stl_map or \ + any(container_name == type_name or type_name.startswith(container_name + "<") for container_name in [ + "std::list", "std::set", "std::deque", "std::forward_list", + "std::multimap", "std::multiset" + ]) + + has_children_attr = hasattr(gdb_val_original, 'children') + is_children_callable = False + if has_children_attr: + try: + children_method_obj = gdb_val_original.children + is_children_callable = callable(children_method_obj) + except Exception: pass + has_children_method = has_children_attr and is_children_callable + + elements = [] + children_processed_successfully = False + + if has_children_method: + try: + children_iter = gdb_val_original.children() + temp_children_list = [item for item in children_iter] + count = 0 + for child_tuple_or_val in temp_children_list: + child_val_to_serialize = None; key_for_map_entry = None + if isinstance(child_tuple_or_val, tuple) and len(child_tuple_or_val) == 2: + key_obj, val_obj = child_tuple_or_val + if is_known_stl_map: + key_for_map_entry = self._serialize_value(key_obj) if isinstance(key_obj, gdb.Value) else str(key_obj) + child_val_to_serialize = val_obj + else: child_val_to_serialize = child_tuple_or_val + + if count < MAX_ARRAY_ELEMENTS: + serialized_element = None + if isinstance(child_val_to_serialize, gdb.Value): serialized_element = self._serialize_value(child_val_to_serialize) + elif child_val_to_serialize is None or isinstance(child_val_to_serialize, (int, float, str, bool, list, dict)): serialized_element = child_val_to_serialize + else: serialized_element = "".format(type(child_val_to_serialize)) + + if key_for_map_entry is not None: elements.append({"key": key_for_map_entry, "value": serialized_element}) + else: elements.append(serialized_element) + else: + elements.append("".format(MAX_ARRAY_ELEMENTS)); break + count += 1 + if elements or (is_any_known_stl_container and not temp_children_list): children_processed_successfully = True + except Exception: pass + + if not children_processed_successfully and is_known_stl_vector: + try: + m_impl = gdb_val_original['_M_impl'] + m_start_val = m_impl['_M_start']; m_finish_val = m_impl['_M_finish'] + m_start_type_stripped = m_start_val.type.strip_typedefs() + m_finish_type_stripped = m_finish_val.type.strip_typedefs() + + if m_start_type_stripped.code == gdb.TYPE_CODE_PTR and m_finish_type_stripped.code == gdb.TYPE_CODE_PTR: + element_type = m_start_type_stripped.target().strip_typedefs() + if element_type.sizeof == 0: elements = []; children_processed_successfully = True + else: + current_ptr_val = m_start_val; num_elements_manually = 0; manual_elements = [] + max_manual_elements_to_check = MAX_ARRAY_ELEMENTS + 5 + while current_ptr_val != m_finish_val and num_elements_manually < max_manual_elements_to_check : + if num_elements_manually < MAX_ARRAY_ELEMENTS: + try: manual_elements.append(self._serialize_value(current_ptr_val.dereference())) + except gdb.error: manual_elements.append(""); break + num_elements_manually += 1 + try: current_ptr_val = current_ptr_val + 1 + except gdb.error: break + if num_elements_manually >= MAX_ARRAY_ELEMENTS and current_ptr_val != m_finish_val : + manual_elements.append("".format(MAX_ARRAY_ELEMENTS)) + elements = manual_elements; children_processed_successfully = True + except Exception: pass + + if children_processed_successfully: return elements + return None + except Exception: + # _dumper_log_write(f"Outer Python error in _get_iterator_children for {type_name}:\n{traceback.format_exc()}") return None + def _handle_pointer(self, gdb_val, val_type): + # ... (implementazione come prima, senza gdb.write) + if not gdb_val: return None target_type = val_type.target().strip_typedefs() + target_type_name_str = str(target_type.name) if target_type.name else "" + if target_type.code == gdb.TYPE_CODE_INT and ("char" in target_type_name_str or "wchar_t" in target_type_name_str): + try: return gdb_val.string(encoding='utf-8', errors='replace', length=MAX_STRING_LENGTH) + except gdb.error: return "".format(str(gdb_val)) + except UnicodeDecodeError: return "".format(str(gdb_val)) + if self.current_depth < MAX_RECURSION_DEPTH: + try: return self._serialize_value(gdb_val.dereference()) + except gdb.error: return str(gdb_val) + else: return "".format(str(gdb_val)) - # Check for C-style strings (char*, const char*, etc.) - if target_type.code == gdb.TYPE_CODE_INT and \ - target_type.name in ["char", "signed char", "unsigned char", "wchar_t"]: # Added wchar_t - try: - # lazy_string can prevent reading excessive amounts of memory - # For wchar_t, GDB might handle encoding, or you might need specific handling - # if GDB's default lazy_string encoding isn't suitable. - # Assuming UTF-8 for char* types is common. - encoding = 'utf-8' - if target_type.name == "wchar_t": - # wchar_t is tricky. GDB's lazy_string might guess, - # or it might use a system-dependent encoding like UTF-32 or UTF-16. - # For simplicity, we'll try 'utf-8', but this might need adjustment - # based on the target system and how wchar_t is used. - # A more robust solution for wchar_t might involve reading memory - # byte by byte and decoding based on wchar_t size. - # GDB Python API doesn't have a direct 'read_wide_string'. - pass # Keep utf-8, or try a common wchar_t encoding like 'utf-32' if issues. - - return gdb_val.lazy_string(encoding=encoding, length=MAX_STRING_LENGTH) - except gdb.error: - return f"" - except UnicodeDecodeError: - return f"" - - - # For other pointer types, return the address as a string - return str(gdb_val) - - def _handle_array(self, gdb_val, val_type): - """Handles gdb.TYPE_CODE_ARRAY.""" + def _handle_c_array(self, gdb_val, val_type): + # ... (implementazione come prima, senza gdb.write) arr_elements = [] try: - # GDB arrays usually have known bounds (for static arrays or VLA if supported) - # val_type.range() gives (lower_bound, upper_bound) - bounds = val_type.range() + bounds = val_type.range() + if bounds[0] > bounds[1]: return [] num_elements_to_fetch = min(bounds[1] - bounds[0] + 1, MAX_ARRAY_ELEMENTS) - for i in range(num_elements_to_fetch): - arr_elements.append(gdb_val[bounds[0] + i]) # Recursively serialize elements - + arr_elements.append(self._serialize_value(gdb_val[bounds[0] + i])) if (bounds[1] - bounds[0] + 1) > MAX_ARRAY_ELEMENTS: - arr_elements.append(f"") - + arr_elements.append("".format(MAX_ARRAY_ELEMENTS)) return arr_elements - except gdb.error as e: - # This can happen if bounds are indeterminable or access fails - return f"" - except Exception as e_py: # Python error during array processing - return f"" + except gdb.error as e: return "".format(str(e), str(val_type)) + except Exception as e_py: return "".format(str(e_py), str(val_type)) - - def _handle_struct_or_class(self, gdb_val, val_type): - """Handles gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION, and classes.""" - obj_dict = {} + def _handle_struct_or_class(self, gdb_val, val_type, original_type_str=""): + # ... (implementazione come prima, senza gdb.write se non specifici per errori stringa) + obj_dict = {"_type": original_type_str if original_type_str else str(val_type)} try: fields = val_type.fields() + if not fields: + str_summary = str(gdb_val) + if str_summary != obj_dict["_type"]: obj_dict["_summary"] = str_summary + return obj_dict for field in fields: field_name = field.name - if field_name is None: # Skip unnamed fields (e.g., anonymous unions/structs padding) - continue - + if field_name is None: continue + if field.artificial and not field.is_base_class: continue if field.is_base_class: - # For base classes, recursively serialize its fields into the current dict. - # Prefix with base class name to avoid collisions and show hierarchy. - base_val = gdb_val.cast(field.type) - base_obj_dict = self._serialize_value(base_val) # Recursive call - - if isinstance(base_obj_dict, dict): - for k, v_base in base_obj_dict.items(): - obj_dict[f"{field.type.name}::{k}"] = v_base - else: # If base class serialization failed or wasn't a dict - obj_dict[f"{field.type.name}"] = base_obj_dict - else: try: - field_value = gdb_val[field_name] - obj_dict[field_name] = field_value # Will be serialized by default() on next pass - except gdb.error as e_field: - obj_dict[field_name] = f"" - except Exception as e_py_field: - obj_dict[field_name] = f"" + base_val = gdb_val.cast(field.type) + base_obj_dict_or_val = self._serialize_value(base_val) + if isinstance(base_obj_dict_or_val, dict): + base_type_name_prefix = (str(field.type.name) + "::") if field.type.name else "base::" + for k_base, v_base in base_obj_dict_or_val.items(): + if k_base == "_type": continue + obj_dict[base_type_name_prefix + k_base] = v_base + else: obj_dict[str(field.type.name) if field.type.name else "base_class_value"] = base_obj_dict_or_val + except gdb.error as e_base_cast: obj_dict[str(field.type.name) if field.type.name else "base_class_error"] = "".format(str(e_base_cast)) + else: + try: + field_value_obj = gdb_val[field_name] + obj_dict[field_name] = self._serialize_value(field_value_obj) + except gdb.error as e_field: obj_dict[field_name] = "".format(str(e_field), field_name) + except Exception as e_py_field: obj_dict[field_name] = "".format(str(e_py_field), field_name) return obj_dict - except gdb.error as e: - return f"" - except Exception as e_py: - return f"" - + except gdb.error as e: return "".format(str(e), str(val_type)) + except Exception as e_py: return "".format(str(e_py), str(val_type)) class GDBDumpJsonCommand(gdb.Command): - """ - A GDB command to dump the value of a C/C++ expression to JSON format. - Usage: dump_json - The command prints the JSON output enclosed in delimiters: - START_JSON_OUTPUT - ...JSON_string... - END_JSON_OUTPUT - """ - def __init__(self): - # COMMAND_DATA: For commands that inspect data. - # COMPLETE_SYMBOL: GDB will attempt symbol completion for the argument. - super(GDBDumpJsonCommand, self).__init__("dump_json", - gdb.COMMAND_DATA, - gdb.COMPLETE_SYMBOL) + super(GDBDumpJsonCommand, self).__init__("dump_json", gdb.COMMAND_DATA, gdb.COMPLETE_SYMBOL) self.output_start_delimiter = "START_JSON_OUTPUT" self.output_end_delimiter = "END_JSON_OUTPUT" def invoke(self, arg_string, from_tty): - # 'arg_string' contains the expression to evaluate (e.g., variable name, expression) - # 'from_tty' is True if the command was typed by a user at the GDB prompt. - - if not arg_string: - gdb.write("Error: No expression provided.\nUsage: dump_json \n") - return - - gdb.write(f"{self.output_start_delimiter}\n") + _dumper_log_write(f"--- dump_json command invoked with arg: '{arg_string}' ---") + gdb.write("{}\n".format(self.output_start_delimiter)) try: - # Evaluate the expression string in the context of the current frame + if not arg_string: + raise ValueError("No expression provided.") gdb_value = gdb.parse_and_eval(arg_string) - - # Serialize the gdb.Value to JSON using the custom encoder - # indent=None and compact separators for minimal output size - json_output = json.dumps(gdb_value, - cls=EnhancedJsonEncoder, - indent=None, - separators=(',', ':'), - ensure_ascii=False) # Allow unicode characters directly - gdb.write(f"{json_output}\n") - - except gdb.error as e_gdb: - # Handle errors from GDB during parse_and_eval or value access - error_json = json.dumps({"gdb_script_error": str(e_gdb), - "expression": arg_string, - "details": "Error evaluating expression or accessing GDB value."}) - gdb.write(f"{error_json}\n") - except Exception as e_py: - # Handle other Python exceptions that might occur within this script's logic - error_json = json.dumps({"gdb_script_error": str(e_py), - "expression": arg_string, - "details": f"Python error in GDB dumper script: {traceback.format_exc()}"}) - gdb.write(f"{error_json}\n") + _dumper_log_write(f"Successfully parsed expression. Type: {gdb_value.type}") + encoder = EnhancedJsonEncoder(indent=None, separators=(',', ':'), ensure_ascii=False) + json_output = encoder.encode(gdb_value) + gdb.write("{}\n".format(json_output)) + _dumper_log_write("Successfully encoded to JSON.") + except gdb.error as e_gdb: # Errore da GDB (es. simbolo non trovato) + _dumper_log_write(f"GDB error during 'dump_json {arg_string}': {e_gdb}\n{traceback.format_exc()}") + error_payload = {"gdb_script_error": str(e_gdb), "expression": arg_string, "details": "GDB error during evaluation or value access. Check gdb_dumper_debug.log for GDB Python script trace."} + gdb.write("{}\n".format(json.dumps(error_payload))) + except ValueError as e_val: # Errore Python, es. ValueError da noi + _dumper_log_write(f"ValueError during 'dump_json {arg_string}': {e_val}\n{traceback.format_exc()}") + error_payload = {"gdb_script_error": str(e_val), "expression": arg_string, "details": "Input error for dump_json command. Check gdb_dumper_debug.log."} + gdb.write("{}\n".format(json.dumps(error_payload))) + except Exception as e_py: # Altri errori Python nello script + _dumper_log_write(f"Unexpected Python error during 'dump_json {arg_string}': {e_py}\n{traceback.format_exc()}") + # Per il client, un messaggio più generico, ma il log ha i dettagli + error_payload = {"gdb_script_error": "Internal Python script error.", + "expression": arg_string, + "details": "Unexpected Python error in GDB dumper script. Check gdb_dumper_debug.log for details."} + gdb.write("{}\n".format(json.dumps(error_payload))) finally: - # Ensure the end delimiter is always printed, even if an error occurred - gdb.write(f"{self.output_end_delimiter}\n") + gdb.write("{}\n".format(self.output_end_delimiter)) + gdb.flush() + _dumper_log_write(f"--- dump_json command finished for arg: '{arg_string}' ---") -# Register the command with GDB when this script is sourced. -GDBDumpJsonCommand() \ No newline at end of file +GDBDumpJsonCommand() +_dumper_log_write("--- GDB Dumper Script Fully Parsed and Command Registered ---") \ No newline at end of file diff --git a/todo.md b/todo.md index 19f7a5a..52d02b9 100644 --- a/todo.md +++ b/todo.md @@ -19,4 +19,34 @@ dump_to_json myVector dump_to_csv myVector -Troverai output.json e output.csv nella root del progetto. \ No newline at end of file +Troverai output.json e output.csv nella root del progetto. + + + + +//////////////////////////// + + +Reading symbols from C:\src\____GitProjects\cpp_python_debug\ws_luna\test_cpp_python\Debug\test_cpp_python.exe... +(gdb) b 25 +Breakpoint 1 at 0x401740: file ../src/test_cpp_python.cpp, line 25. +(gdb) run +Starting program: C:\src\____GitProjects\cpp_python_debug\ws_luna\test_cpp_python\Debug\test_cpp_python.exe +[New Thread 6004.0x2004] +[New Thread 6004.0x2f04] +[New Thread 6004.0x21b4] + +Thread 1 hit Breakpoint 1, main () at ../src/test_cpp_python.cpp:25 +25 std::cout << "Break here" << std::endl; // <-- punto di stop +(gdb) source C:\src\____GitProjects\cpp_python_debug\cpp_python_debug\core\gdb_dumper.py +(gdb) dump_json myInt +START_JSON_OUTPUT +987 +END_JSON_OUTPUT +(gdb) dump_json myDouble +START_JSON_OUTPUT +123.456 +END_JSON_OUTPUT +(gdb) dump_json myStruct +START_JSON_OUTPUT +DEBUG_STRING_TRACE: _serializ \ No newline at end of file diff --git a/ws_luna/test_cpp_python/.gitignore b/ws_luna/test_cpp_python/.gitignore new file mode 100644 index 0000000..3df573f --- /dev/null +++ b/ws_luna/test_cpp_python/.gitignore @@ -0,0 +1 @@ +/Debug/ diff --git a/ws_luna/test_cpp_python/src/test_cpp_python.cpp b/ws_luna/test_cpp_python/src/test_cpp_python.cpp index fdc6c2a..8fccb20 100644 --- a/ws_luna/test_cpp_python/src/test_cpp_python.cpp +++ b/ws_luna/test_cpp_python/src/test_cpp_python.cpp @@ -10,7 +10,15 @@ struct MyData { int main() { std::vector myVector; - for (int i = 0; i < 5; ++i) { + + int myInt = 987; + double myDouble = 123.456; + + MyData myStruct; + myStruct.id = 99; + myStruct.name = "prova testo 1"; + myStruct.value = 98765.4321; + for (int i = 0; i < 10000; ++i) { myVector.push_back({i, "Item" + std::to_string(i), i * 1.5}); }