Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ def as_dict(self):
return source_dict


class NotSupportedError(KeyError):
"""Raised if a feature is not supported due to its capabilities."""


class DebugCommunication(object):
def __init__(
self,
Expand All @@ -153,7 +157,7 @@ def __init__(
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
Expand Down Expand Up @@ -300,6 +304,9 @@ def _handle_recv_packet(self, packet: Optional[ProtocolMessage]) -> bool:
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":
Expand Down Expand Up @@ -487,13 +494,13 @@ def wait_for_terminated(self, timeout: Optional[float] = None):
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:
Expand Down Expand Up @@ -759,6 +766,9 @@ def request_continue(self, threadId=None, singleThread=False):
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",
Expand Down Expand Up @@ -866,7 +876,7 @@ def request_initialize(self, sourceInitFile=False):
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(
Expand Down Expand Up @@ -971,6 +981,7 @@ def request_stepIn(self, threadId, targetId, granularity="statement"):
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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@ def waitUntil(self, condition_callback):
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.
Expand Down
2 changes: 1 addition & 1 deletion lldb/test/API/tools/lldb-dap/launch/TestDAP_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ def test_version(self):
)
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(),
Expand Down
1 change: 1 addition & 0 deletions lldb/tools/lldb-dap/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ add_lldb_library(lldbDAP
Handler/VariablesRequestHandler.cpp

Protocol/ProtocolBase.cpp
Protocol/ProtocolEvents.cpp
Protocol/ProtocolTypes.cpp
Protocol/ProtocolRequests.cpp

Expand Down
18 changes: 18 additions & 0 deletions lldb/tools/lldb-dap/EventHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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" },
Expand Down
3 changes: 3 additions & 0 deletions lldb/tools/lldb-dap/EventHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions lldb/tools/lldb-dap/Handler/RequestHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
17 changes: 0 additions & 17 deletions lldb/tools/lldb-dap/Handler/RestartRequestHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions lldb/tools/lldb-dap/Protocol/ProtocolBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
20 changes: 20 additions & 0 deletions lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
Original file line number Diff line number Diff line change
@@ -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
46 changes: 46 additions & 0 deletions lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions lldb/unittests/DAP/ProtocolTypesTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
Loading