Initial implementation of Debugger Adapter Protocol

The Debugger Adapter Protocol is a JSON-RPC protocol that IDEs can use
to communicate with debuggers.  You can find more information here:

    https://microsoft.github.io/debug-adapter-protocol/

Frequently this is implemented as a shim, but it seemed to me that GDB
could implement it directly, via the Python API.  This patch is the
initial implementation.

DAP is implemented as a new "interp".  This is slightly weird, because
it doesn't act like an ordinary interpreter -- for example it doesn't
implement a command syntax, and doesn't use GDB's ordinary event loop.
However, this seemed like the best approach overall.

To run GDB in this mode, use:

    gdb -i=dap

The DAP code will accept JSON-RPC messages on stdin and print
responses to stdout.  GDB redirects the inferior's stdout to a new
pipe so that output can be encapsulated by the protocol.

The Python code uses multiple threads to do its work.  Separate
threads are used for reading JSON from the client and for writing JSON
to the client.  All GDB work is done in the main thread.  (The first
implementation used asyncio, but this had some limitations, and so I
rewrote it to use threads instead.)

This is not a complete implementation of the protocol, but it does
implement enough to demonstrate that the overall approach works.

There is a rudimentary test suite.  It uses a JSON parser written in
pure Tcl.  This parser is under the same license as Tcl itself, so I
felt it was acceptable to simply import it into the tree.

There is also a bit of documentation -- just documenting the new
interpreter name.
This commit is contained in:
Tom Tromey 2022-06-23 11:11:36 -06:00
parent c43d829bca
commit de7d7cb58e
26 changed files with 2297 additions and 2 deletions

View file

@ -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 \

View file

@ -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.

View file

@ -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 \

View file

@ -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

View file

@ -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)

View file

@ -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,
}

View file

@ -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))

View file

@ -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)
)

View file

@ -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,
}

View file

@ -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)

View file

@ -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]

View file

@ -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)

View file

@ -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))

View file

@ -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}

View file

@ -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))

View file

@ -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}

View file

@ -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()

View file

@ -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

View file

@ -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}")

View file

@ -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,
}

99
gdb/python/py-dap.c Normal file
View file

@ -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 <http://www.gnu.org/licenses/>. */
#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<ui_out> 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);
});
}

View file

@ -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 <http://www.gnu.org/licenses/>. */
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 */
}

View file

@ -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 <http://www.gnu.org/licenses/>.
# 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

View file

@ -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 <http://www.gnu.org/licenses/>.
# 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]
}

View file

@ -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]

303
gdb/testsuite/lib/ton.tcl Normal file
View file

@ -0,0 +1,303 @@
# This was imported into gdb from:
# https://github.com/jorge-leon/ton
# This software is copyrighted by Georg Lehner <jorge@at.anteris.net>.
# 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