aboutsummaryrefslogtreecommitdiff
path: root/gdb/python/lib
diff options
context:
space:
mode:
Diffstat (limited to 'gdb/python/lib')
-rw-r--r--gdb/python/lib/gdb/dap/__init__.py69
-rw-r--r--gdb/python/lib/gdb/dap/breakpoint.py143
-rw-r--r--gdb/python/lib/gdb/dap/bt.py93
-rw-r--r--gdb/python/lib/gdb/dap/disassemble.py51
-rw-r--r--gdb/python/lib/gdb/dap/evaluate.py42
-rw-r--r--gdb/python/lib/gdb/dap/events.py166
-rw-r--r--gdb/python/lib/gdb/dap/frames.py57
-rw-r--r--gdb/python/lib/gdb/dap/io.py67
-rw-r--r--gdb/python/lib/gdb/dap/launch.py39
-rw-r--r--gdb/python/lib/gdb/dap/next.py51
-rw-r--r--gdb/python/lib/gdb/dap/pause.py23
-rw-r--r--gdb/python/lib/gdb/dap/scopes.py65
-rw-r--r--gdb/python/lib/gdb/dap/server.py205
-rw-r--r--gdb/python/lib/gdb/dap/startup.py189
-rw-r--r--gdb/python/lib/gdb/dap/state.py25
-rw-r--r--gdb/python/lib/gdb/dap/threads.py42
16 files changed, 1327 insertions, 0 deletions
diff --git a/gdb/python/lib/gdb/dap/__init__.py b/gdb/python/lib/gdb/dap/__init__.py
new file mode 100644
index 0000000..0df9386
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/__init__.py
@@ -0,0 +1,69 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import gdb
+
+# This must come before other DAP imports.
+from . import startup
+
+# Load modules that define commands.
+from . import breakpoint
+from . import bt
+from . import disassemble
+from . import evaluate
+from . import launch
+from . import next
+from . import pause
+from . import scopes
+from . import threads
+
+from .server import Server
+
+
+def run():
+ """Main entry point for the DAP server.
+ This is called by the GDB DAP interpreter."""
+ startup.exec_and_log("set python print-stack full")
+ startup.exec_and_log("set pagination off")
+
+ # We want to control gdb stdin and stdout entirely, so we dup
+ # them to new file descriptors.
+ saved_out = os.dup(1)
+ saved_in = os.dup(0)
+ # Make sure these are not inheritable. This is already the case
+ # for Unix, but not for Windows.
+ os.set_inheritable(saved_out, False)
+ os.set_inheritable(saved_in, False)
+
+ # The new gdb (and inferior) stdin will just be /dev/null. For
+ # gdb, the "dap" interpreter also rewires the UI so that gdb
+ # doesn't try to read this (and thus see EOF and exit).
+ new_in = os.open(os.devnull, os.O_RDONLY)
+ os.dup2(new_in, 0, True)
+ os.close(new_in)
+
+ # Make the new stdout be a pipe. This way the DAP code can easily
+ # read from the inferior and send OutputEvent to the client.
+ (rfd, wfd) = os.pipe()
+ os.set_inheritable(rfd, False)
+ os.dup2(wfd, 1, True)
+ # Also send stderr this way.
+ os.dup2(wfd, 2, True)
+ os.close(wfd)
+
+ # Note the inferior output is opened in text mode.
+ server = Server(open(saved_in, "rb"), open(saved_out, "wb"), open(rfd, "r"))
+ startup.start_dap(server.main_loop)
diff --git a/gdb/python/lib/gdb/dap/breakpoint.py b/gdb/python/lib/gdb/dap/breakpoint.py
new file mode 100644
index 0000000..502beb0
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/breakpoint.py
@@ -0,0 +1,143 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+import os
+
+from .server import request, capability
+from .startup import send_gdb_with_response, in_gdb_thread
+
+
+# Map from the breakpoint "kind" (like "function") to a second map, of
+# breakpoints of that type. The second map uses the breakpoint spec
+# as a key, and the gdb.Breakpoint itself as a value. This is used to
+# implement the clearing behavior specified by the protocol, while
+# allowing for reuse when a breakpoint can be kept.
+breakpoint_map = {}
+
+
+@in_gdb_thread
+def breakpoint_descriptor(bp):
+ "Return the Breakpoint object descriptor given a gdb Breakpoint."
+ if bp.locations:
+ # Just choose the first location, because DAP doesn't allow
+ # multiple locations. See
+ # https://github.com/microsoft/debug-adapter-protocol/issues/13
+ loc = bp.locations[0]
+ (basename, line) = loc.source
+ return {
+ "id": bp.number,
+ "verified": True,
+ "source": {
+ "name": os.path.basename(basename),
+ "path": loc.fullname,
+ # We probably don't need this but it doesn't hurt to
+ # be explicit.
+ "sourceReference": 0,
+ },
+ "line": line,
+ "instructionReference": hex(loc.address),
+ }
+ else:
+ return {
+ "id": bp.number,
+ "verified": False,
+ }
+
+
+# Helper function to set some breakpoints according to a list of
+# specifications.
+@in_gdb_thread
+def _set_breakpoints(kind, specs):
+ global breakpoint_map
+ # Try to reuse existing breakpoints if possible.
+ if kind in breakpoint_map:
+ saved_map = breakpoint_map[kind]
+ else:
+ saved_map = {}
+ breakpoint_map[kind] = {}
+ result = []
+ for spec in specs:
+ keyspec = frozenset(spec.items())
+ if keyspec in saved_map:
+ bp = saved_map.pop(keyspec)
+ else:
+ # FIXME handle exceptions here
+ bp = gdb.Breakpoint(**spec)
+ breakpoint_map[kind][keyspec] = bp
+ result.append(breakpoint_descriptor(bp))
+ # Delete any breakpoints that were not reused.
+ for entry in saved_map.values():
+ entry.delete()
+ return result
+
+
+@request("setBreakpoints")
+def set_breakpoint(source, *, breakpoints=[], **args):
+ if "path" not in source:
+ result = []
+ else:
+ specs = []
+ for obj in breakpoints:
+ specs.append(
+ {
+ "source": source["path"],
+ "line": obj["line"],
+ }
+ )
+ # Be sure to include the path in the key, so that we only
+ # clear out breakpoints coming from this same source.
+ key = "source:" + source["path"]
+ result = send_gdb_with_response(lambda: _set_breakpoints(key, specs))
+ return {
+ "breakpoints": result,
+ }
+
+
+@request("setFunctionBreakpoints")
+@capability("supportsFunctionBreakpoints")
+def set_fn_breakpoint(breakpoints, **args):
+ specs = []
+ for bp in breakpoints:
+ specs.append(
+ {
+ "function": bp["name"],
+ }
+ )
+ result = send_gdb_with_response(lambda: _set_breakpoints("function", specs))
+ return {
+ "breakpoints": result,
+ }
+
+
+@request("setInstructionBreakpoints")
+@capability("supportsInstructionBreakpoints")
+def set_insn_breakpoints(*, breakpoints, offset=None, **args):
+ specs = []
+ for bp in breakpoints:
+ # There's no way to set an explicit address breakpoint
+ # from Python, so we rely on "spec" instead.
+ val = "*" + bp["instructionReference"]
+ if offset is not None:
+ val = val + " + " + str(offset)
+ specs.append(
+ {
+ "spec": val,
+ }
+ )
+ result = send_gdb_with_response(lambda: _set_breakpoints("instruction", specs))
+ return {
+ "breakpoints": result,
+ }
diff --git a/gdb/python/lib/gdb/dap/bt.py b/gdb/python/lib/gdb/dap/bt.py
new file mode 100644
index 0000000..990ab13
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/bt.py
@@ -0,0 +1,93 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+import os
+
+from .frames import frame_id
+from .server import request, capability
+from .startup import send_gdb_with_response, in_gdb_thread
+from .state import set_thread
+
+
+# Helper function to safely get the name of a frame as a string.
+@in_gdb_thread
+def _frame_name(frame):
+ name = frame.name()
+ if name is None:
+ name = "???"
+ return name
+
+
+# Helper function to get a frame's SAL without an error.
+@in_gdb_thread
+def _safe_sal(frame):
+ try:
+ return frame.find_sal()
+ except gdb.error:
+ return None
+
+
+# Helper function to compute a stack trace.
+@in_gdb_thread
+def _backtrace(thread_id, levels, startFrame):
+ set_thread(thread_id)
+ frames = []
+ current_number = 0
+ # FIXME could invoke frame filters here.
+ try:
+ current_frame = gdb.newest_frame()
+ except gdb.error:
+ current_frame = None
+ # Note that we always iterate over all frames, which is lame, but
+ # seemingly necessary to support the totalFrames response.
+ # FIXME maybe the mildly mysterious note about "monotonically
+ # increasing totalFrames values" would let us fix this.
+ while current_frame is not None:
+ # This condition handles the startFrame==0 case as well.
+ if current_number >= startFrame and (levels == 0 or len(frames) < levels):
+ newframe = {
+ "id": frame_id(current_frame),
+ "name": _frame_name(current_frame),
+ # This must always be supplied, but we will set it
+ # correctly later if that is possible.
+ "line": 0,
+ # GDB doesn't support columns.
+ "column": 0,
+ "instructionPointerReference": hex(current_frame.pc()),
+ }
+ sal = _safe_sal(current_frame)
+ if sal is not None:
+ newframe["source"] = {
+ "name": os.path.basename(sal.symtab.filename),
+ "path": sal.symtab.filename,
+ # We probably don't need this but it doesn't hurt
+ # to be explicit.
+ "sourceReference": 0,
+ }
+ newframe["line"] = sal.line
+ frames.append(newframe)
+ current_number = current_number + 1
+ current_frame = current_frame.older()
+ return {
+ "stackFrames": frames,
+ "totalFrames": current_number,
+ }
+
+
+@request("stackTrace")
+@capability("supportsDelayedStackTraceLoading")
+def stacktrace(*, levels=0, startFrame=0, threadId, **extra):
+ return send_gdb_with_response(lambda: _backtrace(threadId, levels, startFrame))
diff --git a/gdb/python/lib/gdb/dap/disassemble.py b/gdb/python/lib/gdb/dap/disassemble.py
new file mode 100644
index 0000000..3d3b3a5
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/disassemble.py
@@ -0,0 +1,51 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+from .server import request, capability
+from .startup import send_gdb_with_response, in_gdb_thread
+
+
+@in_gdb_thread
+def _disassemble(pc, skip_insns, count):
+ try:
+ arch = gdb.selected_frame().architecture()
+ except gdb.error:
+ # Maybe there was no frame.
+ arch = gdb.selected_inferior().architecture()
+ result = []
+ total_count = skip_insns + count
+ for elt in arch.disassemble(pc, count=total_count)[skip_insns:]:
+ result.append(
+ {
+ "address": hex(elt["addr"]),
+ "instruction": elt["asm"],
+ }
+ )
+ return {
+ "instructions": result,
+ }
+
+
+@request("disassemble")
+@capability("supportsDisassembleRequest")
+def disassemble(
+ *, memoryReference, offset=0, instructionOffset=0, instructionCount, **extra
+):
+ pc = int(memoryReference, 0) + offset
+ return send_gdb_with_response(
+ lambda: _disassemble(pc, instructionOffset, instructionCount)
+ )
diff --git a/gdb/python/lib/gdb/dap/evaluate.py b/gdb/python/lib/gdb/dap/evaluate.py
new file mode 100644
index 0000000..c05e62d
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/evaluate.py
@@ -0,0 +1,42 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+from .frames import frame_for_id
+from .server import request
+from .startup import send_gdb_with_response, in_gdb_thread
+
+
+# Helper function to evaluate an expression in a certain frame.
+@in_gdb_thread
+def _evaluate(expr, frame_id):
+ if frame_id is not None:
+ frame = frame_for_id(frame_id)
+ frame.select()
+ return str(gdb.parse_and_eval(expr))
+
+
+# FIXME 'format' & hex
+# FIXME return a structured response using pretty-printers / varobj
+# FIXME supportsVariableType handling
+@request("evaluate")
+def eval_request(expression, *, frameId=None, **args):
+ result = send_gdb_with_response(lambda: _evaluate(expression, frameId))
+ return {
+ "result": result,
+ # FIXME
+ "variablesReference": -1,
+ }
diff --git a/gdb/python/lib/gdb/dap/events.py b/gdb/python/lib/gdb/dap/events.py
new file mode 100644
index 0000000..45e2a1e
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/events.py
@@ -0,0 +1,166 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import enum
+import gdb
+
+from .server import send_event
+from .startup import in_gdb_thread, Invoker, log
+from .breakpoint import breakpoint_descriptor
+
+
+@in_gdb_thread
+def _on_exit(event):
+ code = 0
+ if hasattr(event, "exit_code"):
+ code = event.exit_code
+ send_event(
+ "exited",
+ {
+ "exitCode": code,
+ },
+ )
+
+
+@in_gdb_thread
+def _bp_modified(event):
+ send_event(
+ "breakpoint",
+ {
+ "reason": "changed",
+ "breakpoint": breakpoint_descriptor(event),
+ },
+ )
+
+
+@in_gdb_thread
+def _bp_created(event):
+ send_event(
+ "breakpoint",
+ {
+ "reason": "new",
+ "breakpoint": breakpoint_descriptor(event),
+ },
+ )
+
+
+@in_gdb_thread
+def _bp_deleted(event):
+ send_event(
+ "breakpoint",
+ {
+ "reason": "removed",
+ "breakpoint": breakpoint_descriptor(event),
+ },
+ )
+
+
+@in_gdb_thread
+def _new_thread(event):
+ send_event(
+ "thread",
+ {
+ "reason": "started",
+ "threadId": event.inferior_thread.global_num,
+ },
+ )
+
+
+_suppress_cont = False
+
+
+@in_gdb_thread
+def _cont(event):
+ global _suppress_cont
+ if _suppress_cont:
+ log("_suppress_cont case")
+ _suppress_cont = False
+ else:
+ send_event(
+ "continued",
+ {
+ "threadId": gdb.selected_thread().global_num,
+ "allThreadsContinued": True,
+ },
+ )
+
+
+class StopKinds(enum.Enum):
+ # The values here are chosen to follow the DAP spec.
+ STEP = "step"
+ BREAKPOINT = "breakpoint"
+ PAUSE = "pause"
+ EXCEPTION = "exception"
+
+
+_expected_stop = None
+
+
+@in_gdb_thread
+def expect_stop(reason):
+ """Indicate that a stop is expected, for the reason given."""
+ global _expected_stop
+ _expected_stop = reason
+
+
+# A wrapper for Invoker that also sets the expected stop.
+class ExecutionInvoker(Invoker):
+ """A subclass of Invoker that sets the expected stop.
+ Note that this assumes that the command will restart the inferior,
+ so it will also cause ContinuedEvents to be suppressed."""
+
+ def __init__(self, cmd, expected):
+ super().__init__(cmd)
+ self.expected = expected
+
+ @in_gdb_thread
+ def __call__(self):
+ expect_stop(self.expected)
+ global _suppress_cont
+ _suppress_cont = True
+ # FIXME if the call fails should we clear _suppress_cont?
+ super().__call__()
+
+
+@in_gdb_thread
+def _on_stop(event):
+ log("entering _on_stop: " + repr(event))
+ global _expected_stop
+ obj = {
+ "threadId": gdb.selected_thread().global_num,
+ # FIXME we don't support non-stop for now.
+ "allThreadsStopped": True,
+ }
+ if isinstance(event, gdb.BreakpointEvent):
+ # Ignore the expected stop, we hit a breakpoint instead.
+ # FIXME differentiate between 'breakpoint', 'function breakpoint',
+ # 'data breakpoint' and 'instruction breakpoint' here.
+ _expected_stop = StopKinds.BREAKPOINT
+ obj["hitBreakpointIds"] = [x.number for x in event.breakpoints]
+ elif _expected_stop is None:
+ # FIXME what is even correct here
+ _expected_stop = StopKinds.EXCEPTION
+ obj["reason"] = _expected_stop.value
+ _expected_stop = None
+ send_event("stopped", obj)
+
+
+gdb.events.stop.connect(_on_stop)
+gdb.events.exited.connect(_on_exit)
+gdb.events.breakpoint_created.connect(_bp_created)
+gdb.events.breakpoint_modified.connect(_bp_modified)
+gdb.events.breakpoint_deleted.connect(_bp_deleted)
+gdb.events.new_thread.connect(_new_thread)
+gdb.events.cont.connect(_cont)
diff --git a/gdb/python/lib/gdb/dap/frames.py b/gdb/python/lib/gdb/dap/frames.py
new file mode 100644
index 0000000..a1c2689
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/frames.py
@@ -0,0 +1,57 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+from .startup import in_gdb_thread
+
+
+# Map from frame (thread,level) pair to frame ID numbers. Note we
+# can't use the frame itself here as it is not hashable.
+_frame_ids = {}
+
+# Map from frame ID number back to frames.
+_id_to_frame = {}
+
+
+# Clear all the frame IDs.
+@in_gdb_thread
+def _clear_frame_ids(evt):
+ global _frame_ids, _id_to_frame
+ _frame_ids = {}
+ _id_to_frame = {}
+
+
+# Clear the frame ID map whenever the inferior runs.
+gdb.events.cont.connect(_clear_frame_ids)
+
+
+@in_gdb_thread
+def frame_id(frame):
+ """Return the frame identifier for FRAME."""
+ global _frame_ids, _id_to_frame
+ pair = (gdb.selected_thread().global_num, frame.level)
+ if pair not in _frame_ids:
+ id = len(_frame_ids)
+ _frame_ids[pair] = id
+ _id_to_frame[id] = frame
+ return _frame_ids[pair]
+
+
+@in_gdb_thread
+def frame_for_id(id):
+ """Given a frame identifier ID, return the corresponding frame."""
+ global _id_to_frame
+ return _id_to_frame[id]
diff --git a/gdb/python/lib/gdb/dap/io.py b/gdb/python/lib/gdb/dap/io.py
new file mode 100644
index 0000000..656ac08
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/io.py
@@ -0,0 +1,67 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+
+from .startup import start_thread, send_gdb
+
+
+def read_json(stream):
+ """Read a JSON-RPC message from STREAM.
+ The decoded object is returned."""
+ # First read and parse the header.
+ content_length = None
+ while True:
+ line = stream.readline()
+ line = line.strip()
+ if line == b"":
+ break
+ if line.startswith(b"Content-Length:"):
+ line = line[15:].strip()
+ content_length = int(line)
+ data = bytes()
+ while len(data) < content_length:
+ new_data = stream.read(content_length - len(data))
+ data += new_data
+ result = json.loads(data)
+ return result
+
+
+def start_json_writer(stream, queue):
+ """Start the JSON writer thread.
+ It will read objects from QUEUE and write them to STREAM,
+ following the JSON-RPC protocol."""
+
+ def _json_writer():
+ seq = 1
+ while True:
+ obj = queue.get()
+ if obj is None:
+ # This is an exit request. The stream is already
+ # flushed, so all that's left to do is request an
+ # exit.
+ send_gdb("quit")
+ break
+ obj["seq"] = seq
+ seq = seq + 1
+ encoded = json.dumps(obj)
+ body_bytes = encoded.encode("utf-8")
+ header = f"Content-Length: {len(body_bytes)}\r\n\r\n"
+ header_bytes = header.encode("ASCII")
+ stream.write(header_bytes)
+ stream.write(body_bytes)
+ stream.flush()
+
+ start_thread("JSON writer", _json_writer)
diff --git a/gdb/python/lib/gdb/dap/launch.py b/gdb/python/lib/gdb/dap/launch.py
new file mode 100644
index 0000000..7ac8177
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/launch.py
@@ -0,0 +1,39 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from .events import ExecutionInvoker
+from .server import request, capability
+from .startup import send_gdb
+
+
+_program = None
+
+
+@request("launch")
+def launch(*, program=None, **args):
+ if program is not None:
+ global _program
+ _program = program
+ send_gdb(f"file {_program}")
+
+
+@capability("supportsConfigurationDoneRequest")
+@request("configurationDone")
+def config_done(**args):
+ global _program
+ if _program is not None:
+ # Suppress the continue event, but don't set any particular
+ # expected stop.
+ send_gdb(ExecutionInvoker("run", None))
diff --git a/gdb/python/lib/gdb/dap/next.py b/gdb/python/lib/gdb/dap/next.py
new file mode 100644
index 0000000..726b659
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/next.py
@@ -0,0 +1,51 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from .events import StopKinds, ExecutionInvoker
+from .server import capability, request
+from .startup import send_gdb
+from .state import set_thread
+
+
+# Helper function to set the current thread.
+def _handle_thread_step(threadId):
+ # Ensure we're going to step the correct thread.
+ send_gdb(lambda: set_thread(threadId))
+
+
+@request("next")
+def next(*, threadId, granularity="statement", **args):
+ _handle_thread_step(threadId)
+ cmd = "next"
+ if granularity == "instruction":
+ cmd += "i"
+ send_gdb(ExecutionInvoker(cmd, StopKinds.STEP))
+
+
+@capability("supportsSteppingGranularity")
+@request("stepIn")
+def stepIn(*, threadId, granularity="statement", **args):
+ _handle_thread_step(threadId)
+ cmd = "step"
+ if granularity == "instruction":
+ cmd += "i"
+ send_gdb(ExecutionInvoker(cmd, StopKinds.STEP))
+
+
+@request("continue")
+def continue_request(**args):
+ send_gdb(ExecutionInvoker("continue", None))
+ # FIXME Just ignore threadId for the time being, and assume all-stop.
+ return {"allThreadsContinued": True}
diff --git a/gdb/python/lib/gdb/dap/pause.py b/gdb/python/lib/gdb/dap/pause.py
new file mode 100644
index 0000000..74fdf48
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/pause.py
@@ -0,0 +1,23 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from .events import StopKinds, ExecutionInvoker
+from .server import request
+from .startup import send_gdb
+
+
+@request("pause")
+def pause(**args):
+ send_gdb(ExecutionInvoker("interrupt -a", StopKinds.PAUSE))
diff --git a/gdb/python/lib/gdb/dap/scopes.py b/gdb/python/lib/gdb/dap/scopes.py
new file mode 100644
index 0000000..0c887db
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/scopes.py
@@ -0,0 +1,65 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+from .frames import frame_for_id
+from .startup import send_gdb_with_response, in_gdb_thread
+from .server import request
+
+
+# Helper function to return a frame's block without error.
+@in_gdb_thread
+def _safe_block(frame):
+ try:
+ return frame.block()
+ except gdb.error:
+ return None
+
+
+# Helper function to return a list of variables of block, up to the
+# enclosing function.
+@in_gdb_thread
+def _block_vars(block):
+ result = []
+ while True:
+ result += list(block)
+ if block.function is not None:
+ break
+ block = block.superblock
+ return result
+
+
+# Helper function to create a DAP scopes for a given frame ID.
+@in_gdb_thread
+def _get_scope(id):
+ frame = frame_for_id(id)
+ block = _safe_block(frame)
+ scopes = []
+ if block is not None:
+ new_scope = {
+ # FIXME
+ "name": "Locals",
+ "expensive": False,
+ "namedVariables": len(_block_vars(block)),
+ }
+ scopes.append(new_scope)
+ return scopes
+
+
+@request("scopes")
+def scopes(*, frameId, **extra):
+ scopes = send_gdb_with_response(lambda: _get_scope(frameId))
+ return {"scopes": scopes}
diff --git a/gdb/python/lib/gdb/dap/server.py b/gdb/python/lib/gdb/dap/server.py
new file mode 100644
index 0000000..d6fc0bd
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/server.py
@@ -0,0 +1,205 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import queue
+
+from .io import start_json_writer, read_json
+from .startup import (
+ in_dap_thread,
+ start_thread,
+ log,
+ log_stack,
+ send_gdb_with_response,
+)
+
+
+# Map capability names to values.
+_capabilities = {}
+
+# Map command names to callables.
+_commands = {}
+
+# The global server.
+_server = None
+
+
+class Server:
+ """The DAP server class."""
+
+ def __init__(self, in_stream, out_stream, child_stream):
+ self.in_stream = in_stream
+ self.out_stream = out_stream
+ self.child_stream = child_stream
+ self.delayed_events = []
+ # This queue accepts JSON objects that are then sent to the
+ # DAP client. Writing is done in a separate thread to avoid
+ # blocking the read loop.
+ self.write_queue = queue.SimpleQueue()
+ self.done = False
+ global _server
+ _server = self
+
+ # Treat PARAMS as a JSON-RPC request and perform its action.
+ # PARAMS is just a dictionary from the JSON.
+ @in_dap_thread
+ def _handle_command(self, params):
+ # We don't handle 'cancel' for now.
+ result = {
+ "request_seq": params["seq"],
+ "type": "response",
+ "command": params["command"],
+ }
+ try:
+ if "arguments" in params:
+ args = params["arguments"]
+ else:
+ args = {}
+ global _commands
+ body = _commands[params["command"]](**args)
+ if body is not None:
+ result["body"] = body
+ result["success"] = True
+ except BaseException as e:
+ log_stack()
+ result["success"] = False
+ result["message"] = str(e)
+ return result
+
+ # Read inferior output and sends OutputEvents to the client. It
+ # is run in its own thread.
+ def _read_inferior_output(self):
+ while True:
+ line = self.child_stream.readline()
+ self.send_event(
+ "output",
+ {
+ "category": "stdout",
+ "output": line,
+ },
+ )
+
+ # Send OBJ to the client, logging first if needed.
+ def _send_json(self, obj):
+ log("WROTE: <<<" + json.dumps(obj) + ">>>")
+ self.write_queue.put(obj)
+
+ # This must be run in the DAP thread, but we can't use
+ # @in_dap_thread here because the global isn't set until after
+ # this starts running. FIXME.
+ def main_loop(self):
+ """The main loop of the DAP server."""
+ # Before looping, start the thread that writes JSON to the
+ # client, and the thread that reads output from the inferior.
+ start_thread("output reader", self._read_inferior_output)
+ start_json_writer(self.out_stream, self.write_queue)
+ while not self.done:
+ cmd = read_json(self.in_stream)
+ log("READ: <<<" + json.dumps(cmd) + ">>>")
+ result = self._handle_command(cmd)
+ self._send_json(result)
+ events = self.delayed_events
+ self.delayed_events = []
+ for (event, body) in events:
+ self.send_event(event, body)
+ # Got the terminate request. This is handled by the
+ # JSON-writing thread, so that we can ensure that all
+ # responses are flushed to the client before exiting.
+ self.write_queue.put(None)
+
+ @in_dap_thread
+ def send_event_later(self, event, body=None):
+ """Send a DAP event back to the client, but only after the
+ current request has completed."""
+ self.delayed_events.append((event, body))
+
+ # Note that this does not need to be run in any particular thread,
+ # because it just creates an object and writes it to a thread-safe
+ # queue.
+ def send_event(self, event, body=None):
+ """Send an event to the DAP client.
+ EVENT is the name of the event, a string.
+ BODY is the body of the event, an arbitrary object."""
+ obj = {
+ "type": "event",
+ "event": event,
+ }
+ if body is not None:
+ obj["body"] = body
+ self._send_json(obj)
+
+ def shutdown(self):
+ """Request that the server shut down."""
+ # Just set a flag. This operation is complicated because we
+ # want to write the result of the request before exiting. See
+ # main_loop.
+ self.done = True
+
+
+def send_event(event, body):
+ """Send an event to the DAP client.
+ EVENT is the name of the event, a string.
+ BODY is the body of the event, an arbitrary object."""
+ global _server
+ _server.send_event(event, body)
+
+
+def request(name):
+ """A decorator that indicates that the wrapper function implements
+ the DAP request NAME."""
+
+ def wrap(func):
+ global _commands
+ _commands[name] = func
+ # All requests must run in the DAP thread.
+ return in_dap_thread(func)
+
+ return wrap
+
+
+def capability(name):
+ """A decorator that indicates that the wrapper function implements
+ the DAP capability NAME."""
+
+ def wrap(func):
+ global _capabilities
+ _capabilities[name] = True
+ return func
+
+ return wrap
+
+
+@request("initialize")
+def initialize(**args):
+ global _server, _capabilities
+ _server.config = args
+ _server.send_event_later("initialized")
+ return _capabilities.copy()
+
+
+@request("terminate")
+@capability("supportsTerminateRequest")
+def terminate(**args):
+ # We can ignore the result here, because we only really need to
+ # synchronize.
+ send_gdb_with_response("kill")
+
+
+@request("disconnect")
+@capability("supportTerminateDebuggee")
+def disconnect(*, terminateDebuggee=False, **args):
+ if terminateDebuggee:
+ terminate()
+ _server.shutdown()
diff --git a/gdb/python/lib/gdb/dap/startup.py b/gdb/python/lib/gdb/dap/startup.py
new file mode 100644
index 0000000..acfdcb4
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/startup.py
@@ -0,0 +1,189 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Do not import other gdbdap modules here -- this module must come
+# first.
+import functools
+import gdb
+import queue
+import signal
+import threading
+import traceback
+from contextlib import contextmanager
+
+
+# The GDB thread, aka the main thread.
+_gdb_thread = threading.current_thread()
+
+
+# The DAP thread.
+_dap_thread = None
+
+
+@contextmanager
+def blocked_signals():
+ """A helper function that blocks and unblocks signals."""
+ if not hasattr(signal, "pthread_sigmask"):
+ yield
+ return
+
+ to_block = {signal.SIGCHLD, signal.SIGINT, signal.SIGALRM, signal.SIGWINCH}
+ signal.pthread_sigmask(signal.SIG_BLOCK, to_block)
+ try:
+ yield None
+ finally:
+ signal.pthread_sigmask(signal.SIG_UNBLOCK, to_block)
+
+
+def start_thread(name, target, args=()):
+ """Start a new thread, invoking TARGET with *ARGS there.
+ This is a helper function that ensures that any GDB signals are
+ correctly blocked."""
+ # GDB requires that these be delivered to the gdb thread. We
+ # do this here to avoid any possible race with the creation of
+ # the new thread. The thread mask is inherited by new
+ # threads.
+ with blocked_signals():
+ result = threading.Thread(target=target, args=args, daemon=True)
+ result.start()
+ return result
+
+
+def start_dap(target):
+ """Start the DAP thread and invoke TARGET there."""
+ global _dap_thread
+ exec_and_log("set breakpoint pending on")
+ _dap_thread = start_thread("DAP", target)
+
+
+def in_gdb_thread(func):
+ """A decorator that asserts that FUNC must be run in the GDB thread."""
+
+ @functools.wraps(func)
+ def ensure_gdb_thread(*args, **kwargs):
+ assert threading.current_thread() is _gdb_thread
+ return func(*args, **kwargs)
+
+ return ensure_gdb_thread
+
+
+def in_dap_thread(func):
+ """A decorator that asserts that FUNC must be run in the DAP thread."""
+
+ @functools.wraps(func)
+ def ensure_dap_thread(*args, **kwargs):
+ assert threading.current_thread() is _dap_thread
+ return func(*args, **kwargs)
+
+ return ensure_dap_thread
+
+
+class LoggingParam(gdb.Parameter):
+ """Whether DAP logging is enabled."""
+
+ set_doc = "Set the DAP logging status."
+ show_doc = "Show the DAP logging status."
+
+ log_file = None
+
+ def __init__(self):
+ super().__init__(
+ "debug dap-log-file", gdb.COMMAND_MAINTENANCE, gdb.PARAM_OPTIONAL_FILENAME
+ )
+ self.value = None
+
+ def get_set_string(self):
+ # Close any existing log file, no matter what.
+ if self.log_file is not None:
+ self.log_file.close()
+ self.log_file = None
+ if self.value is not None:
+ self.log_file = open(self.value, "w")
+ return ""
+
+
+dap_log = LoggingParam()
+
+
+def log(something):
+ """Log SOMETHING to the log file, if logging is enabled."""
+ if dap_log.log_file is not None:
+ print(something, file=dap_log.log_file)
+ dap_log.log_file.flush()
+
+
+def log_stack():
+ """Log a stack trace to the log file, if logging is enabled."""
+ if dap_log.log_file is not None:
+ traceback.print_exc(file=dap_log.log_file)
+
+
+@in_gdb_thread
+def exec_and_log(cmd):
+ """Execute the gdb command CMD.
+ If logging is enabled, log the command and its output."""
+ log("+++ " + cmd)
+ try:
+ output = gdb.execute(cmd, from_tty=True, to_string=True)
+ if output != "":
+ log(">>> " + output)
+ except gdb.error:
+ log_stack()
+
+
+class Invoker(object):
+ """A simple class that can invoke a gdb command."""
+
+ def __init__(self, cmd):
+ self.cmd = cmd
+
+ # This is invoked in the gdb thread to run the command.
+ @in_gdb_thread
+ def __call__(self):
+ exec_and_log(self.cmd)
+
+
+def send_gdb(cmd):
+ """Send CMD to the gdb thread.
+ CMD can be either a function or a string.
+ If it is a string, it is passed to gdb.execute."""
+ if isinstance(cmd, str):
+ cmd = Invoker(cmd)
+ gdb.post_event(cmd)
+
+
+def send_gdb_with_response(fn):
+ """Send FN to the gdb thread and return its result.
+ If FN is a string, it is passed to gdb.execute and None is
+ returned as the result.
+ If FN throws an exception, this function will throw the
+ same exception in the calling thread.
+ """
+ if isinstance(fn, str):
+ fn = Invoker(fn)
+ result_q = queue.SimpleQueue()
+
+ def message():
+ try:
+ val = fn()
+ result_q.put(val)
+ except Exception as e:
+ result_q.put(e)
+
+ send_gdb(message)
+ val = result_q.get()
+ if isinstance(val, Exception):
+ raise val
+ return val
diff --git a/gdb/python/lib/gdb/dap/state.py b/gdb/python/lib/gdb/dap/state.py
new file mode 100644
index 0000000..fb2e543
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/state.py
@@ -0,0 +1,25 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from .startup import in_gdb_thread, exec_and_log, log
+
+
+@in_gdb_thread
+def set_thread(thread_id):
+ """Set the current thread to THREAD_ID."""
+ if thread_id == 0:
+ log("+++ Thread == 0 +++")
+ else:
+ exec_and_log(f"thread {thread_id}")
diff --git a/gdb/python/lib/gdb/dap/threads.py b/gdb/python/lib/gdb/dap/threads.py
new file mode 100644
index 0000000..b6a0ca0
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/threads.py
@@ -0,0 +1,42 @@
+# Copyright 2022 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+from .server import request
+from .startup import send_gdb_with_response, in_gdb_thread
+
+
+# A helper function to construct the list of threads.
+@in_gdb_thread
+def _get_threads():
+ result = []
+ for thr in gdb.selected_inferior().threads():
+ one_result = {
+ "id": thr.global_num,
+ }
+ name = thr.name
+ if name is not None:
+ one_result["name"] = name
+ result.append(one_result)
+ return result
+
+
+@request("threads")
+def threads(**args):
+ result = send_gdb_with_response(_get_threads)
+ return {
+ "threads": result,
+ }