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 type=str, 109 metavar='<directory>', 110 default=None, 111 help='source root directory') 112 parser.add_argument( 113 '--debugger-use-relative-paths', 114 action='store_true', 115 default=False, 116 help='pass the debugger paths relative to --source-root-dir') 117 118def handle_debugger_tool_base_options(context, defaults): # noqa 119 options = context.options 120 121 if options.lldb_executable is None: 122 options.lldb_executable = defaults.lldb_executable 123 else: 124 if getattr(options, 'debugger', 'lldb') != 'lldb': 125 _warn_meaningless_option(context, '--lldb-executable') 126 127 options.lldb_executable = os.path.abspath(options.lldb_executable) 128 if not os.path.isfile(options.lldb_executable): 129 raise ToolArgumentError('<d>could not find</> <r>"{}"</>'.format( 130 options.lldb_executable)) 131 132 133def handle_debugger_tool_options(context, defaults): # noqa 134 options = context.options 135 136 handle_debugger_tool_base_options(context, defaults) 137 138 if options.arch is None: 139 options.arch = defaults.arch 140 else: 141 if options.debugger != 'lldb': 142 _warn_meaningless_option(context, '--arch') 143 144 if options.show_debugger is None: 145 options.show_debugger = defaults.show_debugger 146 else: 147 if options.debugger == 'lldb': 148 _warn_meaningless_option(context, '--show-debugger') 149 150 if options.source_root_dir != None: 151 if not os.path.isabs(options.source_root_dir): 152 raise ToolArgumentError(f'<d>--source-root-dir: expected absolute path, got</> <r>"{options.source_root_dir}"</>') 153 if not os.path.isdir(options.source_root_dir): 154 raise ToolArgumentError(f'<d>--source-root-dir: could not find directory</> <r>"{options.source_root_dir}"</>') 155 156 if options.debugger_use_relative_paths: 157 if not options.source_root_dir: 158 raise ToolArgumentError(f'<d>--debugger-relative-paths</> <r>requires --source-root-dir</>') 159 160def run_debugger_subprocess(debugger_controller, working_dir_path): 161 with NamedTemporaryFile( 162 dir=working_dir_path, delete=False, mode='wb') as fp: 163 pickle.dump(debugger_controller, fp, protocol=pickle.HIGHEST_PROTOCOL) 164 controller_path = fp.name 165 166 dexter_py = os.path.basename(sys.argv[0]) 167 if not os.path.isfile(dexter_py): 168 dexter_py = os.path.join(get_root_directory(), '..', dexter_py) 169 assert os.path.isfile(dexter_py) 170 171 with NamedTemporaryFile(dir=working_dir_path) as fp: 172 args = [ 173 sys.executable, 174 dexter_py, 175 'run-debugger-internal-', 176 controller_path, 177 '--working-directory={}'.format(working_dir_path), 178 '--unittest=off', 179 '--indent-timer-level={}'.format(Timer.indent + 2) 180 ] 181 try: 182 with Timer('running external debugger process'): 183 subprocess.check_call(args) 184 except subprocess.CalledProcessError as e: 185 raise DebuggerException(e) 186 187 with open(controller_path, 'rb') as fp: 188 debugger_controller = pickle.load(fp) 189 190 return debugger_controller 191 192 193class Debuggers(object): 194 @classmethod 195 def potential_debuggers(cls): 196 try: 197 return cls._potential_debuggers 198 except AttributeError: 199 cls._potential_debuggers = _get_potential_debuggers() 200 return cls._potential_debuggers 201 202 def __init__(self, context): 203 self.context = context 204 205 def load(self, key): 206 with Timer('load {}'.format(key)): 207 return Debuggers.potential_debuggers()[key](self.context) 208 209 def _populate_debugger_cache(self): 210 debuggers = [] 211 for key in sorted(Debuggers.potential_debuggers()): 212 debugger = self.load(key) 213 214 class LoadedDebugger(object): 215 pass 216 217 LoadedDebugger.option_name = key 218 LoadedDebugger.full_name = '[{}]'.format(debugger.name) 219 LoadedDebugger.is_available = debugger.is_available 220 221 if LoadedDebugger.is_available: 222 try: 223 LoadedDebugger.version = debugger.version.splitlines() 224 except AttributeError: 225 LoadedDebugger.version = [''] 226 else: 227 try: 228 LoadedDebugger.error = debugger.loading_error.splitlines() 229 except AttributeError: 230 LoadedDebugger.error = [''] 231 232 try: 233 LoadedDebugger.error_trace = debugger.loading_error_trace 234 except AttributeError: 235 LoadedDebugger.error_trace = None 236 237 debuggers.append(LoadedDebugger) 238 return debuggers 239 240 def list(self): 241 debuggers = self._populate_debugger_cache() 242 243 max_o_len = max(len(d.option_name) for d in debuggers) 244 max_n_len = max(len(d.full_name) for d in debuggers) 245 246 msgs = [] 247 248 for d in debuggers: 249 # Option name, right padded with spaces for alignment 250 option_name = ( 251 '{{name: <{}}}'.format(max_o_len).format(name=d.option_name)) 252 253 # Full name, right padded with spaces for alignment 254 full_name = ('{{name: <{}}}'.format(max_n_len) 255 .format(name=d.full_name)) 256 257 if d.is_available: 258 name = '<b>{} {}</>'.format(option_name, full_name) 259 260 # If the debugger is available, show the first line of the 261 # version info. 262 available = '<g>YES</>' 263 info = '<b>({})</>'.format(d.version[0]) 264 else: 265 name = '<y>{} {}</>'.format(option_name, full_name) 266 267 # If the debugger is not available, show the first line of the 268 # error reason. 269 available = '<r>NO</> ' 270 info = '<y>({})</>'.format(d.error[0]) 271 272 msg = '{} {} {}'.format(name, available, info) 273 274 if self.context.options.verbose: 275 # If verbose mode and there was more version or error output 276 # than could be displayed in a single line, display the whole 277 # lot slightly indented. 278 verbose_info = None 279 if d.is_available: 280 if d.version[1:]: 281 verbose_info = d.version + ['\n'] 282 else: 283 # Some of list elems may contain multiple lines, so make 284 # sure each elem is a line of its own. 285 verbose_info = d.error_trace 286 287 if verbose_info: 288 verbose_info = '\n'.join(' {}'.format(l.rstrip()) 289 for l in verbose_info) + '\n' 290 msg = '{}\n\n{}'.format(msg, verbose_info) 291 292 msgs.append(msg) 293 self.context.o.auto('\n{}\n\n'.format('\n'.join(msgs))) 294