11364750dSJames Henderson# DExTer : Debugging Experience Tester 21364750dSJames Henderson# ~~~~~~ ~ ~~ ~ ~~ 31364750dSJames Henderson# 41364750dSJames Henderson# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 51364750dSJames Henderson# See https://llvm.org/LICENSE.txt for license information. 61364750dSJames Henderson# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 71364750dSJames Henderson"""Conditional Controller Class for DExTer.-""" 81364750dSJames Henderson 91364750dSJames Henderson 101364750dSJames Hendersonimport os 111364750dSJames Hendersonimport time 121364750dSJames Hendersonfrom collections import defaultdict 131364750dSJames Hendersonfrom itertools import chain 141364750dSJames Henderson 15f98ee40fSTobias Hietafrom dex.debugger.DebuggerControllers.ControllerHelpers import ( 16f98ee40fSTobias Hieta in_source_file, 17f98ee40fSTobias Hieta update_step_watches, 18f98ee40fSTobias Hieta) 19f98ee40fSTobias Hietafrom dex.debugger.DebuggerControllers.DebuggerControllerBase import ( 20f98ee40fSTobias Hieta DebuggerControllerBase, 21f98ee40fSTobias Hieta) 221364750dSJames Hendersonfrom dex.debugger.DebuggerBase import DebuggerBase 231364750dSJames Hendersonfrom dex.utils.Exceptions import DebuggerException 24ee5617dcSStephen Tozerfrom dex.utils.Timeout import Timeout 251364750dSJames Henderson 261364750dSJames Henderson 271364750dSJames Hendersonclass BreakpointRange: 281364750dSJames Henderson """A range of breakpoints and a set of conditions. 291364750dSJames Henderson 301364750dSJames Henderson The leading breakpoint (on line `range_from`) is always active. 311364750dSJames Henderson 321364750dSJames Henderson When the leading breakpoint is hit the trailing range should be activated 331364750dSJames Henderson when `expression` evaluates to any value in `values`. If there are no 341364750dSJames Henderson conditions (`expression` is None) then the trailing breakpoint range should 351364750dSJames Henderson always be activated upon hitting the leading breakpoint. 361364750dSJames Henderson 371364750dSJames Henderson Args: 381364750dSJames Henderson expression: None for no conditions, or a str expression to compare 391364750dSJames Henderson against `values`. 401364750dSJames Henderson 411364750dSJames Henderson hit_count: None for no limit, or int to set the number of times the 421364750dSJames Henderson leading breakpoint is triggered before it is removed. 431364750dSJames Henderson """ 441364750dSJames Henderson 45f98ee40fSTobias Hieta def __init__( 46f98ee40fSTobias Hieta self, 47f98ee40fSTobias Hieta expression: str, 48f98ee40fSTobias Hieta path: str, 49f98ee40fSTobias Hieta range_from: int, 50f98ee40fSTobias Hieta range_to: int, 51f98ee40fSTobias Hieta values: list, 52f98ee40fSTobias Hieta hit_count: int, 53f98ee40fSTobias Hieta finish_on_remove: bool, 54f98ee40fSTobias Hieta ): 551364750dSJames Henderson self.expression = expression 561364750dSJames Henderson self.path = path 571364750dSJames Henderson self.range_from = range_from 581364750dSJames Henderson self.range_to = range_to 591364750dSJames Henderson self.conditional_values = values 601364750dSJames Henderson self.max_hit_count = hit_count 611364750dSJames Henderson self.current_hit_count = 0 626cf69179SStephen Tozer self.finish_on_remove = finish_on_remove 631364750dSJames Henderson 641364750dSJames Henderson def has_conditions(self): 65*ca92bdfaSEisuke Kawashima return self.expression is not None 661364750dSJames Henderson 671364750dSJames Henderson def get_conditional_expression_list(self): 681364750dSJames Henderson conditional_list = [] 691364750dSJames Henderson for value in self.conditional_values: 701364750dSJames Henderson # (<expression>) == (<value>) 71f98ee40fSTobias Hieta conditional_expression = "({}) == ({})".format(self.expression, value) 721364750dSJames Henderson conditional_list.append(conditional_expression) 731364750dSJames Henderson return conditional_list 741364750dSJames Henderson 751364750dSJames Henderson def add_hit(self): 761364750dSJames Henderson self.current_hit_count += 1 771364750dSJames Henderson 781364750dSJames Henderson def should_be_removed(self): 79*ca92bdfaSEisuke Kawashima if self.max_hit_count is None: 801364750dSJames Henderson return False 811364750dSJames Henderson return self.current_hit_count >= self.max_hit_count 821364750dSJames Henderson 831364750dSJames Henderson 841364750dSJames Hendersonclass ConditionalController(DebuggerControllerBase): 851364750dSJames Henderson def __init__(self, context, step_collection): 861364750dSJames Henderson self._bp_ranges = None 871364750dSJames Henderson self._watches = set() 881364750dSJames Henderson self._step_index = 0 891364750dSJames Henderson self._pause_between_steps = context.options.pause_between_steps 901364750dSJames Henderson self._max_steps = context.options.max_steps 911364750dSJames Henderson # Map {id: BreakpointRange} 921364750dSJames Henderson self._leading_bp_handles = {} 933a094d8bSJeremy Morse super(ConditionalController, self).__init__(context, step_collection) 943a094d8bSJeremy Morse self._build_bp_ranges() 951364750dSJames Henderson 961364750dSJames Henderson def _build_bp_ranges(self): 971364750dSJames Henderson commands = self.step_collection.commands 981364750dSJames Henderson self._bp_ranges = [] 991364750dSJames Henderson try: 100f98ee40fSTobias Hieta limit_commands = commands["DexLimitSteps"] 1011364750dSJames Henderson for lc in limit_commands: 1021364750dSJames Henderson bpr = BreakpointRange( 1031364750dSJames Henderson lc.expression, 1041364750dSJames Henderson lc.path, 1051364750dSJames Henderson lc.from_line, 1061364750dSJames Henderson lc.to_line, 1071364750dSJames Henderson lc.values, 1086cf69179SStephen Tozer lc.hit_count, 109f98ee40fSTobias Hieta False, 110f98ee40fSTobias Hieta ) 1111364750dSJames Henderson self._bp_ranges.append(bpr) 1121364750dSJames Henderson except KeyError: 113f98ee40fSTobias Hieta raise DebuggerException( 114f98ee40fSTobias Hieta "Missing DexLimitSteps commands, cannot conditionally step." 115f98ee40fSTobias Hieta ) 116f98ee40fSTobias Hieta if "DexFinishTest" in commands: 117f98ee40fSTobias Hieta finish_commands = commands["DexFinishTest"] 1186cf69179SStephen Tozer for ic in finish_commands: 1196cf69179SStephen Tozer bpr = BreakpointRange( 1206cf69179SStephen Tozer ic.expression, 1216cf69179SStephen Tozer ic.path, 1226cf69179SStephen Tozer ic.on_line, 1236cf69179SStephen Tozer ic.on_line, 1246cf69179SStephen Tozer ic.values, 1256cf69179SStephen Tozer ic.hit_count + 1, 126f98ee40fSTobias Hieta True, 127f98ee40fSTobias Hieta ) 1286cf69179SStephen Tozer self._bp_ranges.append(bpr) 1291364750dSJames Henderson 1301364750dSJames Henderson def _set_leading_bps(self): 1311364750dSJames Henderson # Set a leading breakpoint for each BreakpointRange, building a 1321364750dSJames Henderson # map of {leading bp id: BreakpointRange}. 1331364750dSJames Henderson for bpr in self._bp_ranges: 1341364750dSJames Henderson if bpr.has_conditions(): 1351364750dSJames Henderson # Add a conditional breakpoint for each condition. 1361364750dSJames Henderson for cond_expr in bpr.get_conditional_expression_list(): 137f98ee40fSTobias Hieta id = self.debugger.add_conditional_breakpoint( 138f98ee40fSTobias Hieta bpr.path, bpr.range_from, cond_expr 139f98ee40fSTobias Hieta ) 1401364750dSJames Henderson self._leading_bp_handles[id] = bpr 1411364750dSJames Henderson else: 1421364750dSJames Henderson # Add an unconditional breakpoint. 1431364750dSJames Henderson id = self.debugger.add_breakpoint(bpr.path, bpr.range_from) 1441364750dSJames Henderson self._leading_bp_handles[id] = bpr 1451364750dSJames Henderson 1463a094d8bSJeremy Morse def _run_debugger_custom(self, cmdline): 1471364750dSJames Henderson # TODO: Add conditional and unconditional breakpoint support to dbgeng. 148f98ee40fSTobias Hieta if self.debugger.get_name() == "dbgeng": 149f98ee40fSTobias Hieta raise DebuggerException( 150f98ee40fSTobias Hieta "DexLimitSteps commands are not supported by dbgeng" 151f98ee40fSTobias Hieta ) 1521364750dSJames Henderson 1531364750dSJames Henderson self.step_collection.clear_steps() 1541364750dSJames Henderson self._set_leading_bps() 1551364750dSJames Henderson 1561364750dSJames Henderson for command_obj in chain.from_iterable(self.step_collection.commands.values()): 1571364750dSJames Henderson self._watches.update(command_obj.get_watches()) 1581364750dSJames Henderson 1593a094d8bSJeremy Morse self.debugger.launch(cmdline) 1601364750dSJames Henderson time.sleep(self._pause_between_steps) 1616cf69179SStephen Tozer 1626cf69179SStephen Tozer exit_desired = False 163ee5617dcSStephen Tozer timed_out = False 164ee5617dcSStephen Tozer total_timeout = Timeout(self.context.options.timeout_total) 1656cf69179SStephen Tozer 1661364750dSJames Henderson while not self.debugger.is_finished: 167ee5617dcSStephen Tozer breakpoint_timeout = Timeout(self.context.options.timeout_breakpoint) 168ee5617dcSStephen Tozer while self.debugger.is_running and not timed_out: 169ee5617dcSStephen Tozer # Check to see whether we've timed out while we're waiting. 170ee5617dcSStephen Tozer if total_timeout.timed_out(): 171f98ee40fSTobias Hieta self.context.logger.error( 172f98ee40fSTobias Hieta "Debugger session has been " 173f98ee40fSTobias Hieta f"running for {total_timeout.elapsed}s, timeout reached!" 174f98ee40fSTobias Hieta ) 175ee5617dcSStephen Tozer timed_out = True 176ee5617dcSStephen Tozer if breakpoint_timeout.timed_out(): 177f98ee40fSTobias Hieta self.context.logger.error( 178f98ee40fSTobias Hieta f"Debugger session has not " 179f98ee40fSTobias Hieta f"hit a breakpoint for {breakpoint_timeout.elapsed}s, timeout " 180f98ee40fSTobias Hieta "reached!" 181f98ee40fSTobias Hieta ) 182ee5617dcSStephen Tozer timed_out = True 183ee5617dcSStephen Tozer 184ee5617dcSStephen Tozer if timed_out: 185ee5617dcSStephen Tozer break 1861364750dSJames Henderson 1871364750dSJames Henderson step_info = self.debugger.get_step_info(self._watches, self._step_index) 1881364750dSJames Henderson if step_info.current_frame: 1891364750dSJames Henderson self._step_index += 1 190f98ee40fSTobias Hieta update_step_watches( 191f98ee40fSTobias Hieta step_info, self._watches, self.step_collection.commands 192f98ee40fSTobias Hieta ) 1931364750dSJames Henderson self.step_collection.new_step(self.context, step_info) 1941364750dSJames Henderson 1951364750dSJames Henderson bp_to_delete = [] 1961364750dSJames Henderson for bp_id in self.debugger.get_triggered_breakpoint_ids(): 1971364750dSJames Henderson try: 1981364750dSJames Henderson # See if this is one of our leading breakpoints. 1991364750dSJames Henderson bpr = self._leading_bp_handles[bp_id] 2001364750dSJames Henderson except KeyError: 2011364750dSJames Henderson # This is a trailing bp. Mark it for removal. 2021364750dSJames Henderson bp_to_delete.append(bp_id) 2031364750dSJames Henderson continue 2041364750dSJames Henderson 2051364750dSJames Henderson bpr.add_hit() 2061364750dSJames Henderson if bpr.should_be_removed(): 2076cf69179SStephen Tozer if bpr.finish_on_remove: 2086cf69179SStephen Tozer exit_desired = True 2091364750dSJames Henderson bp_to_delete.append(bp_id) 2101364750dSJames Henderson del self._leading_bp_handles[bp_id] 2111364750dSJames Henderson # Add a range of trailing breakpoints covering the lines 2121364750dSJames Henderson # requested in the DexLimitSteps command. Ignore first line as 2131364750dSJames Henderson # that's covered by the leading bp we just hit and include the 2141364750dSJames Henderson # final line. 2151364750dSJames Henderson for line in range(bpr.range_from + 1, bpr.range_to + 1): 2161364750dSJames Henderson self.debugger.add_breakpoint(bpr.path, line) 2171364750dSJames Henderson 2181364750dSJames Henderson # Remove any trailing or expired leading breakpoints we just hit. 219b3f14802Sgbtozers self.debugger.delete_breakpoints(bp_to_delete) 2201364750dSJames Henderson 2216cf69179SStephen Tozer if exit_desired: 2226cf69179SStephen Tozer break 2231364750dSJames Henderson self.debugger.go() 2241364750dSJames Henderson time.sleep(self._pause_between_steps) 225