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        default=None,
109        help='prefix path to ignore when matching debug info and source files.')
110
111
112def handle_debugger_tool_base_options(context, defaults):  # noqa
113    options = context.options
114
115    if options.lldb_executable is None:
116        options.lldb_executable = defaults.lldb_executable
117    else:
118        if getattr(options, 'debugger', 'lldb') != 'lldb':
119            _warn_meaningless_option(context, '--lldb-executable')
120
121        options.lldb_executable = os.path.abspath(options.lldb_executable)
122        if not os.path.isfile(options.lldb_executable):
123            raise ToolArgumentError('<d>could not find</> <r>"{}"</>'.format(
124                options.lldb_executable))
125
126
127def handle_debugger_tool_options(context, defaults):  # noqa
128    options = context.options
129
130    handle_debugger_tool_base_options(context, defaults)
131
132    if options.arch is None:
133        options.arch = defaults.arch
134    else:
135        if options.debugger != 'lldb':
136            _warn_meaningless_option(context, '--arch')
137
138    if options.show_debugger is None:
139        options.show_debugger = defaults.show_debugger
140    else:
141        if options.debugger == 'lldb':
142            _warn_meaningless_option(context, '--show-debugger')
143
144
145def run_debugger_subprocess(debugger_controller, working_dir_path):
146    with NamedTemporaryFile(
147            dir=working_dir_path, delete=False, mode='wb') as fp:
148        pickle.dump(debugger_controller, fp, protocol=pickle.HIGHEST_PROTOCOL)
149        controller_path = fp.name
150
151    dexter_py = os.path.basename(sys.argv[0])
152    if not os.path.isfile(dexter_py):
153        dexter_py = os.path.join(get_root_directory(), '..', dexter_py)
154    assert os.path.isfile(dexter_py)
155
156    with NamedTemporaryFile(dir=working_dir_path) as fp:
157        args = [
158            sys.executable,
159            dexter_py,
160            'run-debugger-internal-',
161            controller_path,
162            '--working-directory={}'.format(working_dir_path),
163            '--unittest=off',
164            '--indent-timer-level={}'.format(Timer.indent + 2)
165        ]
166        try:
167            with Timer('running external debugger process'):
168                subprocess.check_call(args)
169        except subprocess.CalledProcessError as e:
170            raise DebuggerException(e)
171
172    with open(controller_path, 'rb') as fp:
173        debugger_controller = pickle.load(fp)
174
175    return debugger_controller
176
177
178class Debuggers(object):
179    @classmethod
180    def potential_debuggers(cls):
181        try:
182            return cls._potential_debuggers
183        except AttributeError:
184            cls._potential_debuggers = _get_potential_debuggers()
185            return cls._potential_debuggers
186
187    def __init__(self, context):
188        self.context = context
189
190    def load(self, key):
191        with Timer('load {}'.format(key)):
192            return Debuggers.potential_debuggers()[key](self.context)
193
194    def _populate_debugger_cache(self):
195        debuggers = []
196        for key in sorted(Debuggers.potential_debuggers()):
197            debugger = self.load(key)
198
199            class LoadedDebugger(object):
200                pass
201
202            LoadedDebugger.option_name = key
203            LoadedDebugger.full_name = '[{}]'.format(debugger.name)
204            LoadedDebugger.is_available = debugger.is_available
205
206            if LoadedDebugger.is_available:
207                try:
208                    LoadedDebugger.version = debugger.version.splitlines()
209                except AttributeError:
210                    LoadedDebugger.version = ['']
211            else:
212                try:
213                    LoadedDebugger.error = debugger.loading_error.splitlines()
214                except AttributeError:
215                    LoadedDebugger.error = ['']
216
217                try:
218                    LoadedDebugger.error_trace = debugger.loading_error_trace
219                except AttributeError:
220                    LoadedDebugger.error_trace = None
221
222            debuggers.append(LoadedDebugger)
223        return debuggers
224
225    def list(self):
226        debuggers = self._populate_debugger_cache()
227
228        max_o_len = max(len(d.option_name) for d in debuggers)
229        max_n_len = max(len(d.full_name) for d in debuggers)
230
231        msgs = []
232
233        for d in debuggers:
234            # Option name, right padded with spaces for alignment
235            option_name = (
236                '{{name: <{}}}'.format(max_o_len).format(name=d.option_name))
237
238            # Full name, right padded with spaces for alignment
239            full_name = ('{{name: <{}}}'.format(max_n_len)
240                         .format(name=d.full_name))
241
242            if d.is_available:
243                name = '<b>{} {}</>'.format(option_name, full_name)
244
245                # If the debugger is available, show the first line of the
246                #  version info.
247                available = '<g>YES</>'
248                info = '<b>({})</>'.format(d.version[0])
249            else:
250                name = '<y>{} {}</>'.format(option_name, full_name)
251
252                # If the debugger is not available, show the first line of the
253                # error reason.
254                available = '<r>NO</> '
255                info = '<y>({})</>'.format(d.error[0])
256
257            msg = '{} {} {}'.format(name, available, info)
258
259            if self.context.options.verbose:
260                # If verbose mode and there was more version or error output
261                # than could be displayed in a single line, display the whole
262                # lot slightly indented.
263                verbose_info = None
264                if d.is_available:
265                    if d.version[1:]:
266                        verbose_info = d.version + ['\n']
267                else:
268                    # Some of list elems may contain multiple lines, so make
269                    # sure each elem is a line of its own.
270                    verbose_info = d.error_trace
271
272                if verbose_info:
273                    verbose_info = '\n'.join('        {}'.format(l.rstrip())
274                                             for l in verbose_info) + '\n'
275                    msg = '{}\n\n{}'.format(msg, verbose_info)
276
277            msgs.append(msg)
278        self.context.o.auto('\n{}\n\n'.format('\n'.join(msgs)))
279