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"""Default class for controlling debuggers."""
8
9from itertools import chain
10import os
11import time
12
13from dex.debugger.DebuggerControllers.DebuggerControllerBase import DebuggerControllerBase
14from dex.debugger.DebuggerControllers.ControllerHelpers import in_source_file, update_step_watches
15from dex.utils.Exceptions import DebuggerException, LoadDebuggerException
16from dex.utils.Timeout import Timeout
17
18class EarlyExitCondition(object):
19    def __init__(self, on_line, hit_count, expression, values):
20        self.on_line = on_line
21        self.hit_count = hit_count
22        self.expression = expression
23        self.values = values
24
25class DefaultController(DebuggerControllerBase):
26    def __init__(self, context, step_collection):
27        self.source_files = context.options.source_files
28        self.watches = set()
29        self.step_index = 0
30        super(DefaultController, self).__init__(context, step_collection)
31
32    def _break_point_all_lines(self):
33        for s in self.context.options.source_files:
34            with open(s, 'r') as fp:
35                num_lines = len(fp.readlines())
36            for line in range(1, num_lines + 1):
37                try:
38                   self.debugger.add_breakpoint(s, line)
39                except DebuggerException:
40                   raise LoadDebuggerException(DebuggerException.msg)
41
42    def _get_early_exit_conditions(self):
43        commands = self.step_collection.commands
44        early_exit_conditions = []
45        if 'DexFinishTest' in commands:
46            finish_commands = commands['DexFinishTest']
47            for fc in finish_commands:
48                condition = EarlyExitCondition(on_line=fc.on_line,
49                                               hit_count=fc.hit_count,
50                                               expression=fc.expression,
51                                               values=fc.values)
52                early_exit_conditions.append(condition)
53        return early_exit_conditions
54
55    def _should_exit(self, early_exit_conditions, line_no):
56        for condition in early_exit_conditions:
57            if condition.on_line == line_no:
58                exit_condition_hit = condition.expression is None
59                if condition.expression is not None:
60                    # For the purposes of consistent behaviour with the
61                    # Conditional Controller, check equality in the debugger
62                    # rather than in python (as the two can differ).
63                    for value in condition.values:
64                        expr_val = self.debugger.evaluate_expression(f'({condition.expression}) == ({value})')
65                        if expr_val.value == 'true':
66                            exit_condition_hit = True
67                            break
68                if exit_condition_hit:
69                    if condition.hit_count <= 0:
70                        return True
71                    else:
72                        condition.hit_count -= 1
73        return False
74
75
76    def _run_debugger_custom(self, cmdline):
77        self.step_collection.debugger = self.debugger.debugger_info
78        self._break_point_all_lines()
79        self.debugger.launch(cmdline)
80
81        for command_obj in chain.from_iterable(self.step_collection.commands.values()):
82            self.watches.update(command_obj.get_watches())
83        early_exit_conditions = self._get_early_exit_conditions()
84
85        timed_out = False
86        total_timeout = Timeout(self.context.options.timeout_total)
87        max_steps = self.context.options.max_steps
88        for _ in range(max_steps):
89
90            breakpoint_timeout = Timeout(self.context.options.timeout_breakpoint)
91            while self.debugger.is_running and not timed_out:
92                # Check to see whether we've timed out while we're waiting.
93                if total_timeout.timed_out():
94                    self.context.logger.error('Debugger session has been '
95                        f'running for {total_timeout.elapsed}s, timeout reached!')
96                    timed_out = True
97                if breakpoint_timeout.timed_out():
98                    self.context.logger.error(f'Debugger session has not '
99                        f'hit a breakpoint for {breakpoint_timeout.elapsed}s, timeout '
100                        'reached!')
101                    timed_out = True
102
103            if timed_out or self.debugger.is_finished:
104                break
105
106            self.step_index += 1
107            step_info = self.debugger.get_step_info(self.watches, self.step_index)
108
109            if step_info.current_frame:
110                update_step_watches(step_info, self.watches, self.step_collection.commands)
111                self.step_collection.new_step(self.context, step_info)
112                if self._should_exit(early_exit_conditions, step_info.current_frame.loc.lineno):
113                    break
114
115            if in_source_file(self.source_files, step_info):
116                self.debugger.step()
117            else:
118                self.debugger.go()
119
120            time.sleep(self.context.options.pause_between_steps)
121        else:
122            raise DebuggerException(
123                'maximum number of steps reached ({})'.format(max_steps))
124