import contextlib
import typing
import tbot
import tbot_contrib.utils
from tbot.machine import channel, connector, linux, shell
GDB_PROMPT = b"GDB-ORRG65BNM5SGECQ "
[docs]class GDBShell(shell.Shell):
"""
Shell implementation for GDB.
This class implements CLI interaction with GDB. You will most likely not
use it directly but instead use a :py:class:`gdb.GDB() <tbot_contrib.gdb.GDB>`
machine instead.
.. versionadded:: 0.9.3
"""
name = "gdb"
_gdb_at_prompt = False
@contextlib.contextmanager
def _init_shell(self) -> typing.Iterator:
with self.ch.with_prompt(GDB_PROMPT):
streams_orig = self.ch._streams
try:
for i in range(4):
res = self.ch.expect(
[
"(gdb) ",
"Enable debuginfod for this session?",
"--Type <RET> for more, q to quit, c to continue without paging--",
],
timeout=5,
)
if res.i == 0:
# We hit the prompt, nice!
break
elif res.i == 1:
# Say no to debuginfod
self.ch.sendline("n")
elif res.i == 2:
# When pagination comes up during startup, don't paginate
self.ch.sendline("c")
else:
raise Exception("could not reach gdb prompt")
self.ch._streams = []
self.ch.sendline(b"set prompt " + GDB_PROMPT, True)
self.ch.read_until_prompt()
self.ch.sendline("set confirm off")
self.ch.read_until_prompt()
self.ch.sendline("set pagination off")
self.ch.read_until_prompt()
# Disable styling so output is not clobbered with escape sequences
self.ch.sendline("set style enabled off")
self.ch.read_until_prompt()
self._gdb_at_prompt = True
yield None
finally:
self.ch._streams = streams_orig
def escape(self, *args: str) -> str:
"""
Escape a string so it can be used safely on the GDB command-line.
.. todo::
At the moment, this is a noop because GDB escaping is very
different from traditional escaping ... Use with care.
"""
return " ".join(args)
def exec(self, *args: str) -> str:
"""
Run a GDB command (and wait for it to finish).
**Example**:
.. code-block:: python
gdb.exec("break", "main")
# Returns once the program hits a breakpoint
gdb.exec("run")
gdb.exec("backtrace")
"""
cmd = self.escape(*args)
with tbot.log_event.command(self.name, cmd) as ev:
self.ch.sendline(cmd, read_back=True)
with self.ch.with_stream(ev, show_prompt=False):
out = self.ch.read_until_prompt()
# Get rid of bracketed paste and similar escapes
out = tbot_contrib.utils.strip_ansi_escapes(out)
ev.data["stdout"] = out
return out
def interactive(self) -> None:
"""Drop into an interactive GDB session."""
self.ch.sendline("set prompt (gdb) ", True)
self.ch.read_until_prompt("(gdb) ")
self.ch.sendline(" ")
tbot.log.message(
f"Entering interactive GDB shell ({tbot.log.c('CTRL+D to exit').bold}) ..."
)
self.ch.attach_interactive(ctrld_exit=True)
self.ch.sendcontrol("C")
self.ch.read_until_prompt("(gdb) ")
self.ch.sendline(b"set prompt " + GDB_PROMPT, True)
self.ch.read_until_prompt()
H = typing.TypeVar("H", bound=linux.LinuxShell)
[docs]class GDB(connector.Connector, GDBShell):
"""
GDB Machine.
This machine can be used to invoke GDB on a
:py:class:`~tbot.machine.linux.LinuxShell` and interact with it. The
machine will live in a context like any other:
.. code-block:: python
from tbot_contrib import gdb
with tbot.acquire_lab() as lh:
with gdb.GDB(lh, "/usr/bin/echo") as g:
# GDB active here
...
You can then send commands to gdb using
:py:meth:`gdb.exec() <tbot_contrib.gdb.GDB.exec>` or drop the user into an
interactive GDB session with
:py:meth:`gdb.interactive() <tbot_contrib.gdb.GDB.interactive>`:
.. code-block:: python
with gdb.GDB(lh, "/usr/bin/echo", "hello", "world") as g:
# Break on the first getenv
g.exec("break", "getenv")
g.exec("run")
# Not let the user take over and interact with GDB
g.interactive()
.. versionadded:: 0.9.3
"""
def __init__(
self,
host: H,
*args: typing.Union[str, linux.Path[H]],
gdb: typing.Union[str, linux.Path[H], None] = None,
) -> None:
if gdb is None:
gdb = linux.Path(host, "gdb")
elif isinstance(gdb, str):
gdb = linux.Path(host, gdb)
self._gdb = gdb
self.name = f"{host.name} <{gdb.name}>"
self._host = host
self._args = args
@contextlib.contextmanager
def _connect(self) -> typing.Iterator[channel.Channel]:
with self._host.run(self._gdb, "-nh", "-nx", *self._args) as ch:
try:
yield ch
except linux.CommandEndedException:
ch.terminate0()
raise
finally:
if not ch.closed:
if self._gdb_at_prompt:
tbot.log_event.command(self.name, "quit")
ch.sendline("quit")
ch.terminate0()
else:
tbot.log.warning("Trying to abort GDB with ^C + `quit`...")
ch.sendcontrol("C")
try:
ch.read_until_timeout(5)
except linux.CommandEndedException:
ch.terminate()
else:
tbot.log_event.command(self.name, "quit")
ch.sendline("quit")
ch.terminate()
def clone(self) -> typing.NoReturn:
raise NotImplementedError("cannot clone a GDB machine")