feat(debug): add GDB-based debug visualizer for linked lists
Replace the custom toDebugGraph() method that generated JSON directly in C++ with a generic GDB Python script (debug_graph.py). The script traverses pointers and structures to build a graph representation for the VS Code Debug Visualizer, enabling external pointer display and more extensible visualization.
Update launch configurations to load the script and to disable array/string truncation (needed for the visualizer JSON). Add a debugAdapterConfigurations entry in settings.json to use the expression template $debug_graph(${expr}) for the cppdbg adapter.
This commit is contained in:
114
scripts/dump_debug_graph.py
Executable file
114
scripts/dump_debug_graph.py
Executable file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Drives both the C++ and Rust debug builds under gdb to their equivalent
|
||||
breakpoint (right after the 30/20/10 list is built), evaluates
|
||||
$debug_graph(list) exactly as VS Code's "Debug Visualizer" extension would
|
||||
(via -data-evaluate-expression), and replays the extension's own
|
||||
parseEvaluationResultFromGenericDebugAdapter algorithm on the raw result --
|
||||
so we can tell, without going through the VS Code UI, whether our JSON will
|
||||
actually be recognized or whether the extension will fall back to its
|
||||
generic byte-walk visualization.
|
||||
|
||||
The parsing port below mirrors:
|
||||
external/vscode-debug-visualizer/extension/src/VisualizationBackend/
|
||||
parseEvaluationResultFromGenericDebugAdapter.ts
|
||||
Keep it in sync if that file changes upstream.
|
||||
|
||||
Writes debug_cpp.json / debug_rust.json into the output directory (default
|
||||
~/lg, matching the style of the extension's own captured dumps): either the
|
||||
resolved data object on success, or a {"PARSE_ERROR": ..., "raw": ...}
|
||||
diagnostic on failure.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def run_gdb(gdb_bin, binary, break_file, break_line):
|
||||
commands = [
|
||||
"-gdb-set print elements 0",
|
||||
"-gdb-set print characters 0",
|
||||
"source %s" % (REPO / "gdb" / "debug_graph.py"),
|
||||
"-break-insert %s:%d" % (break_file, break_line),
|
||||
"-exec-run",
|
||||
'-data-evaluate-expression "$debug_graph(list)"',
|
||||
]
|
||||
proc = subprocess.run(
|
||||
[gdb_bin, "--interpreter=mi", binary],
|
||||
input="\n".join(commands).encode(),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
timeout=15,
|
||||
cwd=str(REPO),
|
||||
)
|
||||
out = proc.stdout.decode(errors="replace")
|
||||
matches = re.findall(r'\^done,value="(.*)"\s*$', out, re.M)
|
||||
errors = re.findall(r'\^error,msg="(.*)"\s*$', out, re.M)
|
||||
if not matches:
|
||||
raise RuntimeError("no ^done,value=... in gdb output; errors=%r\n---\n%s" % (errors, out))
|
||||
return matches[-1]
|
||||
|
||||
|
||||
def is_enclosed_with(s, ch):
|
||||
return s.startswith(ch) and s.endswith(ch) and len(s) >= 2
|
||||
|
||||
|
||||
def parse_like_extension(mi_escaped_value):
|
||||
"""Port of parseEvaluationResultFromGenericDebugAdapter, applied to the
|
||||
raw MI `value="..."` payload (itself one layer of MI's own
|
||||
string-escaping around whatever gdb's evaluate actually reported)."""
|
||||
result_text = json.loads('"' + mi_escaped_value + '"') # undo MI's escaping
|
||||
json_data = result_text.strip()
|
||||
|
||||
try:
|
||||
try:
|
||||
if is_enclosed_with(json_data, '"') or is_enclosed_with(json_data, "'"):
|
||||
json_data2 = json_data[1:-1]
|
||||
else:
|
||||
json_data2 = json_data
|
||||
result_obj = json.loads(json_data2)
|
||||
except Exception:
|
||||
# "in case of C++": the whole thing is itself a JSON string
|
||||
# literal; unwrap it once, then parse *that* as JSON.
|
||||
s = json.loads(json_data)
|
||||
result_obj = json.loads(s)
|
||||
return {"ok": True, "data": result_obj}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e), "raw": result_text}
|
||||
|
||||
|
||||
def dump(label, gdb_bin, binary, break_file, break_line, out_dir):
|
||||
try:
|
||||
raw_value = run_gdb(gdb_bin, binary, break_file, break_line)
|
||||
except Exception as e:
|
||||
result = {"PARSE_ERROR": "gdb invocation failed", "detail": str(e)}
|
||||
else:
|
||||
parsed = parse_like_extension(raw_value)
|
||||
if parsed["ok"]:
|
||||
result = parsed["data"]
|
||||
else:
|
||||
result = {
|
||||
"PARSE_ERROR": parsed["error"],
|
||||
"raw": parsed["raw"],
|
||||
"note": "This is what the Debug Visualizer extension would fail to parse, "
|
||||
"triggering its generic byte-walk fallback (constructGraphFromVariablesReference).",
|
||||
}
|
||||
|
||||
out_path = out_dir / ("debug_%s.json" % label)
|
||||
out_path.write_text(json.dumps(result, indent=4))
|
||||
print("Wrote %s (%s)" % (out_path, "OK" if "PARSE_ERROR" not in result else "PARSE_ERROR"))
|
||||
|
||||
|
||||
def main():
|
||||
out_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.home() / "lg"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
dump("cpp", "gdb", str(REPO / "cpp" / "list_example_cpp"), "cpp/main.cpp", 66, out_dir)
|
||||
dump("rust", "rust-gdb", str(REPO / "target" / "debug" / "list_example"), "src/main.rs", 51, out_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user