Simplify DAP make_source callers
[binutils-gdb.git] / gdb / python / lib / gdb / dap / breakpoint.py
blobe60265b2f69d83c8e4a9d33f2286a947d2d77884
1 # Copyright 2022-2024 Free Software Foundation, Inc.
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 3 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 import re
17 from contextlib import contextmanager
19 # These are deprecated in 3.9, but required in older versions.
20 from typing import Optional, Sequence
22 import gdb
24 from .server import capability, request, send_event
25 from .sources import make_source
26 from .startup import DAPException, LogLevel, in_gdb_thread, log_stack, parse_and_eval
27 from .typecheck import type_check
29 # True when suppressing new breakpoint events.
30 _suppress_bp = False
33 @contextmanager
34 def suppress_new_breakpoint_event():
35 """Return a new context manager that suppresses new breakpoint events."""
36 global _suppress_bp
37 saved = _suppress_bp
38 _suppress_bp = True
39 try:
40 yield None
41 finally:
42 _suppress_bp = saved
45 @in_gdb_thread
46 def _bp_modified(event):
47 global _suppress_bp
48 if not _suppress_bp:
49 send_event(
50 "breakpoint",
52 "reason": "changed",
53 "breakpoint": _breakpoint_descriptor(event),
58 @in_gdb_thread
59 def _bp_created(event):
60 global _suppress_bp
61 if not _suppress_bp:
62 send_event(
63 "breakpoint",
65 "reason": "new",
66 "breakpoint": _breakpoint_descriptor(event),
71 @in_gdb_thread
72 def _bp_deleted(event):
73 global _suppress_bp
74 if not _suppress_bp:
75 send_event(
76 "breakpoint",
78 "reason": "removed",
79 "breakpoint": _breakpoint_descriptor(event),
84 gdb.events.breakpoint_created.connect(_bp_created)
85 gdb.events.breakpoint_modified.connect(_bp_modified)
86 gdb.events.breakpoint_deleted.connect(_bp_deleted)
89 # Map from the breakpoint "kind" (like "function") to a second map, of
90 # breakpoints of that type. The second map uses the breakpoint spec
91 # as a key, and the gdb.Breakpoint itself as a value. This is used to
92 # implement the clearing behavior specified by the protocol, while
93 # allowing for reuse when a breakpoint can be kept.
94 breakpoint_map = {}
97 @in_gdb_thread
98 def _breakpoint_descriptor(bp):
99 "Return the Breakpoint object descriptor given a gdb Breakpoint."
100 result = {
101 "id": bp.number,
102 "verified": not bp.pending,
104 if bp.pending:
105 result["reason"] = "pending"
106 if bp.locations:
107 # Just choose the first location, because DAP doesn't allow
108 # multiple locations. See
109 # https://github.com/microsoft/debug-adapter-protocol/issues/13
110 loc = bp.locations[0]
111 if loc.source:
112 (filename, line) = loc.source
113 if loc.fullname is not None:
114 filename = loc.fullname
116 result.update(
118 "source": make_source(filename),
119 "line": line,
123 if loc.address:
124 result["instructionReference"] = hex(loc.address)
126 return result
129 # Extract entries from a hash table and return a list of them. Each
130 # entry is a string. If a key of that name appears in the hash table,
131 # it is removed and pushed on the result list; if it does not appear,
132 # None is pushed on the list.
133 def _remove_entries(table, *names):
134 return [table.pop(name, None) for name in names]
137 # Helper function to set some breakpoints according to a list of
138 # specifications and a callback function to do the work of creating
139 # the breakpoint.
140 @in_gdb_thread
141 def _set_breakpoints_callback(kind, specs, creator):
142 global breakpoint_map
143 # Try to reuse existing breakpoints if possible.
144 if kind in breakpoint_map:
145 saved_map = breakpoint_map[kind]
146 else:
147 saved_map = {}
148 breakpoint_map[kind] = {}
149 result = []
150 with suppress_new_breakpoint_event():
151 for spec in specs:
152 # It makes sense to reuse a breakpoint even if the condition
153 # or ignore count differs, so remove these entries from the
154 # spec first.
155 (condition, hit_condition) = _remove_entries(
156 spec, "condition", "hitCondition"
158 keyspec = frozenset(spec.items())
160 # Create or reuse a breakpoint. If asked, set the condition
161 # or the ignore count. Catch errors coming from gdb and
162 # report these as an "unverified" breakpoint.
163 bp = None
164 try:
165 if keyspec in saved_map:
166 bp = saved_map.pop(keyspec)
167 else:
168 bp = creator(**spec)
170 bp.condition = condition
171 if hit_condition is None:
172 bp.ignore_count = 0
173 else:
174 bp.ignore_count = int(
175 parse_and_eval(hit_condition, global_context=True)
178 # Reaching this spot means success.
179 breakpoint_map[kind][keyspec] = bp
180 result.append(_breakpoint_descriptor(bp))
181 # Exceptions other than gdb.error are possible here.
182 except Exception as e:
183 # Don't normally want to see this, as it interferes with
184 # the test suite.
185 log_stack(LogLevel.FULL)
186 # Maybe the breakpoint was made but setting an attribute
187 # failed. We still want this to fail.
188 if bp is not None:
189 bp.delete()
190 # Breakpoint creation failed.
191 result.append(
193 "verified": False,
194 "reason": "failed",
195 "message": str(e),
199 # Delete any breakpoints that were not reused.
200 for entry in saved_map.values():
201 entry.delete()
202 return result
205 class _PrintBreakpoint(gdb.Breakpoint):
206 def __init__(self, logMessage, **args):
207 super().__init__(**args)
208 # Split the message up for easier processing.
209 self.message = re.split("{(.*?)}", logMessage)
211 def stop(self):
212 output = ""
213 for idx, item in enumerate(self.message):
214 if idx % 2 == 0:
215 # Even indices are plain text.
216 output += item
217 else:
218 # Odd indices are expressions to substitute. The {}
219 # have already been stripped by the placement of the
220 # regex capture in the 'split' call.
221 try:
222 # No real need to use the DAP parse_and_eval here.
223 val = gdb.parse_and_eval(item)
224 output += str(val)
225 except Exception as e:
226 output += "<" + str(e) + ">"
227 send_event(
228 "output",
230 "category": "console",
231 "output": output,
234 # Do not stop.
235 return False
238 # Set a single breakpoint or a log point. Returns the new breakpoint.
239 # Note that not every spec will pass logMessage, so here we use a
240 # default.
241 @in_gdb_thread
242 def _set_one_breakpoint(*, logMessage=None, **args):
243 if logMessage is not None:
244 return _PrintBreakpoint(logMessage, **args)
245 else:
246 return gdb.Breakpoint(**args)
249 # Helper function to set ordinary breakpoints according to a list of
250 # specifications.
251 @in_gdb_thread
252 def _set_breakpoints(kind, specs):
253 return _set_breakpoints_callback(kind, specs, _set_one_breakpoint)
256 # A helper function that rewrites a SourceBreakpoint into the internal
257 # form passed to the creator. This function also allows for
258 # type-checking of each SourceBreakpoint.
259 @type_check
260 def _rewrite_src_breakpoint(
262 # This is a Source but we don't type-check it.
263 source,
264 line: int,
265 condition: Optional[str] = None,
266 hitCondition: Optional[str] = None,
267 logMessage: Optional[str] = None,
268 **args,
270 return {
271 "source": source["path"],
272 "line": line,
273 "condition": condition,
274 "hitCondition": hitCondition,
275 "logMessage": logMessage,
279 @request("setBreakpoints")
280 @capability("supportsHitConditionalBreakpoints")
281 @capability("supportsConditionalBreakpoints")
282 @capability("supportsLogPoints")
283 def set_breakpoint(*, source, breakpoints: Sequence = (), **args):
284 if "path" not in source:
285 result = []
286 else:
287 # Setting 'source' in BP avoids any Python error if BP already
288 # has a 'source' parameter. Setting this isn't in the spec,
289 # but it is better to be safe. See PR dap/30820.
290 specs = []
291 for bp in breakpoints:
292 bp["source"] = source
293 specs.append(_rewrite_src_breakpoint(**bp))
294 # Be sure to include the path in the key, so that we only
295 # clear out breakpoints coming from this same source.
296 key = "source:" + source["path"]
297 result = _set_breakpoints(key, specs)
298 return {
299 "breakpoints": result,
303 # A helper function that rewrites a FunctionBreakpoint into the
304 # internal form passed to the creator. This function also allows for
305 # type-checking of each FunctionBreakpoint.
306 @type_check
307 def _rewrite_fn_breakpoint(
309 name: str,
310 condition: Optional[str] = None,
311 hitCondition: Optional[str] = None,
312 **args,
314 return {
315 "function": name,
316 "condition": condition,
317 "hitCondition": hitCondition,
321 @request("setFunctionBreakpoints")
322 @capability("supportsFunctionBreakpoints")
323 def set_fn_breakpoint(*, breakpoints: Sequence, **args):
324 specs = [_rewrite_fn_breakpoint(**bp) for bp in breakpoints]
325 return {
326 "breakpoints": _set_breakpoints("function", specs),
330 # A helper function that rewrites an InstructionBreakpoint into the
331 # internal form passed to the creator. This function also allows for
332 # type-checking of each InstructionBreakpoint.
333 @type_check
334 def _rewrite_insn_breakpoint(
336 instructionReference: str,
337 offset: Optional[int] = None,
338 condition: Optional[str] = None,
339 hitCondition: Optional[str] = None,
340 **args,
342 # There's no way to set an explicit address breakpoint from
343 # Python, so we rely on "spec" instead.
344 val = "*" + instructionReference
345 if offset is not None:
346 val = val + " + " + str(offset)
347 return {
348 "spec": val,
349 "condition": condition,
350 "hitCondition": hitCondition,
354 @request("setInstructionBreakpoints")
355 @capability("supportsInstructionBreakpoints")
356 def set_insn_breakpoints(
357 *, breakpoints: Sequence, offset: Optional[int] = None, **args
359 specs = [_rewrite_insn_breakpoint(**bp) for bp in breakpoints]
360 return {
361 "breakpoints": _set_breakpoints("instruction", specs),
365 @in_gdb_thread
366 def _catch_exception(filterId, **args):
367 if filterId in ("assert", "exception", "throw", "rethrow", "catch"):
368 cmd = "-catch-" + filterId
369 else:
370 raise DAPException("Invalid exception filterID: " + str(filterId))
371 result = gdb.execute_mi(cmd)
372 # A little lame that there's no more direct way.
373 for bp in gdb.breakpoints():
374 if bp.number == result["bkptno"]:
375 return bp
376 # Not a DAPException because this is definitely unexpected.
377 raise Exception("Could not find catchpoint after creating")
380 @in_gdb_thread
381 def _set_exception_catchpoints(filter_options):
382 return _set_breakpoints_callback("exception", filter_options, _catch_exception)
385 # A helper function that rewrites an ExceptionFilterOptions into the
386 # internal form passed to the creator. This function also allows for
387 # type-checking of each ExceptionFilterOptions.
388 @type_check
389 def _rewrite_exception_breakpoint(
391 filterId: str,
392 condition: Optional[str] = None,
393 # Note that exception breakpoints do not support a hit count.
394 **args,
396 return {
397 "filterId": filterId,
398 "condition": condition,
402 @request("setExceptionBreakpoints")
403 @capability("supportsExceptionFilterOptions")
404 @capability(
405 "exceptionBreakpointFilters",
408 "filter": "assert",
409 "label": "Ada assertions",
410 "supportsCondition": True,
413 "filter": "exception",
414 "label": "Ada exceptions",
415 "supportsCondition": True,
418 "filter": "throw",
419 "label": "C++ exceptions, when thrown",
420 "supportsCondition": True,
423 "filter": "rethrow",
424 "label": "C++ exceptions, when re-thrown",
425 "supportsCondition": True,
428 "filter": "catch",
429 "label": "C++ exceptions, when caught",
430 "supportsCondition": True,
434 def set_exception_breakpoints(
435 *, filters: Sequence[str], filterOptions: Sequence = (), **args
437 # Convert the 'filters' to the filter-options style.
438 options = [{"filterId": filter} for filter in filters]
439 options.extend(filterOptions)
440 options = [_rewrite_exception_breakpoint(**bp) for bp in options]
441 return {
442 "breakpoints": _set_exception_catchpoints(options),