# DExTer : Debugging Experience Tester # ~~~~~~ ~ ~~ ~ ~~ # # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. # See https://llvm.org/LICENSE.txt for license information. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception """Discover potential/available debugger interfaces.""" from collections import OrderedDict import os import pickle import subprocess import sys from tempfile import NamedTemporaryFile from dex.command import get_command_infos from dex.dextIR import DextIR from dex.utils import get_root_directory, Timer from dex.utils.Environment import is_native_windows from dex.utils.Exceptions import ToolArgumentError from dex.utils.Warning import warn from dex.utils.Exceptions import DebuggerException from dex.debugger.DebuggerControllers.DefaultController import DefaultController from dex.debugger.dbgeng.dbgeng import DbgEng from dex.debugger.lldb.LLDB import LLDB from dex.debugger.visualstudio.VisualStudio2015 import VisualStudio2015 from dex.debugger.visualstudio.VisualStudio2017 import VisualStudio2017 from dex.debugger.visualstudio.VisualStudio2019 import VisualStudio2019 def _get_potential_debuggers(): # noqa """Return a dict of the supported debuggers. Returns: { name (str): debugger (class) } """ return { DbgEng.get_option_name(): DbgEng, LLDB.get_option_name(): LLDB, VisualStudio2015.get_option_name(): VisualStudio2015, VisualStudio2017.get_option_name(): VisualStudio2017, VisualStudio2019.get_option_name(): VisualStudio2019 } def _warn_meaningless_option(context, option): if hasattr(context.options, 'list_debuggers'): return warn(context, 'option "{}" is meaningless with this debugger'.format(option), '--debugger={}'.format(context.options.debugger)) def add_debugger_tool_base_arguments(parser, defaults): defaults.lldb_executable = 'lldb.exe' if is_native_windows() else 'lldb' parser.add_argument( '--lldb-executable', type=str, metavar='', default=None, display_default=defaults.lldb_executable, help='location of LLDB executable') def add_debugger_tool_arguments(parser, context, defaults): debuggers = Debuggers(context) potential_debuggers = sorted(debuggers.potential_debuggers().keys()) add_debugger_tool_base_arguments(parser, defaults) parser.add_argument( '--debugger', type=str, choices=potential_debuggers, required=True, help='debugger to use') parser.add_argument( '--max-steps', metavar='', type=int, default=1000, help='maximum number of program steps allowed') parser.add_argument( '--pause-between-steps', metavar='', type=float, default=0.0, help='number of seconds to pause between steps') defaults.show_debugger = False parser.add_argument( '--show-debugger', action='store_true', default=None, help='show the debugger') defaults.arch = 'x86_64' parser.add_argument( '--arch', type=str, metavar='', default=None, display_default=defaults.arch, help='target architecture') defaults.source_root_dir = '' parser.add_argument( '--source-root-dir', default=None, help='prefix path to ignore when matching debug info and source files.') def handle_debugger_tool_base_options(context, defaults): # noqa options = context.options if options.lldb_executable is None: options.lldb_executable = defaults.lldb_executable else: if getattr(options, 'debugger', 'lldb') != 'lldb': _warn_meaningless_option(context, '--lldb-executable') options.lldb_executable = os.path.abspath(options.lldb_executable) if not os.path.isfile(options.lldb_executable): raise ToolArgumentError('could not find "{}"'.format( options.lldb_executable)) def handle_debugger_tool_options(context, defaults): # noqa options = context.options handle_debugger_tool_base_options(context, defaults) if options.arch is None: options.arch = defaults.arch else: if options.debugger != 'lldb': _warn_meaningless_option(context, '--arch') if options.show_debugger is None: options.show_debugger = defaults.show_debugger else: if options.debugger == 'lldb': _warn_meaningless_option(context, '--show-debugger') def run_debugger_subprocess(debugger_controller, working_dir_path): with NamedTemporaryFile( dir=working_dir_path, delete=False, mode='wb') as fp: pickle.dump(debugger_controller, fp, protocol=pickle.HIGHEST_PROTOCOL) controller_path = fp.name dexter_py = os.path.basename(sys.argv[0]) if not os.path.isfile(dexter_py): dexter_py = os.path.join(get_root_directory(), '..', dexter_py) assert os.path.isfile(dexter_py) with NamedTemporaryFile(dir=working_dir_path) as fp: args = [ sys.executable, dexter_py, 'run-debugger-internal-', controller_path, '--working-directory={}'.format(working_dir_path), '--unittest=off', '--indent-timer-level={}'.format(Timer.indent + 2) ] try: with Timer('running external debugger process'): subprocess.check_call(args) except subprocess.CalledProcessError as e: raise DebuggerException(e) with open(controller_path, 'rb') as fp: debugger_controller = pickle.load(fp) return debugger_controller class Debuggers(object): @classmethod def potential_debuggers(cls): try: return cls._potential_debuggers except AttributeError: cls._potential_debuggers = _get_potential_debuggers() return cls._potential_debuggers def __init__(self, context): self.context = context def load(self, key): with Timer('load {}'.format(key)): return Debuggers.potential_debuggers()[key](self.context) def _populate_debugger_cache(self): debuggers = [] for key in sorted(Debuggers.potential_debuggers()): debugger = self.load(key) class LoadedDebugger(object): pass LoadedDebugger.option_name = key LoadedDebugger.full_name = '[{}]'.format(debugger.name) LoadedDebugger.is_available = debugger.is_available if LoadedDebugger.is_available: try: LoadedDebugger.version = debugger.version.splitlines() except AttributeError: LoadedDebugger.version = [''] else: try: LoadedDebugger.error = debugger.loading_error.splitlines() except AttributeError: LoadedDebugger.error = [''] try: LoadedDebugger.error_trace = debugger.loading_error_trace except AttributeError: LoadedDebugger.error_trace = None debuggers.append(LoadedDebugger) return debuggers def list(self): debuggers = self._populate_debugger_cache() max_o_len = max(len(d.option_name) for d in debuggers) max_n_len = max(len(d.full_name) for d in debuggers) msgs = [] for d in debuggers: # Option name, right padded with spaces for alignment option_name = ( '{{name: <{}}}'.format(max_o_len).format(name=d.option_name)) # Full name, right padded with spaces for alignment full_name = ('{{name: <{}}}'.format(max_n_len) .format(name=d.full_name)) if d.is_available: name = '{} {}'.format(option_name, full_name) # If the debugger is available, show the first line of the # version info. available = 'YES' info = '({})'.format(d.version[0]) else: name = '{} {}'.format(option_name, full_name) # If the debugger is not available, show the first line of the # error reason. available = 'NO ' info = '({})'.format(d.error[0]) msg = '{} {} {}'.format(name, available, info) if self.context.options.verbose: # If verbose mode and there was more version or error output # than could be displayed in a single line, display the whole # lot slightly indented. verbose_info = None if d.is_available: if d.version[1:]: verbose_info = d.version + ['\n'] else: # Some of list elems may contain multiple lines, so make # sure each elem is a line of its own. verbose_info = d.error_trace if verbose_info: verbose_info = '\n'.join(' {}'.format(l.rstrip()) for l in verbose_info) + '\n' msg = '{}\n\n{}'.format(msg, verbose_info) msgs.append(msg) self.context.o.auto('\n{}\n\n'.format('\n'.join(msgs)))