Skip to content

Commit 9cf9710

Browse files
Tom WeaverTom Weaver
authored andcommitted
[Dexter][NFC] Add Debugger Controller To Dexter
Add DebuggerControllerBase and DefaultController to Dexter implements a new architecture that supports new and novel ways of running a debugger under dexter. Current implementation adds the original default behaviour via the new architecture via the DefaultController, this should have NFC. Reviewers: Orlando Differential Revision: https://reviews.llvm.org/D76926
1 parent ecf313c commit 9cf9710

File tree

15 files changed

+233
-182
lines changed

15 files changed

+233
-182
lines changed

debuginfo-tests/dexter/dex/command/ParseCommand.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import unittest
1414
from copy import copy
1515

16-
from collections import defaultdict
16+
from collections import defaultdict, OrderedDict
1717

1818
from dex.utils.Exceptions import CommandParseError
1919

@@ -26,7 +26,8 @@
2626
from dex.command.commands.DexLabel import DexLabel
2727
from dex.command.commands.DexUnreachable import DexUnreachable
2828
from dex.command.commands.DexWatch import DexWatch
29-
29+
from dex.utils import Timer
30+
from dex.utils.Exceptions import CommandParseError, DebuggerException
3031

3132
def _get_valid_commands():
3233
"""Return all top level DExTer test commands.
@@ -262,9 +263,7 @@ def _find_all_commands_in_file(path, file_lines, valid_commands):
262263
raise format_parse_err(msg, path, file_lines, err_point)
263264
return dict(commands)
264265

265-
266-
267-
def find_all_commands(source_files):
266+
def _find_all_commands(source_files):
268267
commands = defaultdict(dict)
269268
valid_commands = _get_valid_commands()
270269
for source_file in source_files:
@@ -277,6 +276,21 @@ def find_all_commands(source_files):
277276

278277
return dict(commands)
279278

279+
def get_command_infos(source_files):
280+
with Timer('parsing commands'):
281+
try:
282+
commands = _find_all_commands(source_files)
283+
command_infos = OrderedDict()
284+
for command_type in commands:
285+
for command in commands[command_type].values():
286+
if command_type not in command_infos:
287+
command_infos[command_type] = []
288+
command_infos[command_type].append(command)
289+
return OrderedDict(command_infos)
290+
except CommandParseError as e:
291+
msg = 'parser error: <d>{}({}):</> {}\n{}\n{}\n'.format(
292+
e.filename, e.lineno, e.info, e.src, e.caret)
293+
raise DebuggerException(msg)
280294

281295
class TestParseCommand(unittest.TestCase):
282296
class MockCmd(CommandBase):

debuginfo-tests/dexter/dex/command/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
# See https://llvm.org/LICENSE.txt for license information.
66
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
77

8-
from dex.command.ParseCommand import find_all_commands
8+
from dex.command.ParseCommand import get_command_infos
99
from dex.command.StepValueInfo import StepValueInfo

debuginfo-tests/dexter/dex/command/commands/DexExpectStepOrder.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
77

88
from dex.command.CommandBase import CommandBase
9+
from dex.dextIR import LocIR
910
from dex.dextIR import ValueIR
1011

1112
class DexExpectStepOrder(CommandBase):
@@ -28,11 +29,9 @@ def __init__(self, *args):
2829
def get_name():
2930
return __class__.__name__
3031

31-
def eval(self, debugger):
32-
step_info = debugger.get_step_info()
33-
loc = step_info.current_location
34-
return {'DexExpectStepOrder': ValueIR(expression=str(loc.lineno),
35-
value=str(debugger.step_index), type_name=None,
32+
def eval(self, step_info):
33+
return {'DexExpectStepOrder': ValueIR(expression=str(step_info.current_location.lineno),
34+
value=str(step_info.step_index), type_name=None,
3635
error_string=None,
3736
could_evaluate=True,
3837
is_optimized_away=True,

debuginfo-tests/dexter/dex/command/commands/DexUnreachable.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def __init(self):
2626
def get_name():
2727
return __class__.__name__
2828

29-
def eval(self, debugger):
29+
def eval(self, step_info):
3030
# If we're ever called, at all, then we're evaluating a line that has
3131
# been marked as unreachable. Which means a failure.
3232
vir = ValueIR(expression="Unreachable",

debuginfo-tests/dexter/dex/debugger/DebuggerBase.py

Lines changed: 2 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@
77
"""Base class for all debugger interface implementations."""
88

99
import abc
10-
from itertools import chain
11-
import os
1210
import sys
13-
import time
1411
import traceback
1512

1613
from dex.dextIR import DebuggerIR, ValueIR
@@ -20,28 +17,22 @@
2017

2118

2219
class DebuggerBase(object, metaclass=abc.ABCMeta):
23-
def __init__(self, context, step_collection):
20+
def __init__(self, context):
2421
self.context = context
25-
self.steps = step_collection
2622
self._interface = None
2723
self.has_loaded = False
2824
self._loading_error = NotYetLoadedDebuggerException()
29-
self.watches = set()
30-
3125
try:
3226
self._interface = self._load_interface()
3327
self.has_loaded = True
3428
self._loading_error = None
3529
except DebuggerException:
3630
self._loading_error = sys.exc_info()
3731

38-
self.step_index = 0
39-
4032
def __enter__(self):
4133
try:
4234
self._custom_init()
4335
self.clear_breakpoints()
44-
self.add_breakpoints()
4536
except DebuggerException:
4637
self._loading_error = sys.exc_info()
4738
return self
@@ -86,80 +77,13 @@ def loading_error_trace(self):
8677
tb = ''.join(tb).splitlines(True)
8778
return tb
8879

89-
def add_breakpoints(self):
90-
for s in self.context.options.source_files:
91-
with open(s, 'r') as fp:
92-
num_lines = len(fp.readlines())
93-
for line in range(1, num_lines + 1):
94-
self.add_breakpoint(s, line)
95-
96-
def _update_step_watches(self, step_info):
97-
loc = step_info.current_location
98-
watch_cmds = ['DexUnreachable', 'DexExpectStepOrder']
99-
towatch = chain.from_iterable(self.steps.commands[x]
100-
for x in watch_cmds
101-
if x in self.steps.commands)
102-
try:
103-
# Iterate over all watches of the types named in watch_cmds
104-
for watch in towatch:
105-
if (os.path.exists(loc.path)
106-
and os.path.samefile(watch.path, loc.path)
107-
and watch.lineno == loc.lineno):
108-
result = watch.eval(self)
109-
step_info.watches.update(result)
110-
break
111-
except KeyError:
112-
pass
113-
11480
def _sanitize_function_name(self, name): # pylint: disable=no-self-use
11581
"""If the function name returned by the debugger needs any post-
11682
processing to make it fit (for example, if it includes a byte offset),
11783
do that here.
11884
"""
11985
return name
12086

121-
def start(self):
122-
self.steps.clear_steps()
123-
self.launch()
124-
125-
for command_obj in chain.from_iterable(self.steps.commands.values()):
126-
self.watches.update(command_obj.get_watches())
127-
128-
max_steps = self.context.options.max_steps
129-
for _ in range(max_steps):
130-
while self.is_running:
131-
pass
132-
133-
if self.is_finished:
134-
break
135-
136-
self.step_index += 1
137-
step_info = self.get_step_info()
138-
139-
if step_info.current_frame:
140-
self._update_step_watches(step_info)
141-
self.steps.new_step(self.context, step_info)
142-
143-
if self.in_source_file(step_info):
144-
self.step()
145-
else:
146-
self.go()
147-
148-
time.sleep(self.context.options.pause_between_steps)
149-
else:
150-
raise DebuggerException(
151-
'maximum number of steps reached ({})'.format(max_steps))
152-
153-
def in_source_file(self, step_info):
154-
if not step_info.current_frame:
155-
return False
156-
if not step_info.current_location.path:
157-
return False
158-
if not os.path.exists(step_info.current_location.path):
159-
return False
160-
return any(os.path.samefile(step_info.current_location.path, f) \
161-
for f in self.context.options.source_files)
162-
16387
@abc.abstractmethod
16488
def _load_interface(self):
16589
pass
@@ -209,7 +133,7 @@ def go(self) -> ReturnCode:
209133
pass
210134

211135
@abc.abstractmethod
212-
def get_step_info(self):
136+
def get_step_info(self, watches, step_index):
213137
pass
214138

215139
@abc.abstractproperty
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
9+
import abc
10+
11+
class DebuggerControllerBase(object, metaclass=abc.ABCMeta):
12+
@abc.abstractclassmethod
13+
def _run_debugger_custom(self):
14+
"""Specify your own implementation of run_debugger_custom in your own
15+
controller.
16+
"""
17+
pass
18+
19+
def run_debugger(self, debugger):
20+
"""Responsible for correctly launching and tearing down the debugger.
21+
"""
22+
self.debugger = debugger
23+
with self.debugger:
24+
self._run_debugger_custom()
25+
# We may need to pickle this debugger controller after running the
26+
# debugger. Debuggers are not picklable objects, so set to None.
27+
self.debugger = None
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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 controlling debuggers."""
8+
9+
from itertools import chain
10+
import os
11+
import time
12+
13+
from dex.debugger.DebuggerControllers.DebuggerControllerBase import DebuggerControllerBase
14+
from dex.utils.Exceptions import DebuggerException
15+
16+
class DefaultController(DebuggerControllerBase):
17+
def __init__(self, context, step_collection):
18+
self.context = context
19+
self.step_collection = step_collection
20+
self.watches = set()
21+
self.step_index = 0
22+
23+
def _update_step_watches(self, step_info):
24+
watch_cmds = ['DexUnreachable', 'DexExpectStepOrder']
25+
towatch = chain.from_iterable(self.step_collection.commands[x]
26+
for x in watch_cmds
27+
if x in self.step_collection.commands)
28+
try:
29+
# Iterate over all watches of the types named in watch_cmds
30+
for watch in towatch:
31+
loc = step_info.current_location
32+
if (os.path.exists(loc.path)
33+
and os.path.samefile(watch.path, loc.path)
34+
and watch.lineno == loc.lineno):
35+
result = watch.eval(step_info)
36+
step_info.watches.update(result)
37+
break
38+
except KeyError:
39+
pass
40+
41+
def _break_point_all_lines(self):
42+
for s in self.context.options.source_files:
43+
with open(s, 'r') as fp:
44+
num_lines = len(fp.readlines())
45+
for line in range(1, num_lines + 1):
46+
self.debugger.add_breakpoint(s, line)
47+
48+
def _in_source_file(self, step_info):
49+
if not step_info.current_frame:
50+
return False
51+
if not step_info.current_location.path:
52+
return False
53+
if not os.path.exists(step_info.current_location.path):
54+
return False
55+
return any(os.path.samefile(step_info.current_location.path, f) \
56+
for f in self.context.options.source_files)
57+
58+
def _run_debugger_custom(self):
59+
self.step_collection.debugger = self.debugger.debugger_info
60+
self._break_point_all_lines()
61+
62+
self.debugger.launch()
63+
64+
for command_obj in chain.from_iterable(self.step_collection.commands.values()):
65+
self.watches.update(command_obj.get_watches())
66+
67+
max_steps = self.context.options.max_steps
68+
for _ in range(max_steps):
69+
while self.debugger.is_running:
70+
pass
71+
72+
if self.debugger.is_finished:
73+
break
74+
75+
self.step_index += 1
76+
step_info = self.debugger.get_step_info(self.watches, self.step_index)
77+
78+
if step_info.current_frame:
79+
self._update_step_watches(step_info)
80+
self.step_collection.new_step(self.context, step_info)
81+
82+
if self._in_source_file(step_info):
83+
self.debugger.step()
84+
else:
85+
self.debugger.go()
86+
87+
time.sleep(self.context.options.pause_between_steps)
88+
else:
89+
raise DebuggerException(
90+
'maximum number of steps reached ({})'.format(max_steps))

0 commit comments

Comments
 (0)