598 lines
22 KiB
Python
598 lines
22 KiB
Python
"""This module defines the `GdbController` class
|
|
which runs gdb as a subprocess and can write to it and read from it to get
|
|
structured output.
|
|
"""
|
|
import json
|
|
import logging
|
|
import os
|
|
import select
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
from distutils.spawn import find_executable
|
|
from typing import Any, List, IO, Optional, Union
|
|
|
|
from pygdbmi import gdbmiparser
|
|
|
|
try: # py3
|
|
from shlex import quote
|
|
except ImportError: # py2
|
|
from pipes import quote
|
|
|
|
_FILE = Union[None, int, IO[Any]]
|
|
PYTHON3 = sys.version_info.major == 3
|
|
DEFAULT_GDB_TIMEOUT_SEC = 1
|
|
DEFAULT_TIME_TO_CHECK_FOR_ADDITIONAL_OUTPUT_SEC = 0.2
|
|
USING_WINDOWS = os.name == "nt"
|
|
if USING_WINDOWS:
|
|
import msvcrt
|
|
from ctypes import windll, byref, wintypes, WinError, POINTER # type: ignore
|
|
from ctypes.wintypes import HANDLE, DWORD, BOOL
|
|
else:
|
|
import fcntl
|
|
|
|
SIGNAL_NAME_TO_NUM = {}
|
|
for n in dir(signal):
|
|
if n.startswith("SIG") and "_" not in n:
|
|
SIGNAL_NAME_TO_NUM[n.upper()] = getattr(signal, n)
|
|
|
|
|
|
class NoGdbProcessError(ValueError):
|
|
"""Raise when trying to interact with gdb subprocess, but it does not exist.
|
|
It may have been killed and removed, or failed to initialize for some reason."""
|
|
|
|
pass
|
|
|
|
|
|
class GdbTimeoutError(ValueError):
|
|
"""Raised when no response is recieved from gdb after the timeout has been triggered"""
|
|
|
|
pass
|
|
|
|
|
|
class MyGdbController:
|
|
def __init__(
|
|
self,
|
|
gdb_path: str = "gdb",
|
|
gdb_args: Optional[List] = None,
|
|
time_to_check_for_additional_output_sec=DEFAULT_TIME_TO_CHECK_FOR_ADDITIONAL_OUTPUT_SEC,
|
|
rr: bool = False,
|
|
verbose: bool = False,
|
|
input_file: IO[Any] = ...,
|
|
output_file: IO[Any] = ...
|
|
):
|
|
"""
|
|
Run gdb as a subprocess. Send commands and receive structured output.
|
|
Create new object, along with a gdb subprocess
|
|
|
|
Args:
|
|
gdb_path: Command to run in shell to spawn new gdb subprocess
|
|
gdb_args: Arguments to pass to shell when spawning new gdb subprocess
|
|
time_to_check_for_additional_output_sec: When parsing responses, wait this amout of time before exiting (exits before timeout is reached to save time). If <= 0, full timeout time is used.
|
|
rr:: Use the `rr replay` command instead of `gdb`. See rr-project.org for more info.
|
|
verbose: Print verbose output if True
|
|
Returns:
|
|
New GdbController object
|
|
"""
|
|
|
|
if gdb_args is None:
|
|
default_gdb_args = ["--nx", "--quiet", "--interpreter=mi2"]
|
|
gdb_args = default_gdb_args
|
|
|
|
self.verbose = verbose
|
|
self.abs_gdb_path = None # abs path to gdb executable
|
|
self.cmd = [] # type: List[str]
|
|
self.time_to_check_for_additional_output_sec = (
|
|
time_to_check_for_additional_output_sec
|
|
)
|
|
self.gdb_process = None
|
|
self.input_file = input_file
|
|
self.output_file = output_file
|
|
self._allow_overwrite_timeout_times = (
|
|
self.time_to_check_for_additional_output_sec > 0
|
|
)
|
|
self.first = True
|
|
|
|
if rr:
|
|
self.cmd = ["rr", "replay"] + gdb_args
|
|
|
|
else:
|
|
if not gdb_path:
|
|
raise ValueError("a valid path to gdb must be specified")
|
|
|
|
else:
|
|
abs_gdb_path = find_executable(gdb_path)
|
|
if abs_gdb_path is None:
|
|
raise ValueError(
|
|
'gdb executable could not be resolved from "%s"' % gdb_path
|
|
)
|
|
|
|
else:
|
|
self.abs_gdb_path = abs_gdb_path
|
|
self.cmd = [self.abs_gdb_path] + gdb_args
|
|
|
|
self._attach_logger(verbose)
|
|
self.spawn_new_gdb_subprocess()
|
|
|
|
def _attach_logger(self, verbose: bool):
|
|
handler = logging.StreamHandler()
|
|
handler.setFormatter(logging.Formatter("%(message)s"))
|
|
unique_number = time.time()
|
|
self.logger = logging.getLogger(__name__ + "." + str(unique_number))
|
|
self.logger.propagate = False
|
|
if verbose:
|
|
level = logging.DEBUG
|
|
else:
|
|
level = logging.ERROR
|
|
self.logger.setLevel(level)
|
|
self.logger.addHandler(handler)
|
|
|
|
def get_subprocess_cmd(self):
|
|
"""Returns the shell-escaped string used to invoke the gdb subprocess.
|
|
This is a string that can be executed directly in a shell.
|
|
"""
|
|
return " ".join(quote(c) for c in self.cmd)
|
|
|
|
def spawn_new_gdb_subprocess(self):
|
|
"""Spawn a new gdb subprocess with the arguments supplied to the object
|
|
during initialization. If gdb subprocess already exists, terminate it before
|
|
spanwing a new one.
|
|
Return int: gdb process id
|
|
"""
|
|
if self.gdb_process:
|
|
self.logger.debug(
|
|
"Killing current gdb subprocess (pid %d)" % self.gdb_process.pid
|
|
)
|
|
self.exit()
|
|
|
|
self.logger.debug('Launching gdb: "%s"' % " ".join(self.cmd))
|
|
self.cmd = ["stdbuf", "-oL", "-eL"] + self.cmd
|
|
print(self.cmd)
|
|
|
|
# Use pipes to the standard streams
|
|
self.gdb_process = subprocess.Popen(
|
|
self.cmd,
|
|
shell=False,
|
|
stdout=subprocess.PIPE,
|
|
stdin=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
bufsize=0,
|
|
)
|
|
|
|
# self.gdb_process.stdin = self.input_file
|
|
# self.gdb_process.stdout = self.output_file
|
|
# self.gdb_process.stderr = self.output_file
|
|
# self.gdb_process.wait()
|
|
|
|
_make_non_blocking(self.gdb_process.stdout)
|
|
_make_non_blocking(self.gdb_process.stderr)
|
|
|
|
# save file numbers for use later
|
|
self.stdout_fileno = self.gdb_process.stdout.fileno()
|
|
self.stderr_fileno = self.gdb_process.stderr.fileno()
|
|
self.stdin_fileno = self.gdb_process.stdin.fileno()
|
|
|
|
self.read_list = [self.stdout_fileno, self.stderr_fileno]
|
|
self.write_list = [self.stdin_fileno]
|
|
|
|
# string buffers for unifinished gdb output
|
|
self._incomplete_output = {"stdout": None, "stderr": None}
|
|
|
|
while True:
|
|
events, _, _ = select.select(self.read_list, [], [], 0)
|
|
flag = False
|
|
for fileno in events:
|
|
# new data is ready to read
|
|
if fileno == self.stdout_fileno:
|
|
self.gdb_process.stdout.flush()
|
|
output = self.gdb_process.stdout.read().decode()
|
|
flag = ("(gdb)" in output)
|
|
if flag:
|
|
break
|
|
|
|
return self.gdb_process.pid
|
|
|
|
def verify_valid_gdb_subprocess(self):
|
|
|
|
"""Verify there is a process object, and that it is still running.
|
|
Raise NoGdbProcessError if either of the above are not true."""
|
|
if not self.gdb_process:
|
|
raise NoGdbProcessError("gdb process is not attached")
|
|
|
|
elif self.gdb_process.poll() is not None:
|
|
raise NoGdbProcessError(
|
|
"gdb process has already finished with return code: %s"
|
|
% str(self.gdb_process.poll())
|
|
)
|
|
|
|
def write(
|
|
self,
|
|
mi_cmd_to_write: Union[str, List[str]],
|
|
timeout_sec=DEFAULT_GDB_TIMEOUT_SEC,
|
|
raise_error_on_timeout: bool = True,
|
|
read_response: bool = True,
|
|
):
|
|
"""Write to gdb process. Block while parsing responses from gdb for a maximum of timeout_sec.
|
|
|
|
Args:
|
|
mi_cmd_to_write: String to write to gdb. If list, it is joined by newlines.
|
|
timeout_sec: Maximum number of seconds to wait for response before exiting. Must be >= 0.
|
|
raise_error_on_timeout: If read_response is True, raise error if no response is received
|
|
read_response: Block and read response. If there is a separate thread running,
|
|
this can be false, and the reading thread read the output.
|
|
Returns:
|
|
List of parsed gdb responses if read_response is True, otherwise []
|
|
Raises:
|
|
NoGdbProcessError: if there is no gdb subprocess running
|
|
TypeError: if mi_cmd_to_write is not valid
|
|
"""
|
|
self.verify_valid_gdb_subprocess()
|
|
if timeout_sec < 0:
|
|
self.logger.warning("timeout_sec was negative, replacing with 0")
|
|
timeout_sec = 0
|
|
|
|
# Ensure proper type of the mi command
|
|
if isinstance(mi_cmd_to_write, str):
|
|
mi_cmd_to_write_str = mi_cmd_to_write
|
|
elif isinstance(mi_cmd_to_write, list):
|
|
mi_cmd_to_write_str = "\n".join(mi_cmd_to_write)
|
|
else:
|
|
raise TypeError(
|
|
"The gdb mi command must a be str or list. Got "
|
|
+ str(type(mi_cmd_to_write))
|
|
)
|
|
|
|
self.logger.debug("writing: %s", mi_cmd_to_write)
|
|
|
|
if not mi_cmd_to_write_str.endswith("\n"):
|
|
mi_cmd_to_write_nl = mi_cmd_to_write_str + "\n"
|
|
else:
|
|
mi_cmd_to_write_nl = mi_cmd_to_write_str
|
|
|
|
if USING_WINDOWS:
|
|
# select not implemented in windows for pipes
|
|
# assume it's always ready
|
|
outputready = [self.stdin_fileno]
|
|
else:
|
|
_, outputready, _ = select.select([], self.write_list, [], timeout_sec)
|
|
for fileno in outputready:
|
|
if fileno == self.stdin_fileno:
|
|
print("用户输入-------", mi_cmd_to_write_nl.encode())
|
|
# ready to write
|
|
self.gdb_process.stdin.write( # type: ignore
|
|
mi_cmd_to_write_nl.encode()
|
|
)
|
|
# don't forget to flush for Python3, otherwise gdb won't realize there is data
|
|
# to evaluate, and we won't get a response
|
|
self.gdb_process.stdin.flush() # type: ignore
|
|
else:
|
|
self.logger.error("got unexpected fileno %d" % fileno)
|
|
|
|
if read_response is True:
|
|
return self.get_gdb_response(
|
|
timeout_sec=timeout_sec, raise_error_on_timeout=raise_error_on_timeout
|
|
)
|
|
|
|
else:
|
|
return []
|
|
|
|
def get_gdb_response(
|
|
self, timeout_sec: float = DEFAULT_GDB_TIMEOUT_SEC, raise_error_on_timeout=True
|
|
):
|
|
"""Get response from GDB, and block while doing so. If GDB does not have any response ready to be read
|
|
by timeout_sec, an exception is raised.
|
|
|
|
Args:
|
|
timeout_sec: Maximum time to wait for reponse. Must be >= 0. Will return after
|
|
raise_error_on_timeout: Whether an exception should be raised if no response was found after timeout_sec
|
|
|
|
Returns:
|
|
List of parsed GDB responses, returned from gdbmiparser.parse_response, with the
|
|
additional key 'stream' which is either 'stdout' or 'stderr'
|
|
|
|
Raises:
|
|
GdbTimeoutError: if response is not received within timeout_sec
|
|
ValueError: if select returned unexpected file number
|
|
NoGdbProcessError: if there is no gdb subprocess running
|
|
"""
|
|
|
|
self.verify_valid_gdb_subprocess()
|
|
if timeout_sec < 0:
|
|
self.logger.warning("timeout_sec was negative, replacing with 0")
|
|
timeout_sec = 0
|
|
|
|
if USING_WINDOWS:
|
|
retval = self._get_responses_windows(timeout_sec)
|
|
else:
|
|
retval = self._get_responses_unix(timeout_sec)
|
|
|
|
if not retval and raise_error_on_timeout:
|
|
raise GdbTimeoutError(
|
|
"Did not get response from gdb after %s seconds" % timeout_sec
|
|
)
|
|
|
|
else:
|
|
return retval
|
|
|
|
def _get_responses_windows(self, timeout_sec):
|
|
"""Get responses on windows. Assume no support for select and use a while loop."""
|
|
timeout_time_sec = time.time() + timeout_sec
|
|
responses = []
|
|
while True:
|
|
try:
|
|
self.gdb_process.stdout.flush()
|
|
if PYTHON3:
|
|
raw_output = self.gdb_process.stdout.readline().replace(
|
|
b"\r", b"\n"
|
|
)
|
|
else:
|
|
raw_output = self.gdb_process.stdout.read().replace(b"\r", b"\n")
|
|
responses += self._get_responses_list(raw_output, "stdout")
|
|
except IOError:
|
|
pass
|
|
|
|
try:
|
|
self.gdb_process.stderr.flush()
|
|
if PYTHON3:
|
|
raw_output = self.gdb_process.stderr.readline().replace(
|
|
b"\r", b"\n"
|
|
)
|
|
else:
|
|
raw_output = self.gdb_process.stderr.read().replace(b"\r", b"\n")
|
|
responses += self._get_responses_list(raw_output, "stderr")
|
|
except IOError:
|
|
pass
|
|
|
|
if time.time() > timeout_time_sec:
|
|
break
|
|
|
|
return responses
|
|
|
|
def _get_responses_unix(self, timeout_sec):
|
|
"""Get responses on unix-like system. Use select to wait for output."""
|
|
timeout_time_sec = time.time() + timeout_sec
|
|
responses = []
|
|
while True:
|
|
select_timeout = timeout_time_sec - time.time()
|
|
# I prefer to not pass a negative value to select
|
|
if select_timeout <= 0:
|
|
select_timeout = 0
|
|
events, _, _ = select.select(self.read_list, [], [], select_timeout)
|
|
responses_list = None # to avoid infinite loop if using Python 2
|
|
try:
|
|
for fileno in events:
|
|
# new data is ready to read
|
|
if fileno == self.stdout_fileno:
|
|
self.gdb_process.stdout.flush()
|
|
raw_output = self.gdb_process.stdout.read()
|
|
stream = "stdout"
|
|
|
|
elif fileno == self.stderr_fileno:
|
|
self.gdb_process.stderr.flush()
|
|
continue
|
|
raw_output = self.gdb_process.stderr.read()
|
|
stream = "stderr"
|
|
|
|
else:
|
|
raise ValueError(
|
|
"Developer error. Got unexpected file number %d" % fileno
|
|
)
|
|
|
|
response_list = list(
|
|
filter(lambda x: x, raw_output.decode(errors="replace").split("\n"))
|
|
) # remove blank lines
|
|
|
|
self.write_output(response_list)
|
|
|
|
if _contains_input_fun(response_list):
|
|
self.write_input()
|
|
time.sleep(1)
|
|
return self.get_gdb_response(timeout_sec=0, raise_error_on_timeout=False)
|
|
|
|
# parse each response from gdb into a dict, and store in a list
|
|
last_response = response_list[len(response_list) - 1]
|
|
if "(gdb)" not in last_response:
|
|
self.write_input()
|
|
time.sleep(1)
|
|
return self.get_gdb_response(timeout_sec=0, raise_error_on_timeout=False)
|
|
|
|
responses_list = self._get_responses_list(raw_output, stream)
|
|
responses += responses_list
|
|
|
|
except IOError: # only occurs in python 2.7
|
|
pass
|
|
|
|
if timeout_sec == 0: # just exit immediately
|
|
break
|
|
|
|
elif responses_list and self._allow_overwrite_timeout_times:
|
|
# update timeout time to potentially be closer to now to avoid lengthy wait times when nothing is being output by gdb
|
|
timeout_time_sec = min(
|
|
time.time() + self.time_to_check_for_additional_output_sec,
|
|
timeout_time_sec,
|
|
)
|
|
|
|
elif time.time() > timeout_time_sec:
|
|
break
|
|
|
|
return responses
|
|
|
|
def _get_responses_list(self, raw_output, stream):
|
|
"""Get parsed response list from string output
|
|
Args:
|
|
raw_output (unicode): gdb output to parse
|
|
stream (str): either stdout or stderr
|
|
"""
|
|
responses = []
|
|
|
|
raw_output, self._incomplete_output[stream] = _buffer_incomplete_responses(
|
|
raw_output, self._incomplete_output.get(stream)
|
|
)
|
|
|
|
if not raw_output:
|
|
return responses
|
|
|
|
response_list = list(
|
|
filter(lambda x: x, raw_output.decode(errors="replace").split("\n"))
|
|
) # remove blank lines
|
|
|
|
for response in response_list:
|
|
if gdbmiparser.response_is_finished(response):
|
|
pass
|
|
else:
|
|
parsed_response = gdbmiparser.parse_response(response)
|
|
parsed_response["stream"] = stream
|
|
if parsed_response["type"] != "log":
|
|
responses.append(parsed_response)
|
|
|
|
return responses
|
|
|
|
def send_signal_to_gdb(self, signal_input):
|
|
"""Send signal name (case insensitive) or number to gdb subprocess
|
|
These are all valid ways to call this method:
|
|
```
|
|
gdbmi.send_signal_to_gdb(2)
|
|
gdbmi.send_signal_to_gdb('sigint')
|
|
gdbmi.send_signal_to_gdb('SIGINT')
|
|
```
|
|
|
|
raises:
|
|
ValueError: if signal_input is invalid
|
|
NoGdbProcessError: if there is no gdb process to send a signal to
|
|
"""
|
|
try:
|
|
signal = int(signal_input)
|
|
except Exception:
|
|
signal = SIGNAL_NAME_TO_NUM.get(signal_input.upper())
|
|
|
|
if not signal:
|
|
raise ValueError(
|
|
'Could not find signal corresponding to "%s"' % str(signal)
|
|
)
|
|
|
|
if self.gdb_process:
|
|
os.kill(self.gdb_process.pid, signal)
|
|
else:
|
|
raise NoGdbProcessError(
|
|
"Cannot send signal to gdb process because no process exists."
|
|
)
|
|
|
|
def interrupt_gdb(self):
|
|
"""Send SIGINT (interrupt signal) to the gdb subprocess"""
|
|
self.send_signal_to_gdb("SIGINT")
|
|
|
|
def exit(self) -> None:
|
|
"""Terminate gdb process"""
|
|
if self.gdb_process:
|
|
self.gdb_process.terminate()
|
|
self.gdb_process.communicate()
|
|
self.gdb_process = None
|
|
return None
|
|
|
|
def write_input(self):
|
|
raw_input = self.input_file.readline()
|
|
print("自动读入的数据为:", raw_input)
|
|
self.gdb_process.stdin.write(raw_input.encode())
|
|
self.gdb_process.stdin.flush()
|
|
|
|
def write_output(self, response_list):
|
|
for response in response_list:
|
|
if gdbmiparser.response_is_finished(response):
|
|
pass
|
|
else:
|
|
parsed_response = gdbmiparser.parse_response(response)
|
|
|
|
if parsed_response["type"] == "output":
|
|
output = parsed_response["payload"]
|
|
self.output_file.writelines(output + "\n")
|
|
self.output_file.flush()
|
|
|
|
|
|
def _buffer_incomplete_responses(raw_output, buf):
|
|
"""It is possible for some of gdb's output to be read before it completely finished its response.
|
|
In that case, a partial mi response was read, which cannot be parsed into structured data.
|
|
We want to ALWAYS parse complete mi records. To do this, we store a buffer of gdb's
|
|
output if the output did not end in a newline.
|
|
|
|
Args:
|
|
raw_output: Contents of the gdb mi output
|
|
buf (str): Buffered gdb response from the past. This is incomplete and needs to be prepended to
|
|
gdb's next output.
|
|
|
|
Returns:
|
|
(raw_output, buf)
|
|
"""
|
|
|
|
if raw_output:
|
|
if buf:
|
|
# concatenate buffer and new output
|
|
raw_output = b"".join([buf, raw_output])
|
|
buf = None
|
|
|
|
if b"\n" not in raw_output:
|
|
# newline was not found, so assume output is incomplete and store in buffer
|
|
buf = raw_output
|
|
raw_output = None
|
|
|
|
elif not raw_output.endswith(b"\n"):
|
|
# raw output doesn't end in a newline, so store everything after the last newline (if anything)
|
|
# in the buffer, and parse everything before it
|
|
remainder_offset = raw_output.rindex(b"\n") + 1
|
|
buf = raw_output[remainder_offset:]
|
|
raw_output = raw_output[:remainder_offset]
|
|
|
|
return (raw_output, buf)
|
|
|
|
|
|
def _make_non_blocking(file_obj):
|
|
"""make file object non-blocking
|
|
Windows doesn't have the fcntl module, but someone on
|
|
stack overflow supplied this code as an answer, and it works
|
|
http://stackoverflow.com/a/34504971/2893090"""
|
|
|
|
if USING_WINDOWS:
|
|
LPDWORD = POINTER(DWORD)
|
|
PIPE_NOWAIT = wintypes.DWORD(0x00000001)
|
|
|
|
SetNamedPipeHandleState = windll.kernel32.SetNamedPipeHandleState
|
|
SetNamedPipeHandleState.argtypes = [HANDLE, LPDWORD, LPDWORD, LPDWORD]
|
|
SetNamedPipeHandleState.restype = BOOL
|
|
|
|
h = msvcrt.get_osfhandle(file_obj.fileno())
|
|
|
|
res = windll.kernel32.SetNamedPipeHandleState(h, byref(PIPE_NOWAIT), None, None)
|
|
if res == 0:
|
|
raise ValueError(WinError())
|
|
|
|
else:
|
|
# Set the file status flag (F_SETFL) on the pipes to be non-blocking
|
|
# so we can attempt to read from a pipe with no new data without locking
|
|
# the program up
|
|
fcntl.fcntl(file_obj.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
|
|
|
|
|
|
'''
|
|
判断是否包含输入函数
|
|
'''
|
|
|
|
|
|
def _contains_input_fun(response_list: list = []):
|
|
running = False
|
|
stop = True
|
|
|
|
for response in response_list:
|
|
if gdbmiparser.response_is_finished(response):
|
|
pass
|
|
else:
|
|
parsed_response = gdbmiparser.parse_response(response)
|
|
|
|
if parsed_response["message"] == "running" and parsed_response["type"] == "notify":
|
|
if parsed_response["payload"]["thread-id"] is not None:
|
|
running = True
|
|
if parsed_response["message"] == "stopped":
|
|
stop = False
|
|
|
|
return running and stop
|