Skip to content

Commit 9b51e65

Browse files
committed
[lldb-dap] Adding support for cancelling a request.
Adding support for cancelling requests. There are two forms of request cancellation. * Preemptively cancelling a request that is in the queue. * Actively cancelling the in progress request as a best effort attempt using `SBDebugger.RequestInterrupt()`.
1 parent fbb8929 commit 9b51e65

File tree

16 files changed

+359
-24
lines changed

16 files changed

+359
-24
lines changed

lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,13 @@ def packet_type_is(packet, packet_type):
8888

8989

9090
def dump_dap_log(log_file):
91-
print("========= DEBUG ADAPTER PROTOCOL LOGS =========")
91+
print("========= DEBUG ADAPTER PROTOCOL LOGS =========", file=sys.stderr)
9292
if log_file is None:
93-
print("no log file available")
93+
print("no log file available", file=sys.stderr)
9494
else:
9595
with open(log_file, "r") as file:
96-
print(file.read())
97-
print("========= END =========")
96+
print(file.read(), file=sys.stderr)
97+
print("========= END =========", file=sys.stderr)
9898

9999

100100
def read_packet_thread(vs_comm, log_file):
@@ -107,6 +107,10 @@ def read_packet_thread(vs_comm, log_file):
107107
# termination of lldb-dap and stop waiting for new packets.
108108
done = not vs_comm.handle_recv_packet(packet)
109109
finally:
110+
# Wait for the process to fully exit before dumping the log file to
111+
# ensure we have the entire log contents.
112+
if vs_comm.process is not None:
113+
vs_comm.process.wait()
110114
dump_dap_log(log_file)
111115

112116

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
C_SOURCES := main.c
2+
3+
include Makefile.rules
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""
2+
Test lldb-dap cancel request
3+
"""
4+
5+
from lldbsuite.test.decorators import *
6+
from lldbsuite.test.lldbtest import *
7+
import lldbdap_testcase
8+
9+
10+
class TestDAP_launch(lldbdap_testcase.DAPTestCaseBase):
11+
def send_async_req(self, command: str, arguments={}) -> int:
12+
seq = self.dap_server.sequence
13+
self.dap_server.send_packet(
14+
{
15+
"type": "request",
16+
"command": command,
17+
"arguments": arguments,
18+
}
19+
)
20+
return seq
21+
22+
def async_blocking_request(self, duration: float) -> int:
23+
"""
24+
Sends an evaluate request that will sleep for the specified duration to
25+
block the request handling thread.
26+
"""
27+
return self.send_async_req(
28+
command="evaluate",
29+
arguments={
30+
"expression": "`script import time; time.sleep({})".format(duration),
31+
"context": "repl",
32+
},
33+
)
34+
35+
def async_cancel(self, requestId: int) -> int:
36+
return self.send_async_req(command="cancel", arguments={"requestId": requestId})
37+
38+
def test_pending_request(self):
39+
"""
40+
Tests cancelling a pending request.
41+
"""
42+
program = self.getBuildArtifact("a.out")
43+
self.build_and_launch(program, stopOnEntry=True)
44+
self.continue_to_next_stop()
45+
46+
# Use a relatively short timeout since this is only to ensure the
47+
# following request is queued.
48+
blocking_seq = self.async_blocking_request(duration=1.0)
49+
# Use a longer timeout to ensure we catch if the request was interrupted
50+
# properly.
51+
pending_seq = self.async_blocking_request(duration=self.timeoutval)
52+
cancel_seq = self.async_cancel(requestId=pending_seq)
53+
54+
blocking_resp = self.dap_server.recv_packet(filter_type=["response"])
55+
self.assertEqual(blocking_resp["request_seq"], blocking_seq)
56+
self.assertEqual(blocking_resp["command"], "evaluate")
57+
self.assertEqual(blocking_resp["success"], True)
58+
59+
pending_resp = self.dap_server.recv_packet(filter_type=["response"])
60+
self.assertEqual(pending_resp["request_seq"], pending_seq)
61+
self.assertEqual(pending_resp["command"], "evaluate")
62+
self.assertEqual(pending_resp["success"], False)
63+
self.assertEqual(pending_resp["message"], "cancelled")
64+
65+
cancel_resp = self.dap_server.recv_packet(filter_type=["response"])
66+
self.assertEqual(cancel_resp["request_seq"], cancel_seq)
67+
self.assertEqual(cancel_resp["command"], "cancel")
68+
self.assertEqual(cancel_resp["success"], True)
69+
self.continue_to_exit()
70+
71+
def test_inflight_request(self):
72+
"""
73+
Tests cancelling an inflight request.
74+
"""
75+
program = self.getBuildArtifact("a.out")
76+
self.build_and_launch(program, stopOnEntry=True)
77+
self.continue_to_next_stop()
78+
79+
blocking_seq = self.async_blocking_request(duration=self.timeoutval)
80+
cancel_seq = self.async_cancel(requestId=blocking_seq)
81+
82+
blocking_resp = self.dap_server.recv_packet(filter_type=["response"])
83+
self.assertEqual(blocking_resp["request_seq"], blocking_seq)
84+
self.assertEqual(blocking_resp["command"], "evaluate")
85+
self.assertEqual(blocking_resp["success"], False)
86+
self.assertEqual(blocking_resp["message"], "cancelled")
87+
88+
cancel_resp = self.dap_server.recv_packet(filter_type=["response"])
89+
self.assertEqual(cancel_resp["request_seq"], cancel_seq)
90+
self.assertEqual(cancel_resp["command"], "cancel")
91+
self.assertEqual(cancel_resp["success"], True)
92+
self.continue_to_exit()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#include <stdio.h>
2+
3+
int main(int argc, char const *argv[]) {
4+
printf("Hello world!\n");
5+
return 0;
6+
}

lldb/test/API/tools/lldb-dap/launch/TestDAP_launch.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def test_default(self):
2727
lines = output.splitlines()
2828
self.assertIn(program, lines[0], "make sure program path is in first argument")
2929

30+
@skipIfWindows
3031
def test_termination(self):
3132
"""
3233
Tests the correct termination of lldb-dap upon a 'disconnect'

