81d9f11ab8
* refactor RPCSessionContext utils * Make TVMLogf platform-independent. * Some platforms need to use an alternate printf() to support basic things like %zu. Since %zu is platform-specific, we prefer to use a printf() that supports it or allow the platform to fix it up as needed.
388 lines
13 KiB
Python
388 lines
13 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
"""Defines functions for controlling debuggers for micro TVM binaries."""
|
|
|
|
import atexit
|
|
import abc
|
|
import errno
|
|
import logging
|
|
import os
|
|
import shlex
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import termios
|
|
import threading
|
|
import time
|
|
|
|
import psutil
|
|
|
|
from .._ffi import register_func
|
|
from . import class_factory
|
|
from . import transport
|
|
from .transport.file_descriptor import FdTransport
|
|
|
|
|
|
_LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class Debugger(metaclass=abc.ABCMeta):
|
|
"""An interface for controlling micro TVM debuggers."""
|
|
|
|
@abc.abstractmethod
|
|
def start(self):
|
|
"""Start the debugger, but do not block on it.
|
|
|
|
The runtime will continue to be driven in the background.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@abc.abstractmethod
|
|
def stop(self):
|
|
"""Terminate the debugger."""
|
|
raise NotImplementedError()
|
|
|
|
|
|
class GdbDebugger(Debugger):
|
|
"""Handles launching, suspending signals, and potentially dealing with terminal issues."""
|
|
|
|
# Number of seconds to wait in stop() for a graceful shutdown. After this time has elapsed,
|
|
# the debugger is kill()'d.
|
|
_GRACEFUL_SHUTDOWN_TIMEOUT_SEC = 5.0
|
|
|
|
# The instance of GdbDebugger that's currently started.
|
|
_STARTED_INSTANCE = None
|
|
|
|
@classmethod
|
|
def _stop_all(cls):
|
|
if cls._STARTED_INSTANCE:
|
|
cls._STARTED_INSTANCE.stop()
|
|
|
|
def __init__(self):
|
|
super(GdbDebugger, self).__init__()
|
|
self._is_running = False
|
|
self._is_running_lock = threading.RLock()
|
|
self._child_exited_event = threading.Event()
|
|
self._signals_reset_event = threading.Event()
|
|
|
|
@abc.abstractmethod
|
|
def popen_kwargs(self):
|
|
raise NotImplementedError()
|
|
|
|
def _internal_stop(self):
|
|
if not self._is_running:
|
|
return
|
|
|
|
os.kill(os.getpid(), signal.SIGUSR1)
|
|
self._signals_reset_event.wait()
|
|
termios.tcsetattr(sys.stdin.fileno(), termios.TCSAFLUSH, self.old_termios)
|
|
|
|
try:
|
|
children = psutil.Process(self.popen.pid).children(recursive=True)
|
|
for c in children:
|
|
c.terminate()
|
|
_, alive = psutil.wait_procs(children, timeout=self._GRACEFUL_SHUTDOWN_TIMEOUT_SEC)
|
|
for a in alive:
|
|
a.kill()
|
|
except psutil.NoSuchProcess:
|
|
pass
|
|
finally:
|
|
self.__class__._STARTED_INSTANCE = None
|
|
self._is_running = False
|
|
self._child_exited_event.set()
|
|
|
|
def _wait_for_child(self):
|
|
self.popen.wait()
|
|
with self._is_running_lock:
|
|
self._internal_stop()
|
|
|
|
@classmethod
|
|
def _sigusr1_handler(cls, signum, stack_frame): # pylint: disable=unused-argument
|
|
assert (
|
|
cls._STARTED_INSTANCE is not None
|
|
), "overridden sigusr1 handler should not be invoked when GDB not started"
|
|
signal.signal(signal.SIGINT, cls._STARTED_INSTANCE.old_sigint_handler)
|
|
signal.signal(signal.SIGUSR1, cls._STARTED_INSTANCE.old_sigusr1_handler)
|
|
cls._STARTED_INSTANCE._signals_reset_event.set()
|
|
|
|
@classmethod
|
|
def _sigint_handler(cls, signum, stack_frame): # pylint: disable=unused-argument
|
|
assert (
|
|
cls._STARTED_INSTANCE is not None
|
|
), "overridden sigint handler should not be invoked when GDB not started"
|
|
with cls._STARTED_INSTANCE._is_running_lock:
|
|
exists = cls._STARTED_INSTANCE._is_running
|
|
if exists:
|
|
try:
|
|
os.killpg(cls._STARTED_INSTANCE.child_pgid, signal.SIGINT)
|
|
except ProcessLookupError:
|
|
pass
|
|
|
|
def start(self):
|
|
with self._is_running_lock:
|
|
assert not self._is_running
|
|
assert not self._STARTED_INSTANCE
|
|
|
|
kwargs = self.popen_kwargs()
|
|
self.did_start_new_session = kwargs.setdefault("start_new_session", True)
|
|
|
|
self.old_termios = termios.tcgetattr(sys.stdin.fileno())
|
|
self.popen = subprocess.Popen(**kwargs)
|
|
self._is_running = True
|
|
self.old_sigint_handler = signal.signal(signal.SIGINT, self._sigint_handler)
|
|
self.old_sigusr1_handler = signal.signal(signal.SIGUSR1, self._sigusr1_handler)
|
|
self.__class__._STARTED_INSTANCE = self
|
|
try:
|
|
self.child_pgid = os.getpgid(self.popen.pid)
|
|
except Exception:
|
|
self.stop()
|
|
raise
|
|
with self._is_running_lock:
|
|
self._is_child_alive = True
|
|
t = threading.Thread(target=self._wait_for_child)
|
|
t.daemon = True
|
|
t.start()
|
|
|
|
def stop(self):
|
|
self._child_exited_event.wait()
|
|
|
|
|
|
atexit.register(GdbDebugger._stop_all)
|
|
|
|
|
|
class GdbTransportDebugger(GdbDebugger):
|
|
"""A debugger that uses a single GDB subprocess as both the transport and the debugger.
|
|
|
|
Opens pipes for the target's stdin and stdout, launches GDB and configures GDB's target
|
|
arguments to read and write from the pipes using /dev/fd.
|
|
"""
|
|
|
|
def __init__(self, args, **popen_kw):
|
|
super(GdbTransportDebugger, self).__init__()
|
|
self.args = args
|
|
self.popen_kw = popen_kw
|
|
|
|
def popen_kwargs(self):
|
|
stdin_read, stdin_write = os.pipe()
|
|
stdout_read, stdout_write = os.pipe()
|
|
|
|
os.set_inheritable(stdin_read, True)
|
|
os.set_inheritable(stdout_write, True)
|
|
|
|
sysname = os.uname()[0]
|
|
if sysname == "Darwin":
|
|
args = [
|
|
"lldb",
|
|
"-O",
|
|
f"target create {self.args[0]}",
|
|
"-O",
|
|
f"settings set target.input-path /dev/fd/{stdin_read}",
|
|
"-O",
|
|
f"settings set target.output-path /dev/fd/{stdout_write}",
|
|
]
|
|
if len(self.args) > 1:
|
|
args.extend(
|
|
["-O", "settings set target.run-args {}".format(" ".join(self.args[1:]))]
|
|
)
|
|
elif sysname == "Linux":
|
|
args = [
|
|
"gdb",
|
|
"-ex",
|
|
f"file {self.args[0]}",
|
|
"-ex",
|
|
(
|
|
f"set args {' '.join(shlex.quote(a) for a in self.args[1:])} "
|
|
f"</dev/fd/{stdin_read} >/dev/fd/{stdout_write}"
|
|
),
|
|
]
|
|
else:
|
|
raise NotImplementedError(f"System {sysname} is not yet supported")
|
|
|
|
self.fd_transport = FdTransport(
|
|
stdout_read, stdin_write, transport.debug_transport_timeouts()
|
|
)
|
|
self.fd_transport.open()
|
|
|
|
return {
|
|
"args": args,
|
|
"pass_fds": [stdin_read, stdout_write],
|
|
}
|
|
|
|
def _internal_stop(self):
|
|
self.fd_transport.close()
|
|
super(GdbTransportDebugger, self)._internal_stop()
|
|
|
|
class _Transport(transport.Transport):
|
|
def __init__(self, gdb_transport_debugger):
|
|
self.gdb_transport_debugger = gdb_transport_debugger
|
|
|
|
def timeouts(self):
|
|
return transport.debug_transport_timeouts()
|
|
|
|
def open(self):
|
|
pass # Pipes opened by parent class.
|
|
|
|
def write(self, data, timeout_sec):
|
|
end_time = time.monotonic() + timeout_sec if timeout_sec is not None else None
|
|
while True:
|
|
try:
|
|
return self.gdb_transport_debugger.fd_transport.write(data, timeout_sec)
|
|
except OSError as exc:
|
|
# NOTE: this error sometimes happens when writes are initiated before the child
|
|
# process launches.
|
|
if exc.errno == errno.EAGAIN:
|
|
if end_time is None or time.monotonic() < end_time:
|
|
time.sleep(0.1) # sleep to avoid excessive CPU usage
|
|
continue
|
|
|
|
raise exc
|
|
|
|
raise base.IoTimeoutError()
|
|
|
|
def read(self, n, timeout_sec):
|
|
end_time = time.monotonic() + timeout_sec if timeout_sec is not None else None
|
|
while True:
|
|
try:
|
|
return self.gdb_transport_debugger.fd_transport.read(n, timeout_sec)
|
|
except OSError as exc:
|
|
# NOTE: this error sometimes happens when reads are initiated before the child
|
|
# process launches.
|
|
if exc.errno == errno.EAGAIN:
|
|
if end_time is None or time.monotonic() < end_time:
|
|
time.sleep(0.1) # sleep to avoid excessive CPU usage
|
|
continue
|
|
|
|
raise exc
|
|
|
|
raise base.IoTimeoutError()
|
|
|
|
def close(self):
|
|
pass # Pipes closed by parent class (DebugWrapperTransport calls stop() next).
|
|
|
|
def transport(self):
|
|
return self._Transport(self)
|
|
|
|
|
|
class GdbRemoteDebugger(GdbDebugger):
|
|
"""A Debugger that invokes GDB and attaches to a remote GDBserver-based target."""
|
|
|
|
def __init__(
|
|
self, gdb_binary, remote_hostport, debug_binary, wrapping_context_manager=None, **popen_kw
|
|
):
|
|
super(GdbRemoteDebugger, self).__init__()
|
|
self.gdb_binary = gdb_binary
|
|
self.remote_hostport = remote_hostport
|
|
self.debug_binary = debug_binary
|
|
self.wrapping_context_manager = wrapping_context_manager
|
|
self.popen_kw = popen_kw
|
|
|
|
def popen_kwargs(self):
|
|
kwargs = {
|
|
"args": [
|
|
self.gdb_binary,
|
|
"-iex",
|
|
f"file {self.debug_binary}",
|
|
"-iex",
|
|
f"target remote {self.remote_hostport}",
|
|
],
|
|
}
|
|
kwargs.update(self.popen_kw)
|
|
|
|
return kwargs
|
|
|
|
def start(self):
|
|
if self.wrapping_context_manager is not None:
|
|
self.wrapping_context_manager.__enter__()
|
|
super(GdbRemoteDebugger, self).start()
|
|
|
|
def stop(self):
|
|
try:
|
|
super(GdbRemoteDebugger, self).stop()
|
|
finally:
|
|
if self.wrapping_context_manager is not None:
|
|
self.wrapping_context_manager.__exit__(None, None, None)
|
|
|
|
|
|
GLOBAL_DEBUGGER = None
|
|
|
|
|
|
class DebuggerFactory(class_factory.ClassFactory):
|
|
|
|
SUPERCLASS = Debugger
|
|
|
|
|
|
def launch_debugger(debugger_factory, *args, **kw):
|
|
global GLOBAL_DEBUGGER
|
|
if GLOBAL_DEBUGGER is not None:
|
|
stop_debugger()
|
|
|
|
GLOBAL_DEBUGGER = debugger_factory.instantiate(*args, **kw)
|
|
GLOBAL_DEBUGGER.start()
|
|
|
|
|
|
@register_func("tvm.micro.debugger.launch_debugger")
|
|
def _launch_debugger(debugger_factory_json):
|
|
launch_debugger(DebuggerFactory.from_json(debugger_factory_json))
|
|
|
|
|
|
@register_func("tvm.micro.debugger.stop_debugger")
|
|
def stop_debugger():
|
|
global GLOBAL_DEBUGGER
|
|
if GLOBAL_DEBUGGER is not None:
|
|
try:
|
|
GLOBAL_DEBUGGER.stop()
|
|
finally:
|
|
GLOBAL_DEBUGGER = None
|
|
|
|
|
|
class RpcDebugger(Debugger):
|
|
"""A Debugger instance that launches the actual debugger on a remote TVM RPC server."""
|
|
|
|
def __init__(self, rpc_session, factory, wrapping_context_manager=None):
|
|
super(RpcDebugger, self).__init__()
|
|
self._factory = factory
|
|
self.launch_debugger = rpc_session.get_function("tvm.micro.debugger.launch_debugger")
|
|
self.stop_debugger = rpc_session.get_function("tvm.micro.debugger.stop_debugger")
|
|
self.wrapping_context_manager = wrapping_context_manager
|
|
|
|
def start(self):
|
|
if self.wrapping_context_manager is not None:
|
|
self.wrapping_context_manager.__enter__()
|
|
|
|
try:
|
|
self.launch_debugger(self._factory.to_json)
|
|
except Exception:
|
|
if self.wrapping_context_manager is not None:
|
|
self.wrapping_context_manager.__exit__(None, None, None)
|
|
raise
|
|
|
|
try:
|
|
input("Press [Enter] when debugger is set")
|
|
except Exception:
|
|
self.stop()
|
|
raise
|
|
|
|
self._is_running = True
|
|
|
|
def stop(self):
|
|
try:
|
|
self.stop_debugger()
|
|
finally:
|
|
if self.wrapping_context_manager is not None:
|
|
self.wrapping_context_manager.__exit__(None, None, None)
|