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:
parent
c43d829bca
commit
de7d7cb58e
26 changed files with 2297 additions and 2 deletions
69
gdb/python/lib/gdb/dap/__init__.py
Normal file
69
gdb/python/lib/gdb/dap/__init__.py
Normal 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)
|
143
gdb/python/lib/gdb/dap/breakpoint.py
Normal file
143
gdb/python/lib/gdb/dap/breakpoint.py
Normal 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,
|
||||
}
|
93
gdb/python/lib/gdb/dap/bt.py
Normal file
93
gdb/python/lib/gdb/dap/bt.py
Normal 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))
|
51
gdb/python/lib/gdb/dap/disassemble.py
Normal file
51
gdb/python/lib/gdb/dap/disassemble.py
Normal 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)
|
||||
)
|
42
gdb/python/lib/gdb/dap/evaluate.py
Normal file
42
gdb/python/lib/gdb/dap/evaluate.py
Normal 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,
|
||||
}
|
166
gdb/python/lib/gdb/dap/events.py
Normal file
166
gdb/python/lib/gdb/dap/events.py
Normal 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)
|
57
gdb/python/lib/gdb/dap/frames.py
Normal file
57
gdb/python/lib/gdb/dap/frames.py
Normal 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]
|
67
gdb/python/lib/gdb/dap/io.py
Normal file
67
gdb/python/lib/gdb/dap/io.py
Normal 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)
|
39
gdb/python/lib/gdb/dap/launch.py
Normal file
39
gdb/python/lib/gdb/dap/launch.py
Normal 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))
|
51
gdb/python/lib/gdb/dap/next.py
Normal file
51
gdb/python/lib/gdb/dap/next.py
Normal 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}
|
23
gdb/python/lib/gdb/dap/pause.py
Normal file
23
gdb/python/lib/gdb/dap/pause.py
Normal 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))
|
65
gdb/python/lib/gdb/dap/scopes.py
Normal file
65
gdb/python/lib/gdb/dap/scopes.py
Normal 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}
|
205
gdb/python/lib/gdb/dap/server.py
Normal file
205
gdb/python/lib/gdb/dap/server.py
Normal 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()
|
189
gdb/python/lib/gdb/dap/startup.py
Normal file
189
gdb/python/lib/gdb/dap/startup.py
Normal 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
|
25
gdb/python/lib/gdb/dap/state.py
Normal file
25
gdb/python/lib/gdb/dap/state.py
Normal 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}")
|
42
gdb/python/lib/gdb/dap/threads.py
Normal file
42
gdb/python/lib/gdb/dap/threads.py
Normal 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
99
gdb/python/py-dap.c
Normal 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);
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue