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"""Base class for all debugger interface implementations.""" 8 9import abc 10import os 11import sys 12import traceback 13import unittest 14 15from types import SimpleNamespace 16from dex.command.CommandBase import StepExpectInfo 17from dex.dextIR import DebuggerIR, FrameIR, LocIR, StepIR, ValueIR 18from dex.utils.Exceptions import DebuggerException 19from dex.utils.Exceptions import NotYetLoadedDebuggerException 20from dex.utils.ReturnCode import ReturnCode 21 22def watch_is_active(watch_info: StepExpectInfo, path, frame_idx, line_no): 23 _, watch_path, watch_frame_idx, watch_line_range = watch_info 24 # If this watch should only be active for a specific file... 25 if watch_path and os.path.isfile(watch_path): 26 # If the current path does not match the expected file, this watch is 27 # not active. 28 if not (path and os.path.isfile(path) and 29 os.path.samefile(path, watch_path)): 30 return False 31 if watch_frame_idx != frame_idx: 32 return False 33 if watch_line_range and line_no not in list(watch_line_range): 34 return False 35 return True 36 37class DebuggerBase(object, metaclass=abc.ABCMeta): 38 def __init__(self, context): 39 self.context = context 40 # Note: We can't already read values from options 41 # as DebuggerBase is created before we initialize options 42 # to read potential_debuggers. 43 self.options = self.context.options 44 45 self._interface = None 46 self.has_loaded = False 47 self._loading_error = NotYetLoadedDebuggerException() 48 try: 49 self._interface = self._load_interface() 50 self.has_loaded = True 51 self._loading_error = None 52 except DebuggerException: 53 self._loading_error = sys.exc_info() 54 55 def __enter__(self): 56 try: 57 self._custom_init() 58 self.clear_breakpoints() 59 except DebuggerException: 60 self._loading_error = sys.exc_info() 61 return self 62 63 def __exit__(self, *args): 64 self._custom_exit() 65 66 def _custom_init(self): 67 pass 68 69 def _custom_exit(self): 70 pass 71 72 @property 73 def debugger_info(self): 74 return DebuggerIR(name=self.name, version=self.version) 75 76 @property 77 def is_available(self): 78 return self.has_loaded and self.loading_error is None 79 80 @property 81 def loading_error(self): 82 return (str(self._loading_error[1]) 83 if self._loading_error is not None else None) 84 85 @property 86 def loading_error_trace(self): 87 if not self._loading_error: 88 return None 89 90 tb = traceback.format_exception(*self._loading_error) 91 92 if self._loading_error[1].orig_exception is not None: 93 orig_exception = traceback.format_exception( 94 *self._loading_error[1].orig_exception) 95 96 if ''.join(orig_exception) not in ''.join(tb): 97 tb.extend(['\n']) 98 tb.extend(orig_exception) 99 100 tb = ''.join(tb).splitlines(True) 101 return tb 102 103 def _sanitize_function_name(self, name): # pylint: disable=no-self-use 104 """If the function name returned by the debugger needs any post- 105 processing to make it fit (for example, if it includes a byte offset), 106 do that here. 107 """ 108 return name 109 110 @abc.abstractmethod 111 def _load_interface(self): 112 pass 113 114 @classmethod 115 def get_option_name(cls): 116 """Short name that will be used on the command line to specify this 117 debugger. 118 """ 119 raise NotImplementedError() 120 121 @classmethod 122 def get_name(cls): 123 """Full name of this debugger.""" 124 raise NotImplementedError() 125 126 @property 127 def name(self): 128 return self.__class__.get_name() 129 130 @property 131 def option_name(self): 132 return self.__class__.get_option_name() 133 134 @abc.abstractproperty 135 def version(self): 136 pass 137 138 @abc.abstractmethod 139 def clear_breakpoints(self): 140 pass 141 142 def add_breakpoint(self, file_, line): 143 """Returns a unique opaque breakpoint id. 144 145 The ID type depends on the debugger being used, but will probably be 146 an int. 147 """ 148 return self._add_breakpoint(self._external_to_debug_path(file_), line) 149 150 @abc.abstractmethod 151 def _add_breakpoint(self, file_, line): 152 """Returns a unique opaque breakpoint id. 153 """ 154 pass 155 156 def add_conditional_breakpoint(self, file_, line, condition): 157 """Returns a unique opaque breakpoint id. 158 159 The ID type depends on the debugger being used, but will probably be 160 an int. 161 """ 162 return self._add_conditional_breakpoint( 163 self._external_to_debug_path(file_), line, condition) 164 165 @abc.abstractmethod 166 def _add_conditional_breakpoint(self, file_, line, condition): 167 """Returns a unique opaque breakpoint id. 168 """ 169 pass 170 171 @abc.abstractmethod 172 def delete_breakpoint(self, id): 173 """Delete a breakpoint by id. 174 175 Raises a KeyError if no breakpoint with this id exists. 176 """ 177 pass 178 179 @abc.abstractmethod 180 def get_triggered_breakpoint_ids(self): 181 """Returns a set of opaque ids for just-triggered breakpoints. 182 """ 183 pass 184 185 @abc.abstractmethod 186 def launch(self): 187 pass 188 189 @abc.abstractmethod 190 def step(self): 191 pass 192 193 @abc.abstractmethod 194 def go(self) -> ReturnCode: 195 pass 196 197 def get_step_info(self, watches, step_index): 198 step_info = self._get_step_info(watches, step_index) 199 for frame in step_info.frames: 200 frame.loc.path = self._debug_to_external_path(frame.loc.path) 201 return step_info 202 203 @abc.abstractmethod 204 def _get_step_info(self, watches, step_index): 205 pass 206 207 @abc.abstractproperty 208 def is_running(self): 209 pass 210 211 @abc.abstractproperty 212 def is_finished(self): 213 pass 214 215 @abc.abstractproperty 216 def frames_below_main(self): 217 pass 218 219 @abc.abstractmethod 220 def evaluate_expression(self, expression, frame_idx=0) -> ValueIR: 221 pass 222 223 def _external_to_debug_path(self, path): 224 if not self.options.debugger_use_relative_paths: 225 return path 226 root_dir = self.options.source_root_dir 227 if not root_dir or not path: 228 return path 229 assert path.startswith(root_dir) 230 return path[len(root_dir):].lstrip(os.path.sep) 231 232 def _debug_to_external_path(self, path): 233 if not self.options.debugger_use_relative_paths: 234 return path 235 if not path or not self.options.source_root_dir: 236 return path 237 for file in self.options.source_files: 238 if path.endswith(self._external_to_debug_path(file)): 239 return file 240 return path 241 242class TestDebuggerBase(unittest.TestCase): 243 244 class MockDebugger(DebuggerBase): 245 246 def __init__(self, context, *args): 247 super().__init__(context, *args) 248 self.step_info = None 249 self.breakpoint_file = None 250 251 def _add_breakpoint(self, file, line): 252 self.breakpoint_file = file 253 254 def _get_step_info(self, watches, step_index): 255 return self.step_info 256 257 def __init__(self, *args): 258 super().__init__(*args) 259 TestDebuggerBase.MockDebugger.__abstractmethods__ = set() 260 self.options = SimpleNamespace(source_root_dir = '', source_files = []) 261 context = SimpleNamespace(options = self.options) 262 self.dbg = TestDebuggerBase.MockDebugger(context) 263 264 def _new_step(self, paths): 265 frames = [ 266 FrameIR( 267 function=None, 268 is_inlined=False, 269 loc=LocIR(path=path, lineno=0, column=0)) for path in paths 270 ] 271 return StepIR(step_index=0, stop_reason=None, frames=frames) 272 273 def _step_paths(self, step): 274 return [frame.loc.path for frame in step.frames] 275 276 def test_add_breakpoint_no_source_root_dir(self): 277 self.options.debugger_use_relative_paths = True 278 self.options.source_root_dir = '' 279 self.dbg.add_breakpoint('/root/some_file', 12) 280 self.assertEqual('/root/some_file', self.dbg.breakpoint_file) 281 282 def test_add_breakpoint_with_source_root_dir(self): 283 self.options.debugger_use_relative_paths = True 284 self.options.source_root_dir = '/my_root' 285 self.dbg.add_breakpoint('/my_root/some_file', 12) 286 self.assertEqual('some_file', self.dbg.breakpoint_file) 287 288 def test_add_breakpoint_with_source_root_dir_slash_suffix(self): 289 self.options.debugger_use_relative_paths = True 290 self.options.source_root_dir = '/my_root/' 291 self.dbg.add_breakpoint('/my_root/some_file', 12) 292 self.assertEqual('some_file', self.dbg.breakpoint_file) 293 294 def test_get_step_info_no_source_root_dir(self): 295 self.options.debugger_use_relative_paths = True 296 self.dbg.step_info = self._new_step(['/root/some_file']) 297 self.assertEqual(['/root/some_file'], 298 self._step_paths(self.dbg.get_step_info([], 0))) 299 300 def test_get_step_info_no_frames(self): 301 self.options.debugger_use_relative_paths = True 302 self.options.source_root_dir = '/my_root' 303 self.dbg.step_info = self._new_step([]) 304 self.assertEqual([], 305 self._step_paths(self.dbg.get_step_info([], 0))) 306 307 def test_get_step_info(self): 308 self.options.debugger_use_relative_paths = True 309 self.options.source_root_dir = '/my_root' 310 self.options.source_files = ['/my_root/some_file'] 311 self.dbg.step_info = self._new_step( 312 [None, '/other/file', '/dbg/some_file']) 313 self.assertEqual([None, '/other/file', '/my_root/some_file'], 314 self._step_paths(self.dbg.get_step_info([], 0))) 315