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 platform 13import subprocess 14import sys 15from tempfile import NamedTemporaryFile 16 17from dex.command import get_command_infos 18from dex.dextIR import DextIR 19from dex.utils import get_root_directory, Timer 20from dex.utils.Environment import is_native_windows 21from dex.utils.Exceptions import ToolArgumentError 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 31from dex.debugger.visualstudio.VisualStudio2022 import VisualStudio2022 32 33 34def _get_potential_debuggers(): # noqa 35 """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 VisualStudio2015.get_option_name(): VisualStudio2015, 43 VisualStudio2017.get_option_name(): VisualStudio2017, 44 VisualStudio2019.get_option_name(): VisualStudio2019, 45 VisualStudio2022.get_option_name(): VisualStudio2022, 46 } 47 48 49def _warn_meaningless_option(context, option): 50 if hasattr(context.options, "list_debuggers"): 51 return 52 53 context.logger.warning( 54 f'option "{option}" is meaningless with this debugger', 55 enable_prefix=True, 56 flag=f"--debugger={context.options.debugger}", 57 ) 58 59 60def add_debugger_tool_base_arguments(parser, defaults): 61 defaults.lldb_executable = "lldb.exe" if is_native_windows() else "lldb" 62 parser.add_argument( 63 "--lldb-executable", 64 type=str, 65 metavar="<file>", 66 default=None, 67 display_default=defaults.lldb_executable, 68 help="location of LLDB executable", 69 ) 70 71 72def add_debugger_tool_arguments(parser, context, defaults): 73 debuggers = Debuggers(context) 74 potential_debuggers = sorted(debuggers.potential_debuggers().keys()) 75 76 add_debugger_tool_base_arguments(parser, defaults) 77 78 parser.add_argument( 79 "--debugger", 80 type=str, 81 choices=potential_debuggers, 82 required=True, 83 help="debugger to use", 84 ) 85 parser.add_argument( 86 "--max-steps", 87 metavar="<int>", 88 type=int, 89 default=1000, 90 help="maximum number of program steps allowed", 91 ) 92 parser.add_argument( 93 "--pause-between-steps", 94 metavar="<seconds>", 95 type=float, 96 default=0.0, 97 help="number of seconds to pause between steps", 98 ) 99 defaults.show_debugger = False 100 parser.add_argument( 101 "--show-debugger", action="store_true", default=None, help="show the debugger" 102 ) 103 defaults.arch = platform.machine() 104 parser.add_argument( 105 "--arch", 106 type=str, 107 metavar="<architecture>", 108 default=None, 109 display_default=defaults.arch, 110 help="target architecture", 111 ) 112 defaults.source_root_dir = "" 113 parser.add_argument( 114 "--source-root-dir", 115 type=str, 116 metavar="<directory>", 117 default=None, 118 help="source root directory", 119 ) 120 parser.add_argument( 121 "--debugger-use-relative-paths", 122 action="store_true", 123 default=False, 124 help="pass the debugger paths relative to --source-root-dir", 125 ) 126 parser.add_argument( 127 "--target-run-args", 128 type=str, 129 metavar="<flags>", 130 default="", 131 help="command line arguments for the test program, in addition to any " 132 "provided by DexCommandLine", 133 ) 134 parser.add_argument( 135 "--timeout-total", 136 metavar="<seconds>", 137 type=float, 138 default=0.0, 139 help="if >0, debugger session will automatically exit after " 140 "running for <timeout-total> seconds", 141 ) 142 parser.add_argument( 143 "--timeout-breakpoint", 144 metavar="<seconds>", 145 type=float, 146 default=0.0, 147 help="if >0, debugger session will automatically exit after " 148 "waiting <timeout-breakpoint> seconds without hitting a " 149 "breakpoint", 150 ) 151 152 153def handle_debugger_tool_base_options(context, defaults): # noqa 154 options = context.options 155 156 if options.lldb_executable is None: 157 options.lldb_executable = defaults.lldb_executable 158 else: 159 if getattr(options, "debugger", "lldb") != "lldb": 160 _warn_meaningless_option(context, "--lldb-executable") 161 162 options.lldb_executable = os.path.abspath(options.lldb_executable) 163 if not os.path.isfile(options.lldb_executable): 164 raise ToolArgumentError( 165 '<d>could not find</> <r>"{}"</>'.format(options.lldb_executable) 166 ) 167 168 169def handle_debugger_tool_options(context, defaults): # noqa 170 options = context.options 171 172 handle_debugger_tool_base_options(context, defaults) 173 174 if options.arch is None: 175 options.arch = defaults.arch 176 else: 177 if options.debugger != "lldb": 178 _warn_meaningless_option(context, "--arch") 179 180 if options.show_debugger is None: 181 options.show_debugger = defaults.show_debugger 182 else: 183 if options.debugger == "lldb": 184 _warn_meaningless_option(context, "--show-debugger") 185 186 if options.source_root_dir is not None: 187 if not os.path.isabs(options.source_root_dir): 188 raise ToolArgumentError( 189 f'<d>--source-root-dir: expected absolute path, got</> <r>"{options.source_root_dir}"</>' 190 ) 191 if not os.path.isdir(options.source_root_dir): 192 raise ToolArgumentError( 193 f'<d>--source-root-dir: could not find directory</> <r>"{options.source_root_dir}"</>' 194 ) 195 196 if options.debugger_use_relative_paths: 197 if not options.source_root_dir: 198 raise ToolArgumentError( 199 f"<d>--debugger-relative-paths</> <r>requires --source-root-dir</>" 200 ) 201 202 203def run_debugger_subprocess(debugger_controller, working_dir_path): 204 with NamedTemporaryFile(dir=working_dir_path, delete=False, mode="wb") as fp: 205 pickle.dump(debugger_controller, fp, protocol=pickle.HIGHEST_PROTOCOL) 206 controller_path = fp.name 207 208 dexter_py = os.path.basename(sys.argv[0]) 209 if not os.path.isfile(dexter_py): 210 dexter_py = os.path.join(get_root_directory(), "..", dexter_py) 211 assert os.path.isfile(dexter_py) 212 213 with NamedTemporaryFile(dir=working_dir_path) as fp: 214 args = [ 215 sys.executable, 216 dexter_py, 217 "run-debugger-internal-", 218 controller_path, 219 "--working-directory={}".format(working_dir_path), 220 "--unittest=off", 221 "--indent-timer-level={}".format(Timer.indent + 2), 222 ] 223 try: 224 with Timer("running external debugger process"): 225 subprocess.check_call(args) 226 except subprocess.CalledProcessError as e: 227 raise DebuggerException(e) 228 229 with open(controller_path, "rb") as fp: 230 debugger_controller = pickle.load(fp) 231 return debugger_controller 232 233 234class Debuggers(object): 235 @classmethod 236 def potential_debuggers(cls): 237 try: 238 return cls._potential_debuggers 239 except AttributeError: 240 cls._potential_debuggers = _get_potential_debuggers() 241 return cls._potential_debuggers 242 243 def __init__(self, context): 244 self.context = context 245 246 def load(self, key): 247 with Timer("load {}".format(key)): 248 return Debuggers.potential_debuggers()[key](self.context) 249 250 def _populate_debugger_cache(self): 251 debuggers = [] 252 for key in sorted(Debuggers.potential_debuggers()): 253 debugger = self.load(key) 254 255 class LoadedDebugger(object): 256 pass 257 258 LoadedDebugger.option_name = key 259 LoadedDebugger.full_name = "[{}]".format(debugger.name) 260 LoadedDebugger.is_available = debugger.is_available 261 262 if LoadedDebugger.is_available: 263 try: 264 LoadedDebugger.version = debugger.version.splitlines() 265 except AttributeError: 266 LoadedDebugger.version = [""] 267 else: 268 try: 269 LoadedDebugger.error = debugger.loading_error.splitlines() 270 except AttributeError: 271 LoadedDebugger.error = [""] 272 273 try: 274 LoadedDebugger.error_trace = debugger.loading_error_trace 275 except AttributeError: 276 LoadedDebugger.error_trace = None 277 278 debuggers.append(LoadedDebugger) 279 return debuggers 280 281 def list(self): 282 debuggers = self._populate_debugger_cache() 283 284 max_o_len = max(len(d.option_name) for d in debuggers) 285 max_n_len = max(len(d.full_name) for d in debuggers) 286 287 msgs = [] 288 289 for d in debuggers: 290 # Option name, right padded with spaces for alignment 291 option_name = "{{name: <{}}}".format(max_o_len).format(name=d.option_name) 292 293 # Full name, right padded with spaces for alignment 294 full_name = "{{name: <{}}}".format(max_n_len).format(name=d.full_name) 295 296 if d.is_available: 297 name = "<b>{} {}</>".format(option_name, full_name) 298 299 # If the debugger is available, show the first line of the 300 # version info. 301 available = "<g>YES</>" 302 info = "<b>({})</>".format(d.version[0]) 303 else: 304 name = "<y>{} {}</>".format(option_name, full_name) 305 306 # If the debugger is not available, show the first line of the 307 # error reason. 308 available = "<r>NO</> " 309 info = "<y>({})</>".format(d.error[0]) 310 311 msg = "{} {} {}".format(name, available, info) 312 313 if self.context.options.verbose: 314 # If verbose mode and there was more version or error output 315 # than could be displayed in a single line, display the whole 316 # lot slightly indented. 317 verbose_info = None 318 if d.is_available: 319 if d.version[1:]: 320 verbose_info = d.version + ["\n"] 321 else: 322 # Some of list elems may contain multiple lines, so make 323 # sure each elem is a line of its own. 324 verbose_info = d.error_trace 325 326 if verbose_info: 327 verbose_info = ( 328 "\n".join(" {}".format(l.rstrip()) for l in verbose_info) 329 + "\n" 330 ) 331 msg = "{}\n\n{}".format(msg, verbose_info) 332 333 msgs.append(msg) 334 self.context.o.auto("\n{}\n\n".format("\n".join(msgs))) 335