xref: /llvm-project/cross-project-tests/debuginfo-tests/dexter/dex/debugger/Debuggers.py (revision ca92bdfa3ef8f9a1cc97167fc96601f8bd7b436b)
1# DExTer : Debugging Experience Tester
2# ~~~~~~   ~         ~~         ~   ~~
3#
4# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
5# See https://llvm.org/LICENSE.txt for license information.
6# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7"""Discover potential/available debugger interfaces."""
8
9from collections import OrderedDict
10import os
11import pickle
12import platform
13import subprocess
14import sys
15from tempfile import NamedTemporaryFile
16
17from dex.command import get_command_infos
18from dex.dextIR import DextIR
19from dex.utils import get_root_directory, Timer
20from dex.utils.Environment import is_native_windows
21from dex.utils.Exceptions import ToolArgumentError
22from dex.utils.Exceptions import DebuggerException
23
24from dex.debugger.DebuggerControllers.DefaultController import DefaultController
25
26from dex.debugger.dbgeng.dbgeng import DbgEng
27from dex.debugger.lldb.LLDB import LLDB
28from dex.debugger.visualstudio.VisualStudio2015 import VisualStudio2015
29from dex.debugger.visualstudio.VisualStudio2017 import VisualStudio2017
30from dex.debugger.visualstudio.VisualStudio2019 import VisualStudio2019
31from dex.debugger.visualstudio.VisualStudio2022 import VisualStudio2022
32
33
34def _get_potential_debuggers():  # noqa
35    """Return a dict of the supported debuggers.
36    Returns:
37        { name (str): debugger (class) }
38    """
39    return {
40        DbgEng.get_option_name(): DbgEng,
41        LLDB.get_option_name(): LLDB,
42        VisualStudio2015.get_option_name(): VisualStudio2015,
43        VisualStudio2017.get_option_name(): VisualStudio2017,
44        VisualStudio2019.get_option_name(): VisualStudio2019,
45        VisualStudio2022.get_option_name(): VisualStudio2022,
46    }
47
48
49def _warn_meaningless_option(context, option):
50    if hasattr(context.options, "list_debuggers"):
51        return
52
53    context.logger.warning(
54        f'option "{option}" is meaningless with this debugger',
55        enable_prefix=True,
56        flag=f"--debugger={context.options.debugger}",
57    )
58
59
60def add_debugger_tool_base_arguments(parser, defaults):
61    defaults.lldb_executable = "lldb.exe" if is_native_windows() else "lldb"
62    parser.add_argument(
63        "--lldb-executable",
64        type=str,
65        metavar="<file>",
66        default=None,
67        display_default=defaults.lldb_executable,
68        help="location of LLDB executable",
69    )
70
71
72def add_debugger_tool_arguments(parser, context, defaults):
73    debuggers = Debuggers(context)
74    potential_debuggers = sorted(debuggers.potential_debuggers().keys())
75
76    add_debugger_tool_base_arguments(parser, defaults)
77
78    parser.add_argument(
79        "--debugger",
80        type=str,
81        choices=potential_debuggers,
82        required=True,
83        help="debugger to use",
84    )
85    parser.add_argument(
86        "--max-steps",
87        metavar="<int>",
88        type=int,
89        default=1000,
90        help="maximum number of program steps allowed",
91    )
92    parser.add_argument(
93        "--pause-between-steps",
94        metavar="<seconds>",
95        type=float,
96        default=0.0,
97        help="number of seconds to pause between steps",
98    )
99    defaults.show_debugger = False
100    parser.add_argument(
101        "--show-debugger", action="store_true", default=None, help="show the debugger"
102    )
103    defaults.arch = platform.machine()
104    parser.add_argument(
105        "--arch",
106        type=str,
107        metavar="<architecture>",
108        default=None,
109        display_default=defaults.arch,
110        help="target architecture",
111    )
112    defaults.source_root_dir = ""
113    parser.add_argument(
114        "--source-root-dir",
115        type=str,
116        metavar="<directory>",
117        default=None,
118        help="source root directory",
119    )
120    parser.add_argument(
121        "--debugger-use-relative-paths",
122        action="store_true",
123        default=False,
124        help="pass the debugger paths relative to --source-root-dir",
125    )
126    parser.add_argument(
127        "--target-run-args",
128        type=str,
129        metavar="<flags>",
130        default="",
131        help="command line arguments for the test program, in addition to any "
132        "provided by DexCommandLine",
133    )
134    parser.add_argument(
135        "--timeout-total",
136        metavar="<seconds>",
137        type=float,
138        default=0.0,
139        help="if >0, debugger session will automatically exit after "
140        "running for <timeout-total> seconds",
141    )
142    parser.add_argument(
143        "--timeout-breakpoint",
144        metavar="<seconds>",
145        type=float,
146        default=0.0,
147        help="if >0, debugger session will automatically exit after "
148        "waiting <timeout-breakpoint> seconds without hitting a "
149        "breakpoint",
150    )
151
152
153def handle_debugger_tool_base_options(context, defaults):  # noqa
154    options = context.options
155
156    if options.lldb_executable is None:
157        options.lldb_executable = defaults.lldb_executable
158    else:
159        if getattr(options, "debugger", "lldb") != "lldb":
160            _warn_meaningless_option(context, "--lldb-executable")
161
162        options.lldb_executable = os.path.abspath(options.lldb_executable)
163        if not os.path.isfile(options.lldb_executable):
164            raise ToolArgumentError(
165                '<d>could not find</> <r>"{}"</>'.format(options.lldb_executable)
166            )
167
168
169def handle_debugger_tool_options(context, defaults):  # noqa
170    options = context.options
171
172    handle_debugger_tool_base_options(context, defaults)
173
174    if options.arch is None:
175        options.arch = defaults.arch
176    else:
177        if options.debugger != "lldb":
178            _warn_meaningless_option(context, "--arch")
179
180    if options.show_debugger is None:
181        options.show_debugger = defaults.show_debugger
182    else:
183        if options.debugger == "lldb":
184            _warn_meaningless_option(context, "--show-debugger")
185
186    if options.source_root_dir is not None:
187        if not os.path.isabs(options.source_root_dir):
188            raise ToolArgumentError(
189                f'<d>--source-root-dir: expected absolute path, got</> <r>"{options.source_root_dir}"</>'
190            )
191        if not os.path.isdir(options.source_root_dir):
192            raise ToolArgumentError(
193                f'<d>--source-root-dir: could not find directory</> <r>"{options.source_root_dir}"</>'
194            )
195
196    if options.debugger_use_relative_paths:
197        if not options.source_root_dir:
198            raise ToolArgumentError(
199                f"<d>--debugger-relative-paths</> <r>requires --source-root-dir</>"
200            )
201
202
203def run_debugger_subprocess(debugger_controller, working_dir_path):
204    with NamedTemporaryFile(dir=working_dir_path, delete=False, mode="wb") as fp:
205        pickle.dump(debugger_controller, fp, protocol=pickle.HIGHEST_PROTOCOL)
206        controller_path = fp.name
207
208    dexter_py = os.path.basename(sys.argv[0])
209    if not os.path.isfile(dexter_py):
210        dexter_py = os.path.join(get_root_directory(), "..", dexter_py)
211    assert os.path.isfile(dexter_py)
212
213    with NamedTemporaryFile(dir=working_dir_path) as fp:
214        args = [
215            sys.executable,
216            dexter_py,
217            "run-debugger-internal-",
218            controller_path,
219            "--working-directory={}".format(working_dir_path),
220            "--unittest=off",
221            "--indent-timer-level={}".format(Timer.indent + 2),
222        ]
223        try:
224            with Timer("running external debugger process"):
225                subprocess.check_call(args)
226        except subprocess.CalledProcessError as e:
227            raise DebuggerException(e)
228
229    with open(controller_path, "rb") as fp:
230        debugger_controller = pickle.load(fp)
231    return debugger_controller
232
233
234class Debuggers(object):
235    @classmethod
236    def potential_debuggers(cls):
237        try:
238            return cls._potential_debuggers
239        except AttributeError:
240            cls._potential_debuggers = _get_potential_debuggers()
241            return cls._potential_debuggers
242
243    def __init__(self, context):
244        self.context = context
245
246    def load(self, key):
247        with Timer("load {}".format(key)):
248            return Debuggers.potential_debuggers()[key](self.context)
249
250    def _populate_debugger_cache(self):
251        debuggers = []
252        for key in sorted(Debuggers.potential_debuggers()):
253            debugger = self.load(key)
254
255            class LoadedDebugger(object):
256                pass
257
258            LoadedDebugger.option_name = key
259            LoadedDebugger.full_name = "[{}]".format(debugger.name)
260            LoadedDebugger.is_available = debugger.is_available
261
262            if LoadedDebugger.is_available:
263                try:
264                    LoadedDebugger.version = debugger.version.splitlines()
265                except AttributeError:
266                    LoadedDebugger.version = [""]
267            else:
268                try:
269                    LoadedDebugger.error = debugger.loading_error.splitlines()
270                except AttributeError:
271                    LoadedDebugger.error = [""]
272
273                try:
274                    LoadedDebugger.error_trace = debugger.loading_error_trace
275                except AttributeError:
276                    LoadedDebugger.error_trace = None
277
278            debuggers.append(LoadedDebugger)
279        return debuggers
280
281    def list(self):
282        debuggers = self._populate_debugger_cache()
283
284        max_o_len = max(len(d.option_name) for d in debuggers)
285        max_n_len = max(len(d.full_name) for d in debuggers)
286
287        msgs = []
288
289        for d in debuggers:
290            # Option name, right padded with spaces for alignment
291            option_name = "{{name: <{}}}".format(max_o_len).format(name=d.option_name)
292
293            # Full name, right padded with spaces for alignment
294            full_name = "{{name: <{}}}".format(max_n_len).format(name=d.full_name)
295
296            if d.is_available:
297                name = "<b>{} {}</>".format(option_name, full_name)
298
299                # If the debugger is available, show the first line of the
300                #  version info.
301                available = "<g>YES</>"
302                info = "<b>({})</>".format(d.version[0])
303            else:
304                name = "<y>{} {}</>".format(option_name, full_name)
305
306                # If the debugger is not available, show the first line of the
307                # error reason.
308                available = "<r>NO</> "
309                info = "<y>({})</>".format(d.error[0])
310
311            msg = "{} {} {}".format(name, available, info)
312
313            if self.context.options.verbose:
314                # If verbose mode and there was more version or error output
315                # than could be displayed in a single line, display the whole
316                # lot slightly indented.
317                verbose_info = None
318                if d.is_available:
319                    if d.version[1:]:
320                        verbose_info = d.version + ["\n"]
321                else:
322                    # Some of list elems may contain multiple lines, so make
323                    # sure each elem is a line of its own.
324                    verbose_info = d.error_trace
325
326                if verbose_info:
327                    verbose_info = (
328                        "\n".join("        {}".format(l.rstrip()) for l in verbose_info)
329                        + "\n"
330                    )
331                    msg = "{}\n\n{}".format(msg, verbose_info)
332
333            msgs.append(msg)
334        self.context.o.auto("\n{}\n\n".format("\n".join(msgs)))
335