363 lines · python
1# DExTer : Debugging Experience Tester2# ~~~~~~ ~ ~~ ~ ~~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-exception7"""Discover potential/available debugger interfaces."""8 9from collections import OrderedDict10import os11import pickle12import platform13import subprocess14import sys15from tempfile import NamedTemporaryFile16 17from dex.command import get_command_infos18from dex.dextIR import DextIR19from dex.utils import get_root_directory, Timer20from dex.utils.Environment import is_native_windows21from dex.utils.Exceptions import ToolArgumentError22from dex.utils.Exceptions import DebuggerException23 24from dex.debugger.DebuggerControllers.DefaultController import DefaultController25 26from dex.debugger.dbgeng.dbgeng import DbgEng27from dex.debugger.lldb.LLDB import LLDB, LLDBDAP28from dex.debugger.visualstudio.VisualStudio2015 import VisualStudio201529from dex.debugger.visualstudio.VisualStudio2017 import VisualStudio201730from dex.debugger.visualstudio.VisualStudio2019 import VisualStudio201931from dex.debugger.visualstudio.VisualStudio2022 import VisualStudio202232 33 34def _get_potential_debuggers(): # noqa35 """Return a dict of the supported debuggers.36 Returns:37 { name (str): debugger (class) }38 """39 return {40 DbgEng.get_option_name(): DbgEng,41 LLDB.get_option_name(): LLDB,42 LLDBDAP.get_option_name(): LLDBDAP,43 VisualStudio2015.get_option_name(): VisualStudio2015,44 VisualStudio2017.get_option_name(): VisualStudio2017,45 VisualStudio2019.get_option_name(): VisualStudio2019,46 VisualStudio2022.get_option_name(): VisualStudio2022,47 }48 49 50def _warn_meaningless_option(context, option):51 if hasattr(context.options, "list_debuggers"):52 return53 54 context.logger.warning(55 f'option "{option}" is meaningless with this debugger',56 enable_prefix=True,57 flag=f"--debugger={context.options.debugger}",58 )59 60 61def add_debugger_tool_base_arguments(parser, defaults):62 defaults.lldb_executable = "lldb.exe" if is_native_windows() else "lldb"63 parser.add_argument(64 "--lldb-executable",65 type=str,66 metavar="<file>",67 default=None,68 display_default=defaults.lldb_executable,69 help="location of `lldb` executable for --debugger=lldb, or `lldb-dap` for --debugger=lldb-dap",70 )71 dap_group = parser.add_argument_group("DAP Debugger arguments")72 dap_group.add_argument(73 "--dap-message-log",74 type=str,75 metavar="<filepath>",76 default=None,77 help="log file for messages between Dexter and the debug adapter; set to '-' to log to stdout, '-e' to log to stderr",78 )79 dap_group.add_argument(80 "--colorize-dap-log",81 action="store_true",82 default=False,83 help="apply colors to the logged DAP messages",84 )85 dap_group.add_argument(86 "--format-dap-log",87 type=str,88 default="pretty",89 choices=["oneline", "pretty"],90 )91 92 93def add_debugger_tool_arguments(parser, context, defaults):94 debuggers = Debuggers(context)95 potential_debuggers = sorted(debuggers.potential_debuggers().keys())96 97 add_debugger_tool_base_arguments(parser, defaults)98 99 parser.add_argument(100 "--debugger",101 type=str,102 choices=potential_debuggers,103 required=True,104 help="debugger to use",105 )106 parser.add_argument(107 "--max-steps",108 metavar="<int>",109 type=int,110 default=1000,111 help="maximum number of program steps allowed",112 )113 parser.add_argument(114 "--pause-between-steps",115 metavar="<seconds>",116 type=float,117 default=0.0,118 help="number of seconds to pause between steps",119 )120 defaults.show_debugger = False121 parser.add_argument(122 "--show-debugger", action="store_true", default=None, help="show the debugger"123 )124 defaults.arch = platform.machine()125 parser.add_argument(126 "--arch",127 type=str,128 metavar="<architecture>",129 default=None,130 display_default=defaults.arch,131 help="target architecture",132 )133 defaults.source_root_dir = ""134 parser.add_argument(135 "--source-root-dir",136 type=str,137 metavar="<directory>",138 default=None,139 help="source root directory",140 )141 parser.add_argument(142 "--debugger-use-relative-paths",143 action="store_true",144 default=False,145 help="pass the debugger paths relative to --source-root-dir",146 )147 parser.add_argument(148 "--target-run-args",149 type=str,150 metavar="<flags>",151 default="",152 help="command line arguments for the test program, in addition to any "153 "provided by DexCommandLine",154 )155 parser.add_argument(156 "--timeout-total",157 metavar="<seconds>",158 type=float,159 default=0.0,160 help="if >0, debugger session will automatically exit after "161 "running for <timeout-total> seconds",162 )163 parser.add_argument(164 "--timeout-breakpoint",165 metavar="<seconds>",166 type=float,167 default=0.0,168 help="if >0, debugger session will automatically exit after "169 "waiting <timeout-breakpoint> seconds without hitting a "170 "breakpoint",171 )172 173 174def handle_debugger_tool_base_options(context, defaults): # noqa175 options = context.options176 177 if options.lldb_executable is None:178 options.lldb_executable = defaults.lldb_executable179 else:180 if getattr(options, "debugger", "lldb") not in ("lldb", "lldb-dap"):181 _warn_meaningless_option(context, "--lldb-executable")182 183 options.lldb_executable = os.path.abspath(options.lldb_executable)184 if not os.path.isfile(options.lldb_executable):185 raise ToolArgumentError(186 '<d>could not find</> <r>"{}"</>'.format(options.lldb_executable)187 )188 189 if (190 options.dap_message_log is not None191 and options.dap_message_log != "-"192 and options.dap_message_log != "-e"193 ):194 options.dap_message_log = os.path.abspath(options.dap_message_log)195 196 197def handle_debugger_tool_options(context, defaults): # noqa198 options = context.options199 200 handle_debugger_tool_base_options(context, defaults)201 202 if options.arch is None:203 options.arch = defaults.arch204 else:205 if options.debugger != "lldb":206 _warn_meaningless_option(context, "--arch")207 208 if options.show_debugger is None:209 options.show_debugger = defaults.show_debugger210 else:211 if options.debugger == "lldb":212 _warn_meaningless_option(context, "--show-debugger")213 214 if options.source_root_dir is not None:215 if not os.path.isabs(options.source_root_dir):216 raise ToolArgumentError(217 f'<d>--source-root-dir: expected absolute path, got</> <r>"{options.source_root_dir}"</>'218 )219 if not os.path.isdir(options.source_root_dir):220 raise ToolArgumentError(221 f'<d>--source-root-dir: could not find directory</> <r>"{options.source_root_dir}"</>'222 )223 224 if options.debugger_use_relative_paths:225 if not options.source_root_dir:226 raise ToolArgumentError(227 f"<d>--debugger-relative-paths</> <r>requires --source-root-dir</>"228 )229 230 231def run_debugger_subprocess(debugger_controller, working_dir_path):232 with NamedTemporaryFile(dir=working_dir_path, delete=False, mode="wb") as fp:233 pickle.dump(debugger_controller, fp, protocol=pickle.HIGHEST_PROTOCOL)234 controller_path = fp.name235 236 dexter_py = os.path.basename(sys.argv[0])237 if not os.path.isfile(dexter_py):238 dexter_py = os.path.join(get_root_directory(), "..", dexter_py)239 assert os.path.isfile(dexter_py)240 241 with NamedTemporaryFile(dir=working_dir_path) as fp:242 args = [243 sys.executable,244 dexter_py,245 "run-debugger-internal-",246 controller_path,247 "--working-directory={}".format(working_dir_path),248 "--unittest=off",249 "--indent-timer-level={}".format(Timer.indent + 2),250 ]251 try:252 with Timer("running external debugger process"):253 subprocess.check_call(args)254 except subprocess.CalledProcessError as e:255 raise DebuggerException(e)256 257 with open(controller_path, "rb") as fp:258 debugger_controller = pickle.load(fp)259 return debugger_controller260 261 262class Debuggers(object):263 @classmethod264 def potential_debuggers(cls):265 try:266 return cls._potential_debuggers267 except AttributeError:268 cls._potential_debuggers = _get_potential_debuggers()269 return cls._potential_debuggers270 271 def __init__(self, context):272 self.context = context273 274 def load(self, key):275 with Timer("load {}".format(key)):276 return Debuggers.potential_debuggers()[key](self.context)277 278 def _populate_debugger_cache(self):279 debuggers = []280 for key in sorted(Debuggers.potential_debuggers()):281 debugger = self.load(key)282 283 class LoadedDebugger(object):284 pass285 286 LoadedDebugger.option_name = key287 LoadedDebugger.full_name = "[{}]".format(debugger.name)288 LoadedDebugger.is_available = debugger.is_available289 290 if LoadedDebugger.is_available:291 try:292 LoadedDebugger.version = debugger.version.splitlines()293 except AttributeError:294 LoadedDebugger.version = [""]295 else:296 try:297 LoadedDebugger.error = debugger.loading_error.splitlines()298 except AttributeError:299 LoadedDebugger.error = [""]300 301 try:302 LoadedDebugger.error_trace = debugger.loading_error_trace303 except AttributeError:304 LoadedDebugger.error_trace = None305 306 debuggers.append(LoadedDebugger)307 return debuggers308 309 def list(self):310 debuggers = self._populate_debugger_cache()311 312 max_o_len = max(len(d.option_name) for d in debuggers)313 max_n_len = max(len(d.full_name) for d in debuggers)314 315 msgs = []316 317 for d in debuggers:318 # Option name, right padded with spaces for alignment319 option_name = "{{name: <{}}}".format(max_o_len).format(name=d.option_name)320 321 # Full name, right padded with spaces for alignment322 full_name = "{{name: <{}}}".format(max_n_len).format(name=d.full_name)323 324 if d.is_available:325 name = "<b>{} {}</>".format(option_name, full_name)326 327 # If the debugger is available, show the first line of the328 # version info.329 available = "<g>YES</>"330 info = "<b>({})</>".format(d.version[0])331 else:332 name = "<y>{} {}</>".format(option_name, full_name)333 334 # If the debugger is not available, show the first line of the335 # error reason.336 available = "<r>NO</> "337 info = "<y>({})</>".format(d.error[0])338 339 msg = "{} {} {}".format(name, available, info)340 341 if self.context.options.verbose:342 # If verbose mode and there was more version or error output343 # than could be displayed in a single line, display the whole344 # lot slightly indented.345 verbose_info = None346 if d.is_available:347 if d.version[1:]:348 verbose_info = d.version + ["\n"]349 else:350 # Some of list elems may contain multiple lines, so make351 # sure each elem is a line of its own.352 verbose_info = d.error_trace353 354 if verbose_info:355 verbose_info = (356 "\n".join(" {}".format(l.rstrip()) for l in verbose_info)357 + "\n"358 )359 msg = "{}\n\n{}".format(msg, verbose_info)360 361 msgs.append(msg)362 self.context.o.auto("\n{}\n\n".format("\n".join(msgs)))363