The issue with these test failures is that the dSYM was not being found by lldb, which is why setting breakpoints was failing and lldb quit without performing any steps. This change copies the dSYM to the same temp directory that the executable is copied to.
333 lines
11 KiB
Python
333 lines
11 KiB
Python
# 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 platform
|
|
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.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
|
|
|
|
context.logger.warning(
|
|
f'option "{option}" is meaningless with this debugger',
|
|
enable_prefix=True,
|
|
flag=f"--debugger={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 = platform.machine()
|
|
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",
|
|
type=str,
|
|
metavar="<directory>",
|
|
default=None,
|
|
help="source root directory",
|
|
)
|
|
parser.add_argument(
|
|
"--debugger-use-relative-paths",
|
|
action="store_true",
|
|
default=False,
|
|
help="pass the debugger paths relative to --source-root-dir",
|
|
)
|
|
parser.add_argument(
|
|
"--target-run-args",
|
|
type=str,
|
|
metavar="<flags>",
|
|
default="",
|
|
help="command line arguments for the test program, in addition to any "
|
|
"provided by DexCommandLine",
|
|
)
|
|
parser.add_argument(
|
|
"--timeout-total",
|
|
metavar="<seconds>",
|
|
type=float,
|
|
default=0.0,
|
|
help="if >0, debugger session will automatically exit after "
|
|
"running for <timeout-total> seconds",
|
|
)
|
|
parser.add_argument(
|
|
"--timeout-breakpoint",
|
|
metavar="<seconds>",
|
|
type=float,
|
|
default=0.0,
|
|
help="if >0, debugger session will automatically exit after "
|
|
"waiting <timeout-breakpoint> seconds without hitting a "
|
|
"breakpoint",
|
|
)
|
|
|
|
|
|
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")
|
|
|
|
if options.source_root_dir != None:
|
|
if not os.path.isabs(options.source_root_dir):
|
|
raise ToolArgumentError(
|
|
f'<d>--source-root-dir: expected absolute path, got</> <r>"{options.source_root_dir}"</>'
|
|
)
|
|
if not os.path.isdir(options.source_root_dir):
|
|
raise ToolArgumentError(
|
|
f'<d>--source-root-dir: could not find directory</> <r>"{options.source_root_dir}"</>'
|
|
)
|
|
|
|
if options.debugger_use_relative_paths:
|
|
if not options.source_root_dir:
|
|
raise ToolArgumentError(
|
|
f"<d>--debugger-relative-paths</> <r>requires --source-root-dir</>"
|
|
)
|
|
|
|
|
|
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)))
|