[lldb-dap] Creating a 'capabilities' event helper. (#142831)

This adds a new 'CapabilitiesEventBody' type for having a well
structured type for the event and updates the 'restart' request
to dynamically set their capabilities.
This commit is contained in:
John Harrison
2025-06-10 10:49:07 -07:00
committed by GitHub
parent c30952592a
commit 07a1d479cc
13 changed files with 144 additions and 30 deletions

View File

@@ -135,6 +135,10 @@ class Source(object):
return source_dict
class NotSupportedError(KeyError):
"""Raised if a feature is not supported due to its capabilities."""
class DebugCommunication(object):
def __init__(
self,
@@ -153,7 +157,7 @@ class DebugCommunication(object):
self.recv_thread = threading.Thread(target=self._read_packet_thread)
self.process_event_body = None
self.exit_status: Optional[int] = None
self.initialize_body = None
self.capabilities: dict[str, Any] = {}
self.progress_events: list[Event] = []
self.reverse_requests = []
self.sequence = 1
@@ -300,6 +304,9 @@ class DebugCommunication(object):
elif event == "breakpoint":
# Breakpoint events are sent when a breakpoint is resolved
self._update_verified_breakpoints([body["breakpoint"]])
elif event == "capabilities":
# Update the capabilities with new ones from the event.
self.capabilities.update(body["capabilities"])
elif packet_type == "response":
if packet["command"] == "disconnect":
@@ -487,13 +494,13 @@ class DebugCommunication(object):
raise ValueError("didn't get terminated event")
return event_dict
def get_initialize_value(self, key):
def get_capability(self, key):
"""Get a value for the given key if it there is a key/value pair in
the "initialize" request response body.
the capabilities reported by the adapter.
"""
if self.initialize_body and key in self.initialize_body:
return self.initialize_body[key]
return None
if key in self.capabilities:
return self.capabilities[key]
raise NotSupportedError(key)
def get_threads(self):
if self.threads is None:
@@ -759,6 +766,9 @@ class DebugCommunication(object):
return response
def request_restart(self, restartArguments=None):
if self.exit_status is not None:
raise ValueError("request_restart called after process exited")
self.get_capability("supportsRestartRequest")
command_dict = {
"command": "restart",
"type": "request",
@@ -866,7 +876,7 @@ class DebugCommunication(object):
response = self.send_recv(command_dict)
if response:
if "body" in response:
self.initialize_body = response["body"]
self.capabilities = response["body"]
return response
def request_launch(
@@ -971,6 +981,7 @@ class DebugCommunication(object):
def request_stepInTargets(self, frameId):
if self.exit_status is not None:
raise ValueError("request_stepInTargets called after process exited")
self.get_capability("supportsStepInTargetsRequest")
args_dict = {"frameId": frameId}
command_dict = {
"command": "stepInTargets",

View File

@@ -128,6 +128,16 @@ class DAPTestCaseBase(TestBase):
time.sleep(0.5)
return False
def assertCapabilityIsSet(self, key: str, msg: Optional[str] = None) -> None:
"""Assert that given capability is set in the client."""
self.assertIn(key, self.dap_server.capabilities, msg)
self.assertEqual(self.dap_server.capabilities[key], True, msg)
def assertCapabilityIsNotSet(self, key: str, msg: Optional[str] = None) -> None:
"""Assert that given capability is not set in the client."""
if key in self.dap_server.capabilities:
self.assertEqual(self.dap_server.capabilities[key], False, msg)
def verify_breakpoint_hit(self, breakpoint_ids, timeout=DEFAULT_TIMEOUT):
"""Wait for the process we are debugging to stop, and verify we hit
any breakpoint location in the "breakpoint_ids" array.

View File

@@ -566,7 +566,7 @@ class TestDAP_launch(lldbdap_testcase.DAPTestCaseBase):
)
version_eval_output = version_eval_response["body"]["result"]
version_string = self.dap_server.get_initialize_value("$__lldb_version")
version_string = self.dap_server.get_capability("$__lldb_version")
self.assertEqual(
version_eval_output.splitlines(),
version_string.splitlines(),

View File

@@ -67,6 +67,7 @@ add_lldb_library(lldbDAP
Handler/VariablesRequestHandler.cpp
Protocol/ProtocolBase.cpp
Protocol/ProtocolEvents.cpp
Protocol/ProtocolTypes.cpp
Protocol/ProtocolRequests.cpp

View File

@@ -11,6 +11,8 @@
#include "DAPError.h"
#include "JSONUtils.h"
#include "LLDBUtils.h"
#include "Protocol/ProtocolEvents.h"
#include "Protocol/ProtocolTypes.h"
#include "lldb/API/SBFileSpec.h"
#include "llvm/Support/Error.h"
@@ -36,6 +38,22 @@ static void SendThreadExitedEvent(DAP &dap, lldb::tid_t tid) {
dap.SendJSON(llvm::json::Value(std::move(event)));
}
void SendTargetBasedCapabilities(DAP &dap) {
if (!dap.target.IsValid())
return;
protocol::CapabilitiesEventBody body;
// We only support restarting launch requests not attach requests.
if (dap.last_launch_request)
body.capabilities.supportedFeatures.insert(
protocol::eAdapterFeatureRestartRequest);
// Only notify the client if supportedFeatures changed.
if (!body.capabilities.supportedFeatures.empty())
dap.Send(protocol::Event{"capabilities", body});
}
// "ProcessEvent": {
// "allOf": [
// { "$ref": "#/definitions/Event" },

View File

@@ -17,6 +17,9 @@ struct DAP;
enum LaunchMethod { Launch, Attach, AttachForSuspendedLaunch };
/// Update capabilities based on the configured target.
void SendTargetBasedCapabilities(DAP &dap);
void SendProcessEvent(DAP &dap, LaunchMethod launch_method);
llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry = false);

View File

@@ -41,6 +41,11 @@ ConfigurationDoneRequestHandler::Run(const ConfigurationDoneArguments &) const {
"any debugger command scripts are not resuming the process during the "
"launch sequence.");
// Waiting until 'configurationDone' to send target based capabilities in case
// the launch or attach scripts adjust the target. The initial dummy target
// may have different capabilities than the final target.
SendTargetBasedCapabilities(dap);
// Clients can request a baseline of currently existing threads after
// we acknowledge the configurationDone request.
// Client requests the baseline of currently existing threads after

View File

@@ -334,9 +334,6 @@ class RestartRequestHandler : public LegacyRequestHandler {
public:
using LegacyRequestHandler::LegacyRequestHandler;
static llvm::StringLiteral GetCommand() { return "restart"; }
FeatureSet GetSupportedFeatures() const override {
return {protocol::eAdapterFeatureRestartRequest};
}
void operator()(const llvm::json::Object &request) const override;
};

View File

@@ -69,23 +69,6 @@ void RestartRequestHandler::operator()(
dap.SendJSON(llvm::json::Value(std::move(response)));
return;
}
// Check if we were in a "launch" session or an "attach" session.
//
// Restarting is not well defined when we started the session by attaching to
// an existing process, because we don't know how the process was started, so
// we don't support it.
//
// Note that when using runInTerminal we're technically attached, but it's an
// implementation detail. The adapter *did* launch the process in response to
// a "launch" command, so we can still stop it and re-run it. This is why we
// don't just check `dap.is_attach`.
if (!dap.last_launch_request) {
response["success"] = llvm::json::Value(false);
EmplaceSafeString(response, "message",
"Restarting an \"attach\" session is not supported.");
dap.SendJSON(llvm::json::Value(std::move(response)));
return;
}
const llvm::json::Object *arguments = request.getObject("arguments");
if (arguments) {

View File

@@ -17,8 +17,8 @@
//
//===----------------------------------------------------------------------===//
#ifndef LLDB_TOOLS_LLDB_DAP_PROTOCOL_H
#define LLDB_TOOLS_LLDB_DAP_PROTOCOL_H
#ifndef LLDB_TOOLS_LLDB_DAP_PROTOCOL_PROTOCOL_BASE_H
#define LLDB_TOOLS_LLDB_DAP_PROTOCOL_PROTOCOL_BASE_H
#include "llvm/Support/JSON.h"
#include <cstdint>

View File

@@ -0,0 +1,20 @@
//===-- ProtocolEvents.cpp ------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "Protocol/ProtocolEvents.h"
#include "llvm/Support/JSON.h"
using namespace llvm;
namespace lldb_dap::protocol {
json::Value toJSON(const CapabilitiesEventBody &CEB) {
return json::Object{{"capabilities", CEB.capabilities}};
}
} // namespace lldb_dap::protocol

View File

@@ -0,0 +1,46 @@
//===-- ProtocolEvents.h --------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file contains POD structs based on the DAP specification at
// https://microsoft.github.io/debug-adapter-protocol/specification
//
// This is not meant to be a complete implementation, new interfaces are added
// when they're needed.
//
// Each struct has a toJSON and fromJSON function, that converts between
// the struct and a JSON representation. (See JSON.h)
//
//===----------------------------------------------------------------------===//
#ifndef LLDB_TOOLS_LLDB_DAP_PROTOCOL_PROTOCOL_EVENTS_H
#define LLDB_TOOLS_LLDB_DAP_PROTOCOL_PROTOCOL_EVENTS_H
#include "Protocol/ProtocolTypes.h"
#include "llvm/Support/JSON.h"
namespace lldb_dap::protocol {
/// The event indicates that one or more capabilities have changed.
///
/// Since the capabilities are dependent on the client and its UI, it might not
/// be possible to change that at random times (or too late).
///
/// Consequently this event has a hint characteristic: a client can only be
/// expected to make a 'best effort' in honoring individual capabilities but
/// there are no guarantees.
///
/// Only changed capabilities need to be included, all other capabilities keep
/// their values.
struct CapabilitiesEventBody {
Capabilities capabilities;
};
llvm::json::Value toJSON(const CapabilitiesEventBody &);
} // end namespace lldb_dap::protocol
#endif

View File

@@ -7,12 +7,14 @@
//===----------------------------------------------------------------------===//
#include "Protocol/ProtocolTypes.h"
#include "Protocol/ProtocolEvents.h"
#include "Protocol/ProtocolRequests.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/JSON.h"
#include "llvm/Testing/Support/Error.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <optional>
using namespace llvm;
using namespace lldb;
@@ -666,3 +668,21 @@ TEST(ProtocolTypesTest, ThreadResponseBody) {
// Validate toJSON
EXPECT_EQ(json, pp(body));
}
TEST(ProtocolTypesTest, CapabilitiesEventBody) {
Capabilities capabilities;
capabilities.supportedFeatures = {
eAdapterFeatureANSIStyling,
eAdapterFeatureBreakpointLocationsRequest,
};
CapabilitiesEventBody body;
body.capabilities = capabilities;
StringRef json = R"({
"capabilities": {
"supportsANSIStyling": true,
"supportsBreakpointLocationsRequest": true
}
})";
// Validate toJSON
EXPECT_EQ(json, pp(body));
}