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