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