]> git.ipfire.org Git - thirdparty/binutils-gdb.git/blob - gdb/python/lib/gdb/dap/breakpoint.py
Update copyright year range in header of all files managed by GDB
[thirdparty/binutils-gdb.git] / gdb / python / lib / gdb / dap / breakpoint.py
1 # Copyright 2022-2024 Free Software Foundation, Inc.
2
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.
7 #
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.
12 #
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/>.
15
16 import gdb
17 import os
18 import re
19
20 from contextlib import contextmanager
21
22 # These are deprecated in 3.9, but required in older versions.
23 from typing import Optional, Sequence
24
25 from .server import request, capability, send_event
26 from .sources import make_source
27 from .startup import in_gdb_thread, log_stack, parse_and_eval, LogLevel, DAPException
28 from .typecheck import type_check
29
30
31 @in_gdb_thread
32 def _bp_modified(event):
33 send_event(
34 "breakpoint",
35 {
36 "reason": "changed",
37 "breakpoint": _breakpoint_descriptor(event),
38 },
39 )
40
41
42 # True when suppressing new breakpoint events.
43 _suppress_bp = False
44
45
46 @contextmanager
47 def suppress_new_breakpoint_event():
48 """Return a new context manager that suppresses new breakpoint events."""
49 global _suppress_bp
50 _suppress_bp = True
51 try:
52 yield None
53 finally:
54 _suppress_bp = False
55
56
57 @in_gdb_thread
58 def _bp_created(event):
59 global _suppress_bp
60 if not _suppress_bp:
61 send_event(
62 "breakpoint",
63 {
64 "reason": "new",
65 "breakpoint": _breakpoint_descriptor(event),
66 },
67 )
68
69
70 @in_gdb_thread
71 def _bp_deleted(event):
72 send_event(
73 "breakpoint",
74 {
75 "reason": "removed",
76 "breakpoint": _breakpoint_descriptor(event),
77 },
78 )
79
80
81 gdb.events.breakpoint_created.connect(_bp_created)
82 gdb.events.breakpoint_modified.connect(_bp_modified)
83 gdb.events.breakpoint_deleted.connect(_bp_deleted)
84
85
86 # Map from the breakpoint "kind" (like "function") to a second map, of
87 # breakpoints of that type. The second map uses the breakpoint spec
88 # as a key, and the gdb.Breakpoint itself as a value. This is used to
89 # implement the clearing behavior specified by the protocol, while
90 # allowing for reuse when a breakpoint can be kept.
91 breakpoint_map = {}
92
93
94 @in_gdb_thread
95 def _breakpoint_descriptor(bp):
96 "Return the Breakpoint object descriptor given a gdb Breakpoint."
97 result = {
98 "id": bp.number,
99 # We always use True here, because this field just indicates
100 # that breakpoint creation was successful -- and if we have a
101 # breakpoint, the creation succeeded.
102 "verified": True,
103 }
104 if bp.locations:
105 # Just choose the first location, because DAP doesn't allow
106 # multiple locations. See
107 # https://github.com/microsoft/debug-adapter-protocol/issues/13
108 loc = bp.locations[0]
109 if loc.source:
110 (filename, line) = loc.source
111 if loc.fullname is not None:
112 filename = loc.fullname
113
114 result.update(
115 {
116 "source": make_source(filename, os.path.basename(filename)),
117 "line": line,
118 }
119 )
120
121 if loc.address:
122 result["instructionReference"] = hex(loc.address)
123
124 return result
125
126
127 # Extract entries from a hash table and return a list of them. Each
128 # entry is a string. If a key of that name appears in the hash table,
129 # it is removed and pushed on the result list; if it does not appear,
130 # None is pushed on the list.
131 def _remove_entries(table, *names):
132 return [table.pop(name, None) for name in names]
133
134
135 # Helper function to set some breakpoints according to a list of
136 # specifications and a callback function to do the work of creating
137 # the breakpoint.
138 @in_gdb_thread
139 def _set_breakpoints_callback(kind, specs, creator):
140 global breakpoint_map
141 # Try to reuse existing breakpoints if possible.
142 if kind in breakpoint_map:
143 saved_map = breakpoint_map[kind]
144 else:
145 saved_map = {}
146 breakpoint_map[kind] = {}
147 result = []
148 for spec in specs:
149 # It makes sense to reuse a breakpoint even if the condition
150 # or ignore count differs, so remove these entries from the
151 # spec first.
152 (condition, hit_condition) = _remove_entries(spec, "condition", "hitCondition")
153 keyspec = frozenset(spec.items())
154
155 # Create or reuse a breakpoint. If asked, set the condition
156 # or the ignore count. Catch errors coming from gdb and
157 # report these as an "unverified" breakpoint.
158 bp = None
159 try:
160 if keyspec in saved_map:
161 bp = saved_map.pop(keyspec)
162 else:
163 with suppress_new_breakpoint_event():
164 bp = creator(**spec)
165
166 bp.condition = condition
167 if hit_condition is None:
168 bp.ignore_count = 0
169 else:
170 bp.ignore_count = int(
171 parse_and_eval(hit_condition, global_context=True)
172 )
173
174 # Reaching this spot means success.
175 breakpoint_map[kind][keyspec] = bp
176 result.append(_breakpoint_descriptor(bp))
177 # Exceptions other than gdb.error are possible here.
178 except Exception as e:
179 # Don't normally want to see this, as it interferes with
180 # the test suite.
181 log_stack(LogLevel.FULL)
182 # Maybe the breakpoint was made but setting an attribute
183 # failed. We still want this to fail.
184 if bp is not None:
185 bp.delete()
186 # Breakpoint creation failed.
187 result.append(
188 {
189 "verified": False,
190 "message": str(e),
191 }
192 )
193
194 # Delete any breakpoints that were not reused.
195 for entry in saved_map.values():
196 entry.delete()
197 return result
198
199
200 class _PrintBreakpoint(gdb.Breakpoint):
201 def __init__(self, logMessage, **args):
202 super().__init__(**args)
203 # Split the message up for easier processing.
204 self.message = re.split("{(.*?)}", logMessage)
205
206 def stop(self):
207 output = ""
208 for idx, item in enumerate(self.message):
209 if idx % 2 == 0:
210 # Even indices are plain text.
211 output += item
212 else:
213 # Odd indices are expressions to substitute. The {}
214 # have already been stripped by the placement of the
215 # regex capture in the 'split' call.
216 try:
217 # No real need to use the DAP parse_and_eval here.
218 val = gdb.parse_and_eval(item)
219 output += str(val)
220 except Exception as e:
221 output += "<" + str(e) + ">"
222 send_event(
223 "output",
224 {
225 "category": "console",
226 "output": output,
227 },
228 )
229 # Do not stop.
230 return False
231
232
233 # Set a single breakpoint or a log point. Returns the new breakpoint.
234 # Note that not every spec will pass logMessage, so here we use a
235 # default.
236 @in_gdb_thread
237 def _set_one_breakpoint(*, logMessage=None, **args):
238 if logMessage is not None:
239 return _PrintBreakpoint(logMessage, **args)
240 else:
241 return gdb.Breakpoint(**args)
242
243
244 # Helper function to set ordinary breakpoints according to a list of
245 # specifications.
246 @in_gdb_thread
247 def _set_breakpoints(kind, specs):
248 return _set_breakpoints_callback(kind, specs, _set_one_breakpoint)
249
250
251 # A helper function that rewrites a SourceBreakpoint into the internal
252 # form passed to the creator. This function also allows for
253 # type-checking of each SourceBreakpoint.
254 @type_check
255 def _rewrite_src_breakpoint(
256 *,
257 # This is a Source but we don't type-check it.
258 source,
259 line: int,
260 condition: Optional[str] = None,
261 hitCondition: Optional[str] = None,
262 logMessage: Optional[str] = None,
263 **args,
264 ):
265 return {
266 "source": source["path"],
267 "line": line,
268 "condition": condition,
269 "hitCondition": hitCondition,
270 "logMessage": logMessage,
271 }
272
273
274 # FIXME we do not specify a type for 'source'.
275 @request("setBreakpoints")
276 @capability("supportsHitConditionalBreakpoints")
277 @capability("supportsConditionalBreakpoints")
278 @capability("supportsLogPoints")
279 def set_breakpoint(*, source, breakpoints: Sequence = (), **args):
280 if "path" not in source:
281 result = []
282 else:
283 # Setting 'source' in BP avoids any Python error if BP already
284 # has a 'source' parameter. Setting this isn't in the spec,
285 # but it is better to be safe. See PR dap/30820.
286 specs = []
287 for bp in breakpoints:
288 bp["source"] = source
289 specs.append(_rewrite_src_breakpoint(**bp))
290 # Be sure to include the path in the key, so that we only
291 # clear out breakpoints coming from this same source.
292 key = "source:" + source["path"]
293 result = _set_breakpoints(key, specs)
294 return {
295 "breakpoints": result,
296 }
297
298
299 # A helper function that rewrites a FunctionBreakpoint into the
300 # internal form passed to the creator. This function also allows for
301 # type-checking of each FunctionBreakpoint.
302 @type_check
303 def _rewrite_fn_breakpoint(
304 *,
305 name: str,
306 condition: Optional[str] = None,
307 hitCondition: Optional[str] = None,
308 **args,
309 ):
310 return {
311 "function": name,
312 "condition": condition,
313 "hitCondition": hitCondition,
314 }
315
316
317 @request("setFunctionBreakpoints")
318 @capability("supportsFunctionBreakpoints")
319 def set_fn_breakpoint(*, breakpoints: Sequence, **args):
320 specs = [_rewrite_fn_breakpoint(**bp) for bp in breakpoints]
321 return {
322 "breakpoints": _set_breakpoints("function", specs),
323 }
324
325
326 # A helper function that rewrites an InstructionBreakpoint into the
327 # internal form passed to the creator. This function also allows for
328 # type-checking of each InstructionBreakpoint.
329 @type_check
330 def _rewrite_insn_breakpoint(
331 *,
332 instructionReference: str,
333 offset: Optional[int] = None,
334 condition: Optional[str] = None,
335 hitCondition: Optional[str] = None,
336 **args,
337 ):
338 # There's no way to set an explicit address breakpoint from
339 # Python, so we rely on "spec" instead.
340 val = "*" + instructionReference
341 if offset is not None:
342 val = val + " + " + str(offset)
343 return {
344 "spec": val,
345 "condition": condition,
346 "hitCondition": hitCondition,
347 }
348
349
350 @request("setInstructionBreakpoints")
351 @capability("supportsInstructionBreakpoints")
352 def set_insn_breakpoints(
353 *, breakpoints: Sequence, offset: Optional[int] = None, **args
354 ):
355 specs = [_rewrite_insn_breakpoint(**bp) for bp in breakpoints]
356 return {
357 "breakpoints": _set_breakpoints("instruction", specs),
358 }
359
360
361 @in_gdb_thread
362 def _catch_exception(filterId, **args):
363 if filterId in ("assert", "exception", "throw", "rethrow", "catch"):
364 cmd = "-catch-" + filterId
365 else:
366 raise DAPException("Invalid exception filterID: " + str(filterId))
367 result = gdb.execute_mi(cmd)
368 # A little lame that there's no more direct way.
369 for bp in gdb.breakpoints():
370 if bp.number == result["bkptno"]:
371 return bp
372 # Not a DAPException because this is definitely unexpected.
373 raise Exception("Could not find catchpoint after creating")
374
375
376 @in_gdb_thread
377 def _set_exception_catchpoints(filter_options):
378 return _set_breakpoints_callback("exception", filter_options, _catch_exception)
379
380
381 # A helper function that rewrites an ExceptionFilterOptions into the
382 # internal form passed to the creator. This function also allows for
383 # type-checking of each ExceptionFilterOptions.
384 @type_check
385 def _rewrite_exception_breakpoint(
386 *,
387 filterId: str,
388 condition: Optional[str] = None,
389 # Note that exception breakpoints do not support a hit count.
390 **args,
391 ):
392 return {
393 "filterId": filterId,
394 "condition": condition,
395 }
396
397
398 @request("setExceptionBreakpoints")
399 @capability("supportsExceptionFilterOptions")
400 @capability(
401 "exceptionBreakpointFilters",
402 (
403 {
404 "filter": "assert",
405 "label": "Ada assertions",
406 "supportsCondition": True,
407 },
408 {
409 "filter": "exception",
410 "label": "Ada exceptions",
411 "supportsCondition": True,
412 },
413 {
414 "filter": "throw",
415 "label": "C++ exceptions, when thrown",
416 "supportsCondition": True,
417 },
418 {
419 "filter": "rethrow",
420 "label": "C++ exceptions, when re-thrown",
421 "supportsCondition": True,
422 },
423 {
424 "filter": "catch",
425 "label": "C++ exceptions, when caught",
426 "supportsCondition": True,
427 },
428 ),
429 )
430 def set_exception_breakpoints(
431 *, filters: Sequence[str], filterOptions: Sequence = (), **args
432 ):
433 # Convert the 'filters' to the filter-options style.
434 options = [{"filterId": filter} for filter in filters]
435 options.extend(filterOptions)
436 options = [_rewrite_exception_breakpoint(**bp) for bp in options]
437 return {
438 "breakpoints": _set_exception_catchpoints(options),
439 }