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