1# Licensed to the Apache Software Foundation (ASF) under one 2# or more contributor license agreements. See the NOTICE file 3# distributed with this work for additional information 4# regarding copyright ownership. The ASF licenses this file 5# to you under the Apache License, Version 2.0 (the 6# "License"); you may not use this file except in compliance 7# with the License. You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, 12# software distributed under the License is distributed on an 13# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14# KIND, either express or implied. See the License for the 15# specific language governing permissions and limitations 16# under the License. 17 18"""Defines functions for controlling debuggers for micro TVM binaries.""" 19 20import abc 21import os 22import signal 23import subprocess 24import threading 25 26from . import transport as _transport 27 28 29class Debugger(metaclass=abc.ABCMeta): 30 """An interface for controlling micro TVM debuggers.""" 31 32 def __init__(self): 33 self.on_terminate_callbacks = [] 34 35 @abc.abstractmethod 36 def start(self): 37 """Start the debugger, but do not block on it. 38 39 The runtime will continue to be driven in the background. 40 """ 41 raise NotImplementedError() 42 43 @abc.abstractmethod 44 def stop(self): 45 """Terminate the debugger.""" 46 raise NotImplementedError() 47 48 49class GdbDebugger(Debugger): 50 """Handles launching, suspending signals, and potentially dealing with terminal issues.""" 51 52 @abc.abstractmethod 53 def popen_kwargs(self): 54 raise NotImplementedError() 55 56 def _wait_restore_signal(self): 57 self.popen.wait() 58 if not self.did_terminate.is_set(): 59 for callback in self.on_terminate_callbacks: 60 try: 61 callback() 62 except Exception: # pylint: disable=broad-except 63 logging.warn("on_terminate_callback raised exception", exc_info=True) 64 65 def start(self): 66 kwargs = self.popen_kwargs() 67 self.did_terminate = threading.Event() 68 self.old_signal = signal.signal(signal.SIGINT, signal.SIG_IGN) 69 self.popen = subprocess.Popen(**kwargs) 70 threading.Thread(target=self._WaitRestoreSignal).start() 71 72 def stop(self): 73 self.did_terminate.set() 74 self.popen.terminate() 75 signal.signal(signal.SIGINT, self.old_signal) 76 77 78class GdbTransportDebugger(GdbDebugger): 79 """A debugger that uses a single GDB subprocess as both the transport and the debugger. 80 81 Opens pipes for the target's stdin and stdout, launches GDB and configures GDB's target 82 arguments to read and write from the pipes using /dev/fd. 83 """ 84 85 def __init__(self, args, **popen_kw): 86 super(GdbTransportDebugger, self).__init__() 87 self.args = args 88 self.popen_kw = popen_kw 89 90 def popen_kwargs(self): 91 stdin_read, stdin_write = os.pipe() 92 stdout_read, stdout_write = os.pipe() 93 94 os.set_inheritable(stdin_read, True) 95 os.set_inheritable(stdout_write, True) 96 97 sysname = os.uname()[0] 98 if sysname == "Darwin": 99 args = [ 100 "lldb", 101 "-O", 102 f"target create {self.args[0]}", 103 "-O", 104 f"settings set target.input-path /dev/fd/{stdin_read}", 105 "-O", 106 f"settings set target.output-path /dev/fd/{stdout_write}", 107 ] 108 if len(self.args) > 1: 109 args.extend( 110 ["-O", "settings set target.run-args {}".format(" ".join(self.args[1:]))] 111 ) 112 elif sysname == "Linux": 113 args = ( 114 ["gdb", "--args"] + self.args + ["</dev/fd/{stdin_read}", ">/dev/fd/{stdout_write}"] 115 ) 116 else: 117 raise NotImplementedError(f"System {sysname} is not yet supported") 118 119 self.stdin = os.fdopen(stdin_write, "wb", buffering=0) 120 self.stdout = os.fdopen(stdout_read, "rb", buffering=0) 121 122 return { 123 "args": args, 124 "pass_fds": [stdin_read, stdout_write], 125 } 126 127 def _wait_for_process_death(self): 128 self.popen.wait() 129 self.stdin.close() 130 self.stdout.close() 131 132 def start(self): 133 to_return = super(GdbTransportDebugger, self).Start() 134 threading.Thread(target=self._wait_for_process_death, daemon=True).start() 135 return to_return 136 137 def stop(self): 138 self.stdin.close() 139 self.stdout.close() 140 super(GdbTransportDebugger, self).Stop() 141 142 class _Transport(_transport.Transport): 143 def __init__(self, gdb_transport_debugger): 144 self.gdb_transport_debugger = gdb_transport_debugger 145 146 def open(self): 147 pass # Pipes opened by parent class. 148 149 def write(self, data): 150 return self.gdb_transport_debugger.stdin.write(data) 151 152 def read(self, n): 153 return self.gdb_transport_debugger.stdout.read(n) 154 155 def close(self): 156 pass # Pipes closed by parent class. 157 158 def transport(self): 159 return self._Transport(self) 160 161 162class GdbRemoteDebugger(GdbDebugger): 163 """A Debugger that invokes GDB and attaches to a remote GDBserver-based target.""" 164 165 def __init__( 166 self, gdb_binary, remote_hostport, debug_binary, wrapping_context_manager=None, **popen_kw 167 ): 168 super(GdbRemoteDebugger, self).__init__() 169 self.gdb_binary = gdb_binary 170 self.remote_hostport = remote_hostport 171 self.debug_binary = debug_binary 172 self.wrapping_context_manager = wrapping_context_manager 173 self.popen_kw = popen_kw 174 175 def popen_kwargs(self): 176 kwargs = { 177 "args": [ 178 self.gdb_binary, 179 "-iex", 180 f"file {self.debug_binary}", 181 "-iex", 182 f"target remote {self.remote_hostport}", 183 ], 184 } 185 kwargs.update(self.popen_kw) 186 187 return kwargs 188 189 def start(self): 190 if self.wrapping_context_manager is not None: 191 self.wrapping_context_manager.__enter__() 192 super(GdbRemoteDebugger, self).Start() 193 194 def stop(self): 195 try: 196 super(GdbRemoteDebugger, self).Stop() 197 finally: 198 if self.wrapping_context_manager is not None: 199 self.wrapping_context_manager.__exit__(None, None, None) 200