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