Files
metabuilder/gameengine/examples/experiment/gdb_debugger.py
2026-03-09 22:30:41 +00:00

429 lines
13 KiB
Python

#!/usr/bin/env python3
"""
GDB-MI (Machine Interface) Debugger Wrapper
Programmatic control of GDB for debugging C++ code
"""
import subprocess
import json
import sys
import time
import re
from typing import Dict, List, Optional, Any, Tuple
from dataclasses import dataclass, asdict
# ============================================================================
# DATA STRUCTURES
# ============================================================================
@dataclass
class BreakpointInfo:
"""Information about a breakpoint"""
number: int
file: str
line: int
enabled: bool
hit_count: int = 0
@dataclass
class StackFrame:
"""Information about a stack frame"""
level: int
function: str
file: str
line: int
pc: str # Program counter
@dataclass
class Variable:
"""Information about a variable"""
name: str
value: str
type: str
@dataclass
class DebugSession:
"""Current debugging session state"""
program: str
running: bool = False
breakpoints: Dict[int, BreakpointInfo] = None
current_frame: Optional[StackFrame] = None
variables: Dict[str, Variable] = None
def __post_init__(self):
if self.breakpoints is None:
self.breakpoints = {}
if self.variables is None:
self.variables = {}
# ============================================================================
# GDB-MI PARSER
# ============================================================================
class GDBMIParser:
"""Parse GDB Machine Interface output"""
@staticmethod
def parse_output(line: str) -> Tuple[Optional[str], Optional[Dict[str, Any]]]:
"""
Parse a GDB-MI output line
Format: token^ result-class result
Example: 1^done,reason="breakpoint-hit"
"""
if not line.strip():
return None, None
# Extract token
match = re.match(r'^(\d+)\^(\w+)(.*)', line)
if not match:
return None, None
token = match.group(1)
result_class = match.group(2) # done, running, stopped, error
result_text = match.group(3)
# Parse result data
data = GDBMIParser._parse_result(result_text) if result_text else {}
return result_class, data
@staticmethod
def _parse_result(text: str) -> Dict[str, Any]:
"""Parse GDB-MI result data"""
result = {}
# Simple key=value parser (not full GDB-MI, but covers common cases)
# Handle: reason="breakpoint-hit",thread-id="1"
parts = re.findall(r'(\w+)=("(?:[^"\\]|\\.)*"|\[.*?\]|\{.*?\}|[^,]*)', text)
for key, value in parts:
# Clean up value
if value.startswith('"') and value.endswith('"'):
result[key] = value[1:-1] # Remove quotes
elif value.startswith('[') or value.startswith('{'):
try:
result[key] = json.loads(value)
except:
result[key] = value
else:
try:
result[key] = int(value)
except:
result[key] = value
return result
# ============================================================================
# GDB DEBUGGER INTERFACE
# ============================================================================
class GDBDebugger:
"""Interface to GDB via Machine Interface"""
def __init__(self, program: str):
self.program = program
self.process: Optional[subprocess.Popen] = None
self.session = DebugSession(program=program)
self.command_counter = 0
self.output_lines: List[str] = []
def start(self) -> bool:
"""Start GDB with MI interface"""
try:
self.process = subprocess.Popen(
['gdb', '-i=mi', self.program],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1
)
print(f"[GDB] Started debugging: {self.program}")
return True
except FileNotFoundError:
print("[ERROR] GDB not found. Install with: brew install gdb")
return False
def send_command(self, cmd: str) -> str:
"""Send a command to GDB and get response"""
if not self.process:
return ""
self.command_counter += 1
full_cmd = f"{self.command_counter}{cmd}\n"
try:
self.process.stdin.write(full_cmd)
self.process.stdin.flush()
# Read response lines until we get the result
response = ""
while True:
line = self.process.stdout.readline()
if not line:
break
response += line
# MI output ends with "(gdb)" prompt
if line.strip() == "(gdb)":
break
# Stop at result line (^done, ^error, etc.)
if line.startswith(str(self.command_counter)) and '^' in line:
# Read one more line to get the prompt
self.process.stdout.readline()
break
return response
except Exception as e:
print(f"[ERROR] Command failed: {e}")
return ""
def set_breakpoint(self, file: str, line: int) -> Optional[int]:
"""Set a breakpoint at file:line"""
cmd = f"-break-insert {file}:{line}"
response = self.send_command(cmd)
# Parse response for breakpoint number
match = re.search(r'number="(\d+)"', response)
if match:
bp_num = int(match.group(1))
self.session.breakpoints[bp_num] = BreakpointInfo(
number=bp_num,
file=file,
line=line,
enabled=True
)
print(f"[GDB] Breakpoint {bp_num} set at {file}:{line}")
return bp_num
return None
def list_breakpoints(self) -> List[BreakpointInfo]:
"""List all breakpoints"""
response = self.send_command("-break-list")
breakpoints = []
for bp_num, bp_info in self.session.breakpoints.items():
breakpoints.append(bp_info)
return breakpoints
def run(self, args: str = "") -> Dict[str, Any]:
"""Run the program"""
cmd = f"-exec-run{' ' + args if args else ''}"
response = self.send_command(cmd)
self.session.running = True
print(f"[GDB] Program running...")
# Wait for it to stop (breakpoint or end)
return self._parse_stop_response(response)
def continue_execution(self) -> Dict[str, Any]:
"""Continue execution after breakpoint"""
response = self.send_command("-exec-continue")
return self._parse_stop_response(response)
def next(self) -> Dict[str, Any]:
"""Step over next line"""
response = self.send_command("-exec-next")
return self._parse_stop_response(response)
def step(self) -> Dict[str, Any]:
"""Step into next line"""
response = self.send_command("-exec-step")
return self._parse_stop_response(response)
def finish(self) -> Dict[str, Any]:
"""Run until function returns"""
response = self.send_command("-exec-finish")
return self._parse_stop_response(response)
def backtrace(self, depth: int = 10) -> List[StackFrame]:
"""Get stack trace"""
response = self.send_command(f"-stack-list-frames 0 {depth}")
frames = []
# Parse frame information
for match in re.finditer(
r'frame=\{level="(\d+)",addr="(0x[0-9a-f]+)",func="([^"]+)",file="([^"]+)",fullname="([^"]+)",line="(\d+)"',
response
):
level = int(match.group(1))
pc = match.group(2)
function = match.group(3)
file = match.group(4)
line = int(match.group(6))
frames.append(StackFrame(
level=level,
function=function,
file=file,
line=line,
pc=pc
))
return frames
def print_variable(self, var_name: str) -> Optional[str]:
"""Evaluate a variable in current context"""
response = self.send_command(f"-data-evaluate-expression {var_name}")
match = re.search(r'value="([^"]*)"', response)
if match:
value = match.group(1)
self.session.variables[var_name] = Variable(
name=var_name,
value=value,
type="unknown"
)
return value
return None
def list_locals(self) -> List[Variable]:
"""List local variables in current frame"""
response = self.send_command("-stack-list-locals --simple-values")
variables = []
for match in re.finditer(
r'name="([^"]+)",type="([^"]+)",value="([^"]*)"',
response
):
name = match.group(1)
var_type = match.group(2)
value = match.group(3)
var = Variable(name=name, type=var_type, value=value)
variables.append(var)
self.session.variables[name] = var
return variables
def get_info(self) -> Dict[str, Any]:
"""Get debugging info"""
return {
"program": self.session.program,
"running": self.session.running,
"breakpoints": {
bp_num: asdict(bp_info)
for bp_num, bp_info in self.session.breakpoints.items()
},
"variables": {
name: asdict(var)
for name, var in self.session.variables.items()
}
}
def _parse_stop_response(self, response: str) -> Dict[str, Any]:
"""Parse stop/breakpoint response"""
result = {
"stopped": "stopped" in response,
"reason": None,
"thread_id": None,
"breakpoint_num": None
}
if "reason=" in response:
match = re.search(r'reason="([^"]+)"', response)
if match:
result["reason"] = match.group(1)
if "thread-id=" in response:
match = re.search(r'thread-id="(\d+)"', response)
if match:
result["thread_id"] = int(match.group(1))
if "bkptno=" in response:
match = re.search(r'bkptno="(\d+)"', response)
if match:
result["breakpoint_num"] = int(match.group(1))
return result
def quit(self):
"""Exit GDB"""
if self.process:
self.send_command("-gdb-exit")
self.process.terminate()
self.session.running = False
print("[GDB] Debugger closed")
# ============================================================================
# INTERACTIVE DEBUG SESSION
# ============================================================================
def debug_session_demo(program: str):
"""Demo interactive debugging session"""
debugger = GDBDebugger(program)
if not debugger.start():
return
print("\n" + "="*60)
print("GDB-MI INTERACTIVE DEBUG SESSION")
print("="*60 + "\n")
# Set breakpoints
print("[1] Setting breakpoints...")
debugger.set_breakpoint("main.cpp", 200) # graphics.init
debugger.set_breakpoint("main.cpp", 335) # render loop
# Run program
print("\n[2] Running program...")
result = debugger.run()
print(f" Result: {result}\n")
# Show stack trace
print("[3] Stack trace:")
frames = debugger.backtrace(depth=5)
for frame in frames:
print(f" #{frame.level}: {frame.function} at {frame.file}:{frame.line}")
# Inspect locals
print("\n[4] Local variables:")
locals_list = debugger.list_locals()
for var in locals_list[:5]: # Show first 5
print(f" {var.name} ({var.type}) = {var.value}")
# Continue to next breakpoint
print("\n[5] Continuing to next breakpoint...")
result = debugger.continue_execution()
print(f" Result: {result}\n")
# Step through code
print("[6] Stepping through code...")
for i in range(3):
debugger.next()
frames = debugger.backtrace(depth=1)
if frames:
frame = frames[0]
print(f" Step {i+1}: {frame.function} at {frame.file}:{frame.line}")
# Get session info
print("\n[7] Debug session info:")
info = debugger.get_info()
print(json.dumps(info, indent=2))
# Cleanup
print("\n[8] Closing debugger...")
debugger.quit()
print("\n" + "="*60)
print("DEBUG SESSION COMPLETE")
print("="*60 + "\n")
if __name__ == "__main__":
if len(sys.argv) > 1:
program = sys.argv[1]
else:
program = "./build/standalone_workflow_cubes"
debug_session_demo(program)