diff --git a/gdb/Makefile.in b/gdb/Makefile.in index c5d66e480d1..b22a6c624a6 100644 --- a/gdb/Makefile.in +++ b/gdb/Makefile.in @@ -396,6 +396,7 @@ SUBDIR_PYTHON_SRCS = \ python/py-cmd.c \ python/py-connection.c \ python/py-continueevent.c \ + python/py-dap.c \ python/py-disasm.c \ python/py-event.c \ python/py-evtregistry.c \ diff --git a/gdb/NEWS b/gdb/NEWS index e61f06081de..41d815567ce 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -5,6 +5,10 @@ * MI version 1 has been removed. +* GDB has initial built-in support for the Debugger Adapter Protocol. + This support requires that GDB be built with Python scripting + enabled. + *** Changes in GDB 13 * MI version 1 is deprecated, and will be removed in GDB 14. diff --git a/gdb/data-directory/Makefile.in b/gdb/data-directory/Makefile.in index 557a63b40d9..f1139291eed 100644 --- a/gdb/data-directory/Makefile.in +++ b/gdb/data-directory/Makefile.in @@ -87,6 +87,22 @@ PYTHON_FILE_LIST = \ gdb/command/type_printers.py \ gdb/command/unwinders.py \ gdb/command/xmethods.py \ + gdb/dap/breakpoint.py \ + gdb/dap/bt.py \ + gdb/dap/disassemble.py \ + gdb/dap/evaluate.py \ + gdb/dap/events.py \ + gdb/dap/frames.py \ + gdb/dap/__init__.py \ + gdb/dap/io.py \ + gdb/dap/launch.py \ + gdb/dap/next.py \ + gdb/dap/pause.py \ + gdb/dap/scopes.py \ + gdb/dap/server.py \ + gdb/dap/startup.py \ + gdb/dap/state.py \ + gdb/dap/threads.py \ gdb/function/__init__.py \ gdb/function/as_string.py \ gdb/function/caller_is.py \ diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index a72b2b9eb26..ea54f25b08e 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -29136,6 +29136,15 @@ The traditional console or command-line interpreter. This is the most often used interpreter with @value{GDBN}. With no interpreter specified at runtime, @value{GDBN} will use this interpreter. +@item dap +@cindex DAP +@cindex Debugger Adapter Protocol +When @value{GDBN} has been built with Python support, it also supports +the Debugger Adapter Protocol. This protocol can be used by a +debugger GUI or an IDE to communicate with @value{GDBN}. This +protocol is documented at +@url{https://microsoft.github.io/debug-adapter-protocol/}. + @item mi @cindex mi interpreter The newest @sc{gdb/mi} interface (currently @code{mi3}). Used primarily diff --git a/gdb/python/lib/gdb/dap/__init__.py b/gdb/python/lib/gdb/dap/__init__.py new file mode 100644 index 00000000000..0df938623a9 --- /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 . + +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 00000000000..502beb0478e --- /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 . + +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 00000000000..990ab135b05 --- /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 . + +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 00000000000..3d3b3a5695a --- /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 . + +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 00000000000..c05e62d17a3 --- /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 . + +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 00000000000..45e2a1e116d --- /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 . + +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 00000000000..a1c2689c350 --- /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 . + +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 00000000000..656ac08b4ec --- /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 . + +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 00000000000..7ac81779111 --- /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 . + +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 00000000000..726b659db7f --- /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 . + +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 00000000000..74fdf48cdfa --- /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 . + +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 00000000000..0c887db91ad --- /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 . + +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 00000000000..d6fc0bd5754 --- /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 . + +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 00000000000..acfdcb4d81b --- /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 . + +# 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 00000000000..fb2e543199a --- /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 . + +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 00000000000..b6a0ca0a900 --- /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 . + +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, + } diff --git a/gdb/python/py-dap.c b/gdb/python/py-dap.c new file mode 100644 index 00000000000..8e977bc55bb --- /dev/null +++ b/gdb/python/py-dap.c @@ -0,0 +1,99 @@ +/* Python DAP interpreter + + Copyright (C) 2022 Free Software Foundation, Inc. + + This file is part of GDB. + + 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 . */ + +#include "defs.h" +#include "python-internal.h" +#include "interps.h" +#include "cli-out.h" +#include "top.h" + +class dap_interp final : public interp +{ +public: + + explicit dap_interp (const char *name) + : interp (name), + m_ui_out (new cli_ui_out (gdb_stdout)) + { + } + + ~dap_interp () override = default; + + void init (bool top_level) override; + + void suspend () override + { + } + + void resume () override + { + } + + gdb_exception exec (const char *command) override + { + /* Just ignore it. */ + return {}; + } + + void set_logging (ui_file_up logfile, bool logging_redirect, + bool debug_redirect) override + { + /* Just ignore it. */ + } + + ui_out *interp_ui_out () override + { + return m_ui_out.get (); + } + +private: + + std::unique_ptr m_ui_out; +}; + +void +dap_interp::init (bool top_level) +{ + gdbpy_enter enter_py; + + gdbpy_ref<> dap_module (PyImport_ImportModule ("gdb.dap")); + if (dap_module == nullptr) + gdbpy_handle_exception (); + + gdbpy_ref<> func (PyObject_GetAttrString (dap_module.get (), "run")); + if (func == nullptr) + gdbpy_handle_exception (); + + gdbpy_ref<> result_obj (PyObject_CallNoArgs (func.get ())); + if (result_obj == nullptr) + gdbpy_handle_exception (); + + current_ui->input_fd = -1; + current_ui->m_input_interactive_p = false; +} + +void _initialize_py_interp (); +void +_initialize_py_interp () +{ + interp_factory_register ("dap", [] (const char *name) -> interp * + { + return new dap_interp (name); + }); +} diff --git a/gdb/testsuite/gdb.dap/basic-dap.c b/gdb/testsuite/gdb.dap/basic-dap.c new file mode 100644 index 00000000000..eab1c999559 --- /dev/null +++ b/gdb/testsuite/gdb.dap/basic-dap.c @@ -0,0 +1,44 @@ +/* Copyright 2022 Free Software Foundation, Inc. + + This file is part of GDB. + + 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 . */ + +int global_variable = 23; + +void +function_breakpoint_here () +{ + ++global_variable; + ++global_variable; +} + +void +do_not_stop_here () +{ + /* This exists to test that breakpoints are cleared. */ +} + +void +address_breakpoint_here () +{ +} + +int main () +{ + do_not_stop_here (); + function_breakpoint_here (); + address_breakpoint_here (); + return 0; /* BREAK */ +} diff --git a/gdb/testsuite/gdb.dap/basic-dap.exp b/gdb/testsuite/gdb.dap/basic-dap.exp new file mode 100644 index 00000000000..d3acf0c22c8 --- /dev/null +++ b/gdb/testsuite/gdb.dap/basic-dap.exp @@ -0,0 +1,151 @@ +# 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 . + +# Basic DAP test. + +load_lib dap-support.exp + +standard_testfile + +if {[build_executable ${testfile}.exp $testfile] == -1} { + return +} + +if {[dap_launch $testfile] == ""} { + return +} + +set obj [dap_check_request_and_response "set breakpoint on two functions" \ + setFunctionBreakpoints \ + {o breakpoints [a [o name [s function_breakpoint_here]] \ + [o name [s do_not_stop_here]]]}] +set fn_bpno [dap_get_breakpoint_number $obj] + +# This also tests that the previous do_not_stop_here breakpoint is +# cleared. +set obj [dap_check_request_and_response "set breakpoint on function" \ + setFunctionBreakpoints \ + {o breakpoints [a [o name [s function_breakpoint_here]]]}] +set fn_bpno [dap_get_breakpoint_number $obj] + +set obj [dap_check_request_and_response "set breakpoint with invalid filename" \ + setBreakpoints \ + [format {o source [o path [s nosuchfilename.c]] breakpoints [a [o line [i 29]]]}]] + +set line [gdb_get_line_number "BREAK"] +set obj [dap_check_request_and_response "set breakpoint by line number" \ + setBreakpoints \ + [format {o source [o path [%s]] breakpoints [a [o line [i %d]]]} \ + [list s $srcfile] $line]] +set line_bpno [dap_get_breakpoint_number $obj] + +# Check the new breakpoint event. +set ok 0 +foreach event [lindex $obj 1] { + set d [namespace eval ton::2dict $event] + if {[dict get $d type] != "event" + || [dict get $d event] != "breakpoint"} { + continue + } + if {[dict get $d body reason] == "new" + && [dict get $d body breakpoint verified] == "true"} { + set ok 1 + pass "check new breakpoint event" + break + } +} +if {!$ok} { + fail "check new breakpoint event" +} + +set obj [dap_check_request_and_response "reset breakpoint by line number" \ + setBreakpoints \ + [format {o source [o path [%s]] breakpoints [a [o line [i %d]]]} \ + [list s $srcfile] $line]] +set new_line_bpno [dap_get_breakpoint_number $obj] + +if {$new_line_bpno == $line_bpno} { + pass "re-setting kept same breakpoint number" +} else { + fail "re-setting kept same breakpoint number" +} + +# This uses "&address_breakpoint_here" as the address -- this is a +# hack because we know how this is implemented under the hood. +set obj [dap_check_request_and_response "set breakpoint by address" \ + setInstructionBreakpoints \ + {o breakpoints [a [o instructionReference [s &address_breakpoint_here]]]}] +set insn_bpno [dap_get_breakpoint_number $obj] + +set d [namespace eval ton::2dict [lindex $obj 0]] +set bplist [dict get $d body breakpoints] +set insn_pc [dict get [lindex $bplist 0] instructionReference] + +dap_check_request_and_response "start inferior" configurationDone +dap_read_event "inferior started" thread "body reason" started + +dap_read_event "stopped at function breakpoint" stopped \ + "body reason" breakpoint \ + "body hitBreakpointIds" $fn_bpno + +set obj [dap_check_request_and_response "evaluate global in function" \ + evaluate {o expression [s global_variable]}] +dap_match_values "global value in function" [lindex $obj 0] \ + "body result" 23 + +dap_check_request_and_response step stepIn {o threadId [i 1]} +dap_read_event "stopped after step" stopped "body reason" step + +set obj [dap_check_request_and_response "evaluate global second time" \ + evaluate {o expression [s global_variable]}] +dap_match_values "global value after step" [lindex $obj 0] \ + "body result" 24 + +dap_check_request_and_response "continue to address" continue +dap_read_event "stopped at address breakpoint" stopped \ + "body reason" breakpoint \ + "body hitBreakpointIds" $insn_bpno + +dap_check_request_and_response "continue to line" continue +dap_read_event "stopped at line breakpoint" stopped \ + "body reason" breakpoint \ + "body hitBreakpointIds" $line_bpno + +set obj [dap_check_request_and_response "evaluate global in main" \ + evaluate {o expression [s global_variable]}] +dap_match_values "global value in main" [lindex $obj 0] \ + "body result" 25 + +set obj [dap_request_and_response "evaluate non-existing variable" \ + evaluate {o expression [s nosuchvariable]}] +set d [namespace eval ton::2dict [lindex $obj 0]] +if {[dict get $d success] == "false"} { + pass "result of invalid request" +} else { + fail "result of invalid request" +} + +set obj [dap_check_request_and_response "disassemble one instruction" \ + disassemble \ + [format {o memoryReference [s %s] instructionCount [i 1]} \ + $insn_pc]] +set d [namespace eval ton::2dict [lindex $obj 0]] +if {[dict exists $d body instructions]} { + pass "instructions in disassemble output" +} else { + fail "instructions in disassemble output" +} + +dap_shutdown diff --git a/gdb/testsuite/lib/dap-support.exp b/gdb/testsuite/lib/dap-support.exp new file mode 100644 index 00000000000..adf332cd7a5 --- /dev/null +++ b/gdb/testsuite/lib/dap-support.exp @@ -0,0 +1,343 @@ +# 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 . + +# The JSON parser. +load_lib ton.tcl + +# The sequence number for the next DAP request. This is used by the +# automatic sequence-counting code below. It is reset each time GDB +# is restarted. +set dap_seq 1 + +# Start gdb using the DAP interpreter. +proc dap_gdb_start {} { + # Keep track of the number of times GDB has been launched. + global gdb_instances + incr gdb_instances + + gdb_stdin_log_init + + global GDBFLAGS stty_init + save_vars { GDBFLAGS stty_init } { + set stty_init "-echo raw" + set logfile [standard_output_file "dap.log.$gdb_instances"] + append GDBFLAGS " -iex \"set debug dap-log-file $logfile\" -q -i=dap" + set res [gdb_spawn] + if {$res != 0} { + return $res + } + } + + # Reset the counter. + set ::dap_seq 1 + + return 0 +} + +# A helper for dap_to_ton that decides if the list L is a JSON object +# or if it is an array. +proc _dap_is_obj {l} { + if {[llength $l] % 2 != 0} { + return 0 + } + foreach {key value} $l { + if {![string is alpha $key]} { + return 0 + } + } + return 1 +} + +# The "TON" format is a bit of a pain to write by hand, so this proc +# can be used to convert an ordinary Tcl list into TON by guessing at +# the correct forms to use. This can't be used in all cases, because +# Tcl can't really differentiate between literal forms. For example, +# there's no way to decide if "true" should be a string or the literal +# true. +# +# JSON objects must be passed in a particular form here -- as a list +# with an even number of elements, alternating keys and values. Each +# key must consist only of letters, no digits or other non-letter +# characters. Note that this is compatible with the Tcl 'dict' +# representation. +proc dap_to_ton {obj} { + if {[string is list $obj] && [llength $obj] > 1} { + if {[_dap_is_obj $obj]} { + set result o + foreach {key value} $obj { + lappend result $key \[[dap_to_ton $value]\] + } + } else { + set result a + foreach val $obj { + lappend result \[[dap_to_ton $val]\] + } + } + } elseif {[string is entier $obj]} { + set result [list i $obj] + } elseif {[string is double $obj]} { + set result [list d $obj] + } elseif {$obj == "true" || $obj == "false" || $obj == "null"} { + set result [list l $obj] + } else { + set result [list s $obj] + } + return $result +} + +# Format the object OBJ, in TON format, as JSON and send it to gdb. +proc dap_send_ton {obj} { + set json [namespace eval ton::2json $obj] + # FIXME this is wrong for non-ASCII characters. + set len [string length $json] + verbose ">>> $json" + send_gdb "Content-Length: $len\r\n\r\n$json" +} + +# Send a DAP request to gdb. COMMAND is the request's "command" +# field, and OBJ is the "arguments" field. If OBJ is empty, it is +# omitted. The sequence number of the request is automatically added, +# and this is also the return value. OBJ is assumed to already be in +# TON form. +proc dap_send_request {command {obj {}}} { + # We can construct this directly as a TON object. + set result $::dap_seq + incr ::dap_seq + set req [format {o seq [i %d] type [s request] command [%s]} \ + $result [list s $command]] + if {$obj != ""} { + append req " arguments \[$obj\]" + } + dap_send_ton $req + return $result +} + +# Read a JSON response from gdb. This will return a TON object on +# success, or throw an exception on error. +proc dap_read_json {} { + set length "" + gdb_expect { + -re "^Content-Length: (\[0-9\]+)\r\n" { + set length $expect_out(1,string) + exp_continue + } + -re "^(\[^\r\n\]+)\r\n" { + # Any other header field. + exp_continue + } + -re "^\r\n" { + # Done. + } + timeout { + error "timeout reading json header" + } + eof { + error "eof reading json header" + } + } + + if {$length == ""} { + error "didn't find content-length" + } + + set json "" + while {$length > 0} { + # Tcl only allows up to 255 characters in a {} expression in a + # regexp, so we may need to read in chunks. + set this_len [expr {min ($length, 255)}] + gdb_expect { + -re "^.{$this_len}" { + append json $expect_out(0,string) + } + timeout { + error "timeout reading json body" + } + eof { + error "eof reading json body" + } + } + incr length -$this_len + } + + return [ton::json2ton $json] +} + +# Read a sequence of JSON objects from gdb, until a response object is +# seen. If the response object has the request sequence number NUM, +# and is for command CMD, return a list of two elements: the response +# object and a list of any preceding events, in the order they were +# emitted. The objects are in TON format. If a response object is +# seen but has the wrong sequence number or command, throw an +# exception +proc dap_read_response {cmd num} { + set result {} + while 1 { + set obj [dap_read_json] + set d [namespace eval ton::2dict $obj] + if {[dict get $d type] == "response"} { + if {[dict get $d request_seq] != $num} { + error "saw wrong request_seq in $obj" + } elseif {[dict get $d command] != $cmd} { + error "saw wrong command in $obj" + } else { + return [list $obj $result] + } + } else { + lappend result $obj + } + } +} + +# A wrapper for dap_send_request and dap_read_response. This sends a +# request to gdb and returns the result. NAME is used to issue a pass +# or fail; on failure, this always returns an empty string. +proc dap_request_and_response {name command {obj {}}} { + set result {} + if {[catch { + set seq [dap_send_request $command $obj] + set result [dap_read_response $command $seq] + } text]} { + verbose "reason: $text" + fail $name + } else { + pass $name + } + return $result +} + +# Like dap_request_and_response, but also checks that the response +# indicates success. +proc dap_check_request_and_response {name command {obj {}}} { + set result [dap_request_and_response $name $command $obj] + if {$result == ""} { + return "" + } + set d [namespace eval ton::2dict [lindex $result 0]] + if {[dict get $d success] != "true"} { + verbose "request failure: $result" + fail "$name success" + return "" + } + pass "$name success" + return $result +} + +# Start gdb, send a DAP initialization request and return the +# response. This approach lets the caller check the feature list, if +# desired. Callers not caring about this should probably use +# dap_launch. Returns the empty string on failure. NAME is used as +# the test name. +proc dap_initialize {name} { + if {[dap_gdb_start]} { + return "" + } + return [dap_check_request_and_response $name initialize] +} + +# Start gdb, send a DAP initialize request, and then a launch request +# specifying FILE as the program to use for the inferior. Returns the +# empty string on failure, or the response object from the launch +# request. After this is called, gdb will be ready to accept +# breakpoint requests. NAME is used as the test name. It has a +# reasonable default but can be overridden in case a test needs to +# launch gdb more than once. +proc dap_launch {file {name startup}} { + if {[dap_initialize "$name - initialize"] == ""} { + return "" + } + return [dap_check_request_and_response "$name - launch" launch \ + [format {o program [%s]} \ + [list s [standard_output_file $file]]]] +} + +# Cleanly shut down gdb. NAME is used as the test name. +proc dap_shutdown {{name shutdown}} { + dap_check_request_and_response $name disconnect +} + +# Search the event list EVENTS for an output event matching the regexp +# RX. Pass the test NAME if found, fail if not. +proc dap_search_output {name rx events} { + foreach event $events { + set d [namespace eval ton::2dict $event] + if {[dict get $d type] != "event" + || [dict get $d event] != "output"} { + continue + } + if {[regexp $rx [dict get $d body output]]} { + pass $name + return + } + } + fail $name +} + +# Check that OBJ (a single TON object) has values that match the +# key/value pairs given in ARGS. NAME is used as the test name. +proc dap_match_values {name obj args} { + set d [namespace eval ton::2dict $obj] + foreach {key value} $args { + if {[eval dict get [list $d] $key] != $value} { + fail "$name (checking $key)" + return "" + } + } + pass $name +} + +# A helper for dap_read_event that reads events, looking for one +# matching TYPE. +proc _dap_read_event {type} { + while 1 { + # We don't do any extra error checking here for the time + # being; we'll just get a timeout thrown instead. + set obj [dap_read_json] + set d [namespace eval ton::2dict $obj] + if {[dict get $d type] == "event" + && [dict get $d event] == $type} { + return $obj + } + } +} + +# Read JSON objects looking for an event whose "event" field is TYPE. +# NAME is used as the test name; it defaults to TYPE. Extra arguments +# are used to check fields of the event; the arguments alternate +# between a field name (in "dict get" form) and its expected value. +# Returns the TON object for the chosen event, or empty string on +# error. +proc dap_read_event {name type args} { + if {$name == ""} { + set name $type + } + + if {[catch {_dap_read_event $type} result]} { + fail $name + return "" + } + + eval dap_match_values [list $name $result] $args + + return $result +} + +# A convenience function to extract the breakpoint number when a new +# breakpoint is created. OBJ is an object as returned by +# dap_check_request_and_response. +proc dap_get_breakpoint_number {obj} { + set d [namespace eval ton::2dict [lindex $obj 0]] + set bplist [dict get $d body breakpoints] + return [dict get [lindex $bplist 0] id] +} diff --git a/gdb/testsuite/lib/mi-support.exp b/gdb/testsuite/lib/mi-support.exp index e9226ada158..1ee087d8127 100644 --- a/gdb/testsuite/lib/mi-support.exp +++ b/gdb/testsuite/lib/mi-support.exp @@ -238,8 +238,6 @@ proc default_mi_gdb_start { { flags {} } } { return [mi_gdb_start_separate_mi_tty $flags] } - set inferior_pty no-tty - # Set the default value, it may be overriden later by specific testfile. set use_gdb_stub [target_info exists use_gdb_stub] diff --git a/gdb/testsuite/lib/ton.tcl b/gdb/testsuite/lib/ton.tcl new file mode 100644 index 00000000000..a9013b91ccf --- /dev/null +++ b/gdb/testsuite/lib/ton.tcl @@ -0,0 +1,303 @@ +# This was imported into gdb from: +# https://github.com/jorge-leon/ton + +# This software is copyrighted by Georg Lehner . +# The following terms apply to all files associated with the software +# unless explicitly disclaimed in individual files. + +# The authors hereby grant permission to use, copy, modify, distribute, +# and license this software and its documentation for any purpose, +# provided that existing copyright notices are retained in all copies +# and that this notice is included verbatim in any distributions. No +# written agreement, license, or royalty fee is required for any of the +# authorized uses. Modifications to this software may be copyrighted by +# their authors and need not follow the licensing terms described here, +# provided that the new terms are clearly indicated on the first page of +# each file where they apply. + +# IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY +# FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +# ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY +# DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, +# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND +# NON-INFRINGEMENT. THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, AND +# THE AUTHORS AND DISTRIBUTORS HAVE NO OBLIGATION TO PROVIDE +# MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +# GOVERNMENT USE: If you are acquiring this software on behalf of the +# U.S. government, the Government shall have only "Restricted Rights" in +# the software and related documentation as defined in the Federal +# Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you +# are acquiring the software on behalf of the Department of Defense, the +# software shall be classified as "Commercial Computer Software" and the +# Government shall have only "Restricted Rights" as defined in Clause +# 252.227-7013 (c) (1) of DFARs. Notwithstanding the foregoing, the +# authors grant the U.S. Government and others acting in its behalf +# permission to use and distribute the software in accordance with the +# terms specified in this license. + + +# leg20180331: ton / TON - Tcl Object Notation +# +# This package provides manipulation functionality for TON - a data +# serialization format with a direct mapping to JSON. +# +# In its essence, a JSON parser is provided, which can convert a JSON +# string into a Tcllib json style dictionary (dicts and arrays mixed), +# into a jimhttp style dictionary (only dicts) or into a nested, typed +# Tcl list. +# +# Finally, TON can be converted into (unformatted) JSON. + +namespace eval ton { + namespace export json2ton + + variable version 0.4 + +} +proc ton::json2ton json { + # Parse JSON string json + # + # return: TON + + set i [trr $json [string length $json]] + if {!$i} {return ""} + lassign [jscan $json $i] i ton + if {[set i [trr $json $i]]} { + error "json string invalid:[incr i -1]: left over characters." + } + return $ton +} +proc ton::trr {s i} { + # Trim righthand whitespace on the first i characters of s. + # return: number of remaining characters in s + + while {[set j $i] && + ([string is space [set c [string index $s [incr i -1]]]] + || $c eq "\n")} {} + return $j +} +proc ton::jscan {json i {d :}} { + # Scan JSON in first i characters of string json. + # d is the default delimiter list for the next token. + # + # return list of + # - remaining characters in json + # - TON + # + # The string must already be whitespace trimmed from the right. + + incr i -1 + + if {[set c [string index $json $i]] eq "\""} { + str $json [incr i -1] + } elseif {$c eq "\}"} { + obj $json $i + } elseif {$c eq "\]"} { + arr $json $i + } elseif {$c in {e l}} { + lit $json $i + } elseif {[string match {[0-9.]} $c]} { + num $json $i $c $d + } else { + error "json string end invalid:$i: ..[string range $json $i-10 $i].." + } +} +proc ton::num {json i c d} { + # Parse number from position i in string json to the left. + # c .. character at position i + # d .. delimiter on which to stop + # + # return list: + # - remaining string length + # - TON of number + + set float [expr {$c eq "."}] + for {set j $i} {$i} {incr i -1} { + if {[string match $d [set c [string index $json $i-1]]]} break + set float [expr {$float || [string match "\[eE.]" $c]}] + } + set num [string trimleft [string range $json $i $j]] + if {!$float && [string is entier $num]} { + list $i "i $num" + } elseif {$float && [string is double $num]} { + list $i "d $num" + } else { + error "number invalid:$i: $num." + } +} +proc ton::lit {json i} { + # Parse literal from position i in string json to the left + # return list: + # - remaining string length + # - TON of literal + + if {[set c [string index $json $i-1]] eq "u"} { + list [incr i -3] "l true" + } elseif {$c eq "s"} { + list [incr i -4] "l false" + } elseif {$c eq "l"} { + list [incr i -3] "l null" + } else { + set e [string range $json $i-3 $i] + error "literal invalid:[incr i -1]: ..$e." + } +} +proc ton::str {json i} { + # Parse string from position i in string json to the left + # return list: + # - remaining string length + # - TON of string + + for {set j $i} {$i} {incr i -1} { + set i [string last \" $json $i] + if {[string index $json $i-1] ne "\\"} break + } + if {$i==-1} { + error "json string start invalid:$i: exhausted while parsing string." + } + list $i "s [list [string range $json $i+1 $j]]" +} +proc ton::arr {json i} { + # Parse array from i characters in string json + # return list: + # - remaining string length + # - TON of array + + set i [trr $json $i] + if {!$i} { + error "json string invalid:0: exhausted while parsing array." + } + if {[string index $json $i-1] eq "\["} { + return [list [incr i -1] a] + } + set r {} + while {$i} { + lassign [jscan $json $i "\[,\[]"] i v + lappend r \[$v\] + set i [trr $json $i] + incr i -1 + if {[set c [string index $json $i]] eq ","} { + set i [trr $json $i] + continue + } elseif {$c eq "\["} break + error "json string invalid:$i: parsing array." + } + lappend r a + return [list $i [join [lreverse $r]]] +} +proc ton::obj {json i} { + # Parse array from i character in string json + # return list: + # - remaining string length + # - TON of object + + set i [trr $json $i] + if {!$i} { + error "json string invalid:0: exhausted while parsing object." + } + if {[string index $json $i-1] eq "\{"} { + return [list [incr i -1] o] + } + set r {} + while {$i} { + lassign [jscan $json $i] i v + set i [trr $json $i] + incr i -1 + if {[string index $json $i] ne ":"} { + error "json string invalid:$i: parsing key in object." + } + set i [trr $json $i] + lassign [jscan $json $i] i k + lassign $k type k + if {$type ne "s"} { + error "json string invalid:[incr i -1]: key not a string." + } + lappend r \[$v\] [list $k] + set i [trr $json $i] + incr i -1 + if {[set c [string index $json $i]] eq ","} { + set i [trr $json $i] + continue + } elseif {$c eq "\{"} break + error "json string invalid:$i: parsing object." + } + lappend r o + return [list $i [join [lreverse $r]]] +} +# TON decoders +namespace eval ton::2list { + proc atom {type v} {list $type $v} + foreach type {i d s l} { + interp alias {} $type {} [namespace current]::atom $type + } + proc a args { + set r a + foreach v $args {lappend r $v} + return $r + } + proc o args { + set r o + foreach {k v} $args {lappend r $k $v} + return $r + } + # There is plenty of room for validation in get + # array index bounds + # object key existence + proc get {l args} { + foreach k $args { + switch [lindex $l 0] { + o {set l [dict get [lrange $l 1 end] $k]} + a {set l [lindex $l [incr k]]} + default { + error "error: key $k to long, or wrong data: [lindex $l 0]" + } + } + } + return $l + } +} +namespace eval ton::2dict { + proc atom v {return $v} + foreach type {i d l s} { + interp alias {} $type {} [namespace current]::atom + } + proc a args {return $args} + proc o args {return $args} +} +namespace eval ton::a2dict { + proc atom v {return $v} + foreach type {i d l s} { + interp alias {} $type {} [namespace current]::atom + } + proc a args { + set i -1 + set r {} + foreach v $args { + lappend r [incr i] $v + } + return $r + } + proc o args {return $args} +} +namespace eval ton::2json { + proc atom v {return $v} + foreach type {i d l} { + interp alias {} $type {} [namespace current]::atom + } + proc a args { + return "\[[join $args {, }]]" + } + proc o args { + set r {} + foreach {k v} $args {lappend r "\"$k\": $v"} + return "{[join $r {, }]}" + } + proc s s {return "\"$s\""} +} + +package provide ton $ton::version