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/>.
17 from contextlib
import contextmanager
19 # These are deprecated in 3.9, but required in older versions.
20 from typing
import Optional
, Sequence
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.
34 def suppress_new_breakpoint_event():
35 """Return a new context manager that suppresses new breakpoint events."""
46 def _bp_modified(event
):
53 "breakpoint": _breakpoint_descriptor(event
),
59 def _bp_created(event
):
66 "breakpoint": _breakpoint_descriptor(event
),
72 def _bp_deleted(event
):
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.
98 def _breakpoint_descriptor(bp
):
99 "Return the Breakpoint object descriptor given a gdb Breakpoint."
102 "verified": not bp
.pending
,
105 result
["reason"] = "pending"
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]
112 (filename
, line
) = loc
.source
113 if loc
.fullname
is not None:
114 filename
= loc
.fullname
118 "source": make_source(filename
),
124 result
["instructionReference"] = hex(loc
.address
)
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
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
]
148 breakpoint_map
[kind
] = {}
150 with
suppress_new_breakpoint_event():
152 # It makes sense to reuse a breakpoint even if the condition
153 # or ignore count differs, so remove these entries from the
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.
165 if keyspec
in saved_map
:
166 bp
= saved_map
.pop(keyspec
)
170 bp
.condition
= condition
171 if hit_condition
is None:
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
185 log_stack(LogLevel
.FULL
)
186 # Maybe the breakpoint was made but setting an attribute
187 # failed. We still want this to fail.
190 # Breakpoint creation failed.
199 # Delete any breakpoints that were not reused.
200 for entry
in saved_map
.values():
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
)
213 for idx
, item
in enumerate(self
.message
):
215 # Even indices are plain text.
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.
222 # No real need to use the DAP parse_and_eval here.
223 val
= gdb
.parse_and_eval(item
)
225 except Exception as e
:
226 output
+= "<" + str(e
) + ">"
230 "category": "console",
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
242 def _set_one_breakpoint(*, logMessage
=None, **args
):
243 if logMessage
is not None:
244 return _PrintBreakpoint(logMessage
, **args
)
246 return gdb
.Breakpoint(**args
)
249 # Helper function to set ordinary breakpoints according to a list of
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.
260 def _rewrite_src_breakpoint(
262 # This is a Source but we don't type-check it.
265 condition
: Optional
[str] = None,
266 hitCondition
: Optional
[str] = None,
267 logMessage
: Optional
[str] = None,
271 "source": source
["path"],
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
:
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.
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
)
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.
307 def _rewrite_fn_breakpoint(
310 condition
: Optional
[str] = None,
311 hitCondition
: Optional
[str] = None,
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
]
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.
334 def _rewrite_insn_breakpoint(
336 instructionReference
: str,
337 offset
: Optional
[int] = None,
338 condition
: Optional
[str] = None,
339 hitCondition
: Optional
[str] = None,
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
)
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
]
361 "breakpoints": _set_breakpoints("instruction", specs
),
366 def _catch_exception(filterId
, **args
):
367 if filterId
in ("assert", "exception", "throw", "rethrow", "catch"):
368 cmd
= "-catch-" + filterId
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"]:
376 # Not a DAPException because this is definitely unexpected.
377 raise Exception("Could not find catchpoint after creating")
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.
389 def _rewrite_exception_breakpoint(
392 condition
: Optional
[str] = None,
393 # Note that exception breakpoints do not support a hit count.
397 "filterId": filterId
,
398 "condition": condition
,
402 @request("setExceptionBreakpoints")
403 @capability("supportsExceptionFilterOptions")
405 "exceptionBreakpointFilters",
409 "label": "Ada assertions",
410 "supportsCondition": True,
413 "filter": "exception",
414 "label": "Ada exceptions",
415 "supportsCondition": True,
419 "label": "C++ exceptions, when thrown",
420 "supportsCondition": True,
424 "label": "C++ exceptions, when re-thrown",
425 "supportsCondition": True,
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
]
442 "breakpoints": _set_exception_catchpoints(options
),