You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
279 lines
9.4 KiB
279 lines
9.4 KiB
4 months ago
|
# 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 <y>"{}"</> 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='<file>',
|
||
|
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='<int>',
|
||
|
type=int,
|
||
|
default=1000,
|
||
|
help='maximum number of program steps allowed')
|
||
|
parser.add_argument(
|
||
|
'--pause-between-steps',
|
||
|
metavar='<seconds>',
|
||
|
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='<architecture>',
|
||
|
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('<d>could not find</> <r>"{}"</>'.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 = '<b>{} {}</>'.format(option_name, full_name)
|
||
|
|
||
|
# If the debugger is available, show the first line of the
|
||
|
# version info.
|
||
|
available = '<g>YES</>'
|
||
|
info = '<b>({})</>'.format(d.version[0])
|
||
|
else:
|
||
|
name = '<y>{} {}</>'.format(option_name, full_name)
|
||
|
|
||
|
# If the debugger is not available, show the first line of the
|
||
|
# error reason.
|
||
|
available = '<r>NO</> '
|
||
|
info = '<y>({})</>'.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)))
|