lldb/tools/lldb-dap/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ add_lldb_tool(lldb-dap
4040
Handler/ResponseHandler.cpp
4141
Handler/AttachRequestHandler.cpp
4242
Handler/BreakpointLocationsHandler.cpp
43+
Handler/CancelRequestHandler.cpp
4344
Handler/CompileUnitsRequestHandler.cpp
4445
Handler/CompletionsHandler.cpp
4546
Handler/ConfigurationDoneRequestHandler.cpp

lldb/tools/lldb-dap/DAP.cpp

Lines changed: 116 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@
3939
#include <algorithm>
4040
#include <cassert>
4141
#include <chrono>
42+
#include <condition_variable>
4243
#include <cstdarg>
4344
#include <cstdio>
4445
#include <fstream>
46+
#include <future>
4547
#include <memory>
4648
#include <mutex>
4749
#include <string>
@@ -778,28 +780,133 @@ llvm::Error DAP::Disconnect(bool terminateDebuggee) {
778780
return ToError(error);
779781
}
780782

783+
template <typename T>
784+
static std::optional<T> getArgumentsIfRequest(const protocol::Message &pm,
785+
llvm::StringLiteral command) {
786+
auto *const req = std::get_if<protocol::Request>(&pm);
787+
if (!req || req->command != command)
788+
return std::nullopt;
789+
790+
T args;
791+
llvm::json::Path::Root root;
792+
if (!fromJSON(req->arguments, args, root)) {
793+
return std::nullopt;
794+
}
795+
796+
return std::move(args);
797+
}
798+
781799
llvm::Error DAP::Loop() {
782-
auto cleanup = llvm::make_scope_exit([this]() {
800+
std::deque<protocol::Message> queue;
801+
std::condition_variable queue_cv;
802+
std::mutex queue_mutex;
803+
std::future<llvm::Error> queue_reader = std::async([&]() -> llvm::Error {
804+
auto cleanup = llvm::make_scope_exit([&]() {
805+
// Ensure we're marked as disconnecting when the reader exits.
806+
disconnecting = true;
807+
queue_cv.notify_all();
808+
});
809+
810+
while (!disconnecting) {
811+
llvm::Expected<std::optional<protocol::Message>> next =
812+
transport.Read(std::chrono::seconds(1));
813+
bool timeout = false;
814+
if (llvm::Error Err = llvm::handleErrors(
815+
next.takeError(),
816+
[&](std::unique_ptr<llvm::StringError> Err) -> llvm::Error {
817+
if (Err->convertToErrorCode() == std::errc::timed_out) {
818+
timeout = true;
819+
return llvm::Error::success();
820+
}
821+
return llvm::Error(std::move(Err));
822+
}))
823+
return Err;
824+
825+
// If the read timed out, continue to check if we should disconnect.
826+
if (timeout)
827+
continue;
828+
829+
// nullopt is returned on EOF.
830+
if (!*next)
831+
break;
832+
833+
{
834+
std::lock_guard<std::mutex> lock(queue_mutex);
835+
836+
// If a cancel is requested for the active request, make a best
837+
// effort attempt to interrupt.
838+
if (const auto cancel_args =
839+
getArgumentsIfRequest<protocol::CancelArguments>(**next,
840+
"cancel");
841+
cancel_args && active_seq == cancel_args->requestId)
842+
debugger.RequestInterrupt();
843+
844+
queue.push_back(std::move(**next));
845+
}
846+
queue_cv.notify_one();
847+
}
848+
849+
return llvm::Error::success();
850+
});
851+
852+
auto cleanup = llvm::make_scope_exit([&]() {
783853
out.Stop();
784854
err.Stop();
785855
StopEventHandlers();
786856
});
857+
787858
while (!disconnecting) {
788-
llvm::Expected<std::optional<protocol::Message>> next = transport.Read();
789-
if (!next)
790-
return next.takeError();
859+
protocol::Message next;
860+
{
861+
std::unique_lock<std::mutex> lock(queue_mutex);
862+
queue_cv.wait(lock, [&] { return disconnecting || !queue.empty(); });
791863

792-
// nullopt on EOF
793-
if (!*next)
794-
break;
864+
if (queue.empty())
865+
break;
795866

796-
if (!HandleObject(**next)) {
867+
next = queue.front();
868+
queue.pop_front();
869+
870+
if (protocol::Request *req = std::get_if<protocol::Request>(&next)) {
871+
active_seq = req->seq;
872+
873+
// Check if we should preempt this request from a queued cancel.
874+
bool cancelled = false;
875+
for (const auto &message : queue) {
876+
if (const auto args =
877+
getArgumentsIfRequest<protocol::CancelArguments>(message,
878+
"cancel");
879+
args && args->requestId == req->seq) {
880+
cancelled = true;
881+
break;
882+
}
883+
}
884+
885+
// Preempt the request and immeidately respond with cancelled.
886+
if (cancelled) {
887+
protocol::Response response;
888+
response.request_seq = req->seq;
889+
response.command = req->command;
890+
response.success = false;
891+
response.message = protocol::Response::Message::cancelled;
892+
Send(response);
893+
continue;
894+
}
895+
} else
896+
active_seq = 0;
897+
}
898+
899+
if (!HandleObject(next)) {
797900
return llvm::createStringError(llvm::inconvertibleErrorCode(),
798901
"unhandled packet");
799902
}
903+
904+
// Clear interrupt marker prior to handling the next request.
905+
if (debugger.InterruptRequested())
906+
debugger.CancelInterruptRequest();
800907
}
801908

802-
return llvm::Error::success();
909+
return queue_reader.get();
803910
}
804911

805912
lldb::SBError DAP::WaitForProcessToStop(uint32_t seconds) {

lldb/tools/lldb-dap/DAP.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,9 @@ struct DAP {
395395
InstructionBreakpoint *GetInstructionBreakpoint(const lldb::break_id_t bp_id);
396396

397397
InstructionBreakpoint *GetInstructionBPFromStopReason(lldb::SBThread &thread);
398+
399+
private:
400+
std::atomic<int64_t> active_seq;
398401
};
399402

400403
} // namespace lldb_dap
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//===-- SourceRequestHandler.cpp ------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "Handler/RequestHandler.h"
10+
#include "Protocol/ProtocolRequests.h"
11+
#include "llvm/Support/Error.h"
12+
#include <variant>
13+
14+
using namespace lldb_dap;
15+
using namespace lldb_dap::protocol;
16+
17+
namespace lldb_dap {
18+
19+
/// The `cancel` request is used by the client in two situations:
20+
///
21+
/// - to indicate that it is no longer interested in the result produced by a
22+
/// specific request issued earlier
23+
/// - to cancel a progress sequence.
24+
///
25+
/// Clients should only call this request if the corresponding capability
26+
/// `supportsCancelRequest` is true.
27+
///
28+
/// This request has a hint characteristic: a debug adapter can only be
29+
/// expected to make a 'best effort' in honoring this request but there are no
30+
/// guarantees.
31+
///
32+
/// The `cancel` request may return an error if it could not cancel
33+
/// an operation but a client should refrain from presenting this error to end
34+
/// users.
35+
///
36+
/// The request that got cancelled still needs to send a response back.
37+
/// This can either be a normal result (`success` attribute true) or an error
38+
/// response (`success` attribute false and the `message` set to `cancelled`).
39+
///
40+
/// Returning partial results from a cancelled request is possible but please
41+
/// note that a client has no generic way for detecting that a response is
42+
/// partial or not.
43+
///
44+
/// The progress that got cancelled still needs to send a `progressEnd` event
45+
/// back.
46+
///
47+
/// A client should not assume that progress just got cancelled after sending
48+
/// the `cancel` request.
49+
llvm::Expected<CancelResponseBody>
50+
CancelRequestHandler::Run(const CancelArguments &arguments) const {
51+
// Cancel support is built into the DAP::Loop handler for detecting
52+
// cancellations of pending or inflight requests.
53+
return std::monostate();
54+
}
55+
56+
} // namespace lldb_dap

lldb/tools/lldb-dap/Handler/InitializeRequestHandler.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,8 @@ void InitializeRequestHandler::operator()(
463463
body.try_emplace("supportsDataBreakpoints", true);
464464
// The debug adapter supports the `readMemory` request.
465465
body.try_emplace("supportsReadMemoryRequest", true);
466+
// The debug adapter supports the `cancel` request.
467+
body.try_emplace("supportsCancelRequest", true);
466468

467469
// Put in non-DAP specification lldb specific information.
468470
llvm::json::Object lldb_json;

lldb/tools/lldb-dap/Handler/RequestHandler.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,16 @@ class ReadMemoryRequestHandler : public LegacyRequestHandler {
381381
void operator()(const llvm::json::Object &request) const override;
382382
};
383383

384+
class CancelRequestHandler
385+
: public RequestHandler<protocol::CancelArguments,
386+
protocol::CancelResponseBody> {
387+
public:
388+
using RequestHandler::RequestHandler;
389+
static llvm::StringLiteral getCommand() { return "cancel"; }
390+
llvm::Expected<protocol::CancelResponseBody>
391+
Run(const protocol::CancelArguments &args) const override;
392+
};
393+
384394
/// A request used in testing to get the details on all breakpoints that are
385395
/// currently set in the target. This helps us to test "setBreakpoints" and
386396
/// "setFunctionBreakpoints" requests to verify we have the correct set of

0 commit comments

Comments
 (0)