mirror of
https://github.com/AdaCore/langkit.git
synced 2026-02-12 12:28:12 -08:00
191 lines
6.1 KiB
Python
191 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
import atexit
|
|
import inspect
|
|
import os
|
|
import re
|
|
import traceback
|
|
|
|
from expect import EXPECT_DIED, EXPECT_TIMEOUT, ExpectProcess
|
|
from quotemeta import convert_expression
|
|
from utils import indent
|
|
|
|
|
|
class GDBSession:
|
|
"""
|
|
Handle for a GDB session, to run GDB commands and inspect their output.
|
|
"""
|
|
|
|
PROMPT_RE = r"\(gdb\) "
|
|
TIMEOUT = 30 # In seconds
|
|
|
|
def __init__(
|
|
self, program: str | None = None, log_file: str | None = None
|
|
):
|
|
|
|
# Make sure that the GDB subprogram is terminated with its logs written
|
|
# somewhere before the end of the script.
|
|
atexit.register(self.stop)
|
|
|
|
self.log_file = log_file or "gdb.log"
|
|
|
|
# Disable the load of .gdbinit to avoid user configuration
|
|
# interference.
|
|
argv = ["gdb", "--nh"]
|
|
|
|
os.environ["TERM"] = "dumb"
|
|
|
|
self.proc = ExpectProcess(argv, save_input=True, save_output=True)
|
|
self.alive = True
|
|
_ = self._read_to_next_prompt()
|
|
|
|
# Enable Python backtraces to ease investigation
|
|
self.execute("set python print-stack full")
|
|
|
|
# Disable interactive mode, which is bound to create trouble in a
|
|
# testsuite.
|
|
self.execute("set interactive-mode off")
|
|
|
|
# Make the output deterministic, independent of the actual terminal
|
|
# size.
|
|
self.execute("set height 0")
|
|
self.execute("set width 80")
|
|
|
|
if program:
|
|
# Only then, load the inferior. Loading gnatdbg before checks that
|
|
# importing it does not rely on the presence of debug information.
|
|
self.test(
|
|
"file {}".format(program),
|
|
r"Reading symbols from {}...@...@/done|/".format(program),
|
|
)
|
|
|
|
def _read_to_next_prompt(self) -> str:
|
|
"""
|
|
Read GDB's output until we reach the next prompt.
|
|
|
|
Return the output in between. Raise a RuntimeError if GDB dies or if
|
|
timeout is reached.
|
|
"""
|
|
assert self.alive
|
|
status = self.proc.expect([self.PROMPT_RE], self.TIMEOUT)
|
|
if status is EXPECT_DIED:
|
|
raise RuntimeError("GDB died")
|
|
elif status is EXPECT_TIMEOUT:
|
|
raise RuntimeError("Timeout reached while waiting for GDB")
|
|
|
|
assert status == 0
|
|
out, prompt = self.proc.out()
|
|
return out
|
|
|
|
def execute(self, command: str) -> None:
|
|
"""
|
|
Shortcut for `test` without an expected output.
|
|
"""
|
|
return self.test(command, None)
|
|
|
|
def test(
|
|
self, command: str, expected_output: str | None, quotemeta: bool = True
|
|
) -> None:
|
|
"""
|
|
Send the given command to GDB and check its output.
|
|
|
|
:param command: GDB command to send.
|
|
:param expected_output: If None, don't check the command output.
|
|
Otherwise, it must be a quotemeta expression that must match the
|
|
output.
|
|
:param quotemeta: Whether to interpret ``expected_output`` as a
|
|
quotemeta expression. If False, expect exactly the given output.
|
|
"""
|
|
assert self.alive
|
|
assert self.proc.send(command)
|
|
output = self._read_to_next_prompt().strip().replace("\r", "")
|
|
|
|
if expected_output is None:
|
|
matcher = ""
|
|
elif quotemeta:
|
|
matcher = convert_expression(expected_output)
|
|
else:
|
|
matcher = re.escape(expected_output)
|
|
|
|
if expected_output is not None and not re.match(matcher, output):
|
|
print("")
|
|
print("FAIL: {}".format(command))
|
|
print("From:")
|
|
# Print the current call stack but omit the current frame
|
|
print(
|
|
"".join(
|
|
traceback.format_stack(inspect.currentframe().f_back)
|
|
).rstrip()
|
|
)
|
|
print("Output:")
|
|
print(indent(output))
|
|
print("Does not match the expected:")
|
|
print(indent(expected_output))
|
|
|
|
def print_expr(self, expr: str, expected_output: str) -> None:
|
|
"""
|
|
Execute the "print" GDB command and check its output.
|
|
|
|
:param expr: Expression to print.
|
|
:param expected_output: Regular expression that the output must
|
|
match. Note that it must not include the '$NUMBER = ' prefix.
|
|
"""
|
|
self.test("print {}".format(expr), "$@NUMBER = " + expected_output)
|
|
|
|
@staticmethod
|
|
def find_loc(filename: str, slug: str) -> str:
|
|
"""
|
|
Look for a source file location.
|
|
|
|
If the location is found, return a string location suitable for GDB's
|
|
break command. Raise a RuntimeError otherwise.
|
|
|
|
:param filename: Target source file name.
|
|
:param slug: Source file excerpt for the location. For instance, a
|
|
specific string that is in a comment.
|
|
:return: String location suitable for GDB's break command.
|
|
"""
|
|
with open(filename, "r") as f:
|
|
for i, line in enumerate(f, 1):
|
|
if slug in line:
|
|
return "{}:{}".format(filename, i)
|
|
raise RuntimeError(
|
|
"Could not find location in {} for {}".format(filename, slug)
|
|
)
|
|
|
|
def run_to(self, location: str) -> None:
|
|
"""
|
|
Start inferior execution until it reaches the given location.
|
|
|
|
:param location: String location suitable for GDB's break command.
|
|
"""
|
|
self.execute("tbreak {}".format(location))
|
|
self.execute("run")
|
|
|
|
def kill(self) -> None:
|
|
"""
|
|
Kill the inferior process currently running.
|
|
"""
|
|
self.test(
|
|
"kill",
|
|
"Kill the program being debugged? (y or n)"
|
|
" [answered Y; input not from terminal]\n"
|
|
"[Inferior 1 (process @/\\d+/) killed]",
|
|
)
|
|
|
|
def stop(self) -> None:
|
|
"""
|
|
Stop GDB.
|
|
|
|
This writes session logs to make post-mortem debugging.
|
|
"""
|
|
if not self.alive:
|
|
return
|
|
|
|
self.alive = False
|
|
|
|
# No matter what, write the session logs to make post-mortem debugging
|
|
# possible.
|
|
with open(self.log_file, "w") as f:
|
|
f.write(self.proc.get_session_logs())
|