Skip to content

Commit 2d30a60

Browse files
authored
[lldb-dap] Adding support for cancelling a request. (#130169)
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 44e32a2 commit 2d30a60

File tree

15 files changed

+510
-42
lines changed

15 files changed

+510
-42
lines changed
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: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""
2+
Test lldb-dap cancel request
3+
"""
4+
5+
import time
6+
7+
from lldbsuite.test.decorators import *
8+
from lldbsuite.test.lldbtest import *
9+
import lldbdap_testcase
10+
11+
12+
class TestDAP_launch(lldbdap_testcase.DAPTestCaseBase):
13+
def send_async_req(self, command: str, arguments={}) -> int:
14+
seq = self.dap_server.sequence
15+
self.dap_server.send_packet(
16+
{
17+
"type": "request",
18+
"command": command,
19+
"arguments": arguments,
20+
}
21+
)
22+
return seq
23+
24+
def async_blocking_request(self, duration: float) -> int:
25+
"""
26+
Sends an evaluate request that will sleep for the specified duration to
27+
block the request handling thread.
28+
"""
29+
return self.send_async_req(
30+
command="evaluate",
31+
arguments={
32+
"expression": '`script import time; print("starting sleep", file=lldb.debugger.GetOutputFileHandle()); time.sleep({})'.format(
33+
duration
34+
),
35+
"context": "repl",
36+
},
37+
)
38+
39+
def async_cancel(self, requestId: int) -> int:
40+
return self.send_async_req(command="cancel", arguments={"requestId": requestId})
41+
42+
def test_pending_request(self):
43+
"""
44+
Tests cancelling a pending request.
45+
"""
46+
program = self.getBuildArtifact("a.out")
47+
self.build_and_launch(program, stopOnEntry=True)
48+
self.continue_to_next_stop()
49+
50+
# Use a relatively short timeout since this is only to ensure the
51+
# following request is queued.
52+
blocking_seq = self.async_blocking_request(duration=1.0)
53+
# Use a longer timeout to ensure we catch if the request was interrupted
54+
# properly.
55+
pending_seq = self.async_blocking_request(duration=self.timeoutval / 2)
56+
cancel_seq = self.async_cancel(requestId=pending_seq)
57+
58+
blocking_resp = self.dap_server.recv_packet(filter_type=["response"])
59+
self.assertEqual(blocking_resp["request_seq"], blocking_seq)
60+
self.assertEqual(blocking_resp["command"], "evaluate")
61+
self.assertEqual(blocking_resp["success"], True)
62+
63+
pending_resp = self.dap_server.recv_packet(filter_type=["response"])
64+
self.assertEqual(pending_resp["request_seq"], pending_seq)
65+
self.assertEqual(pending_resp["command"], "evaluate")
66+
self.assertEqual(pending_resp["success"], False)
67+
self.assertEqual(pending_resp["message"], "cancelled")
68+
69+
cancel_resp = self.dap_server.recv_packet(filter_type=["response"])
70+
self.assertEqual(cancel_resp["request_seq"], cancel_seq)
71+
self.assertEqual(cancel_resp["command"], "cancel")
72+
self.assertEqual(cancel_resp["success"], True)
73+
self.continue_to_exit()
74+
75+
def test_inflight_request(self):
76+
"""
77+
Tests cancelling an inflight request.
78+
"""
79+
program = self.getBuildArtifact("a.out")
80+
self.build_and_launch(program, stopOnEntry=True)
81+
self.continue_to_next_stop()
82+
83+
blocking_seq = self.async_blocking_request(duration=self.timeoutval / 2)
84+
# Wait for the sleep to start to cancel the inflight request.
85+
self.collect_stdout(
86+
timeout_secs=self.timeoutval,
87+
pattern="starting sleep",
88+
)
89+
cancel_seq = self.async_cancel(requestId=blocking_seq)
90+
91+
blocking_resp = self.dap_server.recv_packet(filter_type=["response"])
92+
self.assertEqual(blocking_resp["request_seq"], blocking_seq)
93+
self.assertEqual(blocking_resp["command"], "evaluate")
94+
self.assertEqual(blocking_resp["success"], False)
95+
self.assertEqual(blocking_resp["message"], "cancelled")
96+
97+
cancel_resp = self.dap_server.recv_packet(filter_type=["response"])
98+
self.assertEqual(cancel_resp["request_seq"], cancel_seq)
99+
self.assertEqual(cancel_resp["command"], "cancel")
100+
self.assertEqual(cancel_resp["success"], True)
101+
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
@@ -37,6 +37,7 @@ add_lldb_tool(lldb-dap
3737
Handler/ResponseHandler.cpp
3838
Handler/AttachRequestHandler.cpp
3939
Handler/BreakpointLocationsHandler.cpp
40+
Handler/CancelRequestHandler.cpp
4041
Handler/CompileUnitsRequestHandler.cpp
4142
Handler/CompletionsHandler.cpp
4243
Handler/ConfigurationDoneRequestHandler.cpp

lldb/tools/lldb-dap/DAP.cpp

Lines changed: 151 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "LLDBUtils.h"
1515
#include "OutputRedirector.h"
1616
#include "Protocol/ProtocolBase.h"
17+
#include "Protocol/ProtocolRequests.h"
1718
#include "Protocol/ProtocolTypes.h"
1819
#include "Transport.h"
1920
#include "lldb/API/SBBreakpoint.h"
@@ -40,13 +41,17 @@
4041
#include <algorithm>
4142
#include <cassert>
4243
#include <chrono>
44+
#include <condition_variable>
4345
#include <cstdarg>
4446
#include <cstdio>
4547
#include <fstream>
48+
#include <future>
4649
#include <memory>
4750
#include <mutex>
51+
#include <optional>
4852
#include <string>
4953
#include <utility>
54+
#include <variant>
5055

5156
#if defined(_WIN32)
5257
#define NOMINMAX
@@ -58,6 +63,7 @@
5863
#endif
5964

6065
using namespace lldb_dap;
66+
using namespace lldb_dap::protocol;
6167

6268
namespace {
6369
#ifdef _WIN32
@@ -230,7 +236,7 @@ void DAP::StopEventHandlers() {
230236
void DAP::SendJSON(const llvm::json::Value &json) {
231237
// FIXME: Instead of parsing the output message from JSON, pass the `Message`
232238
// as parameter to `SendJSON`.
233-
protocol::Message message;
239+
Message message;
234240
llvm::json::Path::Root root;
235241
if (!fromJSON(json, message, root)) {
236242
DAP_LOG_ERROR(log, root.getError(), "({1}) encoding failed: {0}",
@@ -240,7 +246,27 @@ void DAP::SendJSON(const llvm::json::Value &json) {
240246
Send(message);
241247
}
242248

243-
void DAP::Send(const protocol::Message &message) {
249+
void DAP::Send(const Message &message) {
250+
// FIXME: After all the requests have migrated from LegacyRequestHandler >
251+
// RequestHandler<> this should be handled in RequestHandler<>::operator().
252+
if (auto *resp = std::get_if<Response>(&message);
253+
resp && debugger.InterruptRequested()) {
254+
// Clear the interrupt request.
255+
debugger.CancelInterruptRequest();
256+
257+
// If the debugger was interrupted, convert this response into a 'cancelled'
258+
// response because we might have a partial result.
259+
Response cancelled{/*request_seq=*/resp->request_seq,
260+
/*command=*/resp->command,
261+
/*success=*/false,
262+
/*message=*/eResponseMessageCancelled,
263+
/*body=*/std::nullopt};
264+
if (llvm::Error err = transport.Write(cancelled))
265+
DAP_LOG_ERROR(log, std::move(err), "({1}) write failed: {0}",
266+
transport.GetClientName());
267+
return;
268+
}
269+
244270
if (llvm::Error err = transport.Write(message))
245271
DAP_LOG_ERROR(log, std::move(err), "({1}) write failed: {0}",
246272
transport.GetClientName());
@@ -675,11 +701,25 @@ void DAP::SetTarget(const lldb::SBTarget target) {
675701
}
676702
}
677703

678-
bool DAP::HandleObject(const protocol::Message &M) {
679-
if (const auto *req = std::get_if<protocol::Request>(&M)) {
704+
bool DAP::HandleObject(const Message &M) {
705+
if (const auto *req = std::get_if<Request>(&M)) {
706+
{
707+
std::lock_guard<std::mutex> guard(m_active_request_mutex);
708+
m_active_request = req;
709+
710+
// Clear the interrupt request prior to invoking a handler.
711+
if (debugger.InterruptRequested())
712+
debugger.CancelInterruptRequest();
713+
}
714+
715+
auto cleanup = llvm::make_scope_exit([&]() {
716+
std::scoped_lock<std::mutex> active_request_lock(m_active_request_mutex);
717+
m_active_request = nullptr;
718+
});
719+
680720
auto handler_pos = request_handlers.find(req->command);
681721
if (handler_pos != request_handlers.end()) {
682-
(*handler_pos->second)(*req);
722+
handler_pos->second->Run(*req);
683723
return true; // Success
684724
}
685725

@@ -688,10 +728,10 @@ bool DAP::HandleObject(const protocol::Message &M) {
688728
return false; // Fail
689729
}
690730

691-
if (const auto *resp = std::get_if<protocol::Response>(&M)) {
731+
if (const auto *resp = std::get_if<Response>(&M)) {
692732
std::unique_ptr<ResponseHandler> response_handler;
693733
{
694-
std::lock_guard<std::mutex> locker(call_mutex);
734+
std::lock_guard<std::mutex> guard(call_mutex);
695735
auto inflight = inflight_reverse_requests.find(resp->request_seq);
696736
if (inflight != inflight_reverse_requests.end()) {
697737
response_handler = std::move(inflight->second);
@@ -782,28 +822,121 @@ llvm::Error DAP::Disconnect(bool terminateDebuggee) {
782822
return ToError(error);
783823
}
784824

825+
bool DAP::IsCancelled(const protocol::Request &req) {
826+
std::lock_guard<std::mutex> guard(m_cancelled_requests_mutex);
827+
return m_cancelled_requests.contains(req.seq);
828+
}
829+
830+
void DAP::ClearCancelRequest(const CancelArguments &args) {
831+
std::lock_guard<std::mutex> guard(m_cancelled_requests_mutex);
832+
if (args.requestId)
833+
m_cancelled_requests.erase(*args.requestId);
834+
}
835+
836+
template <typename T>
837+
static std::optional<T> getArgumentsIfRequest(const Message &pm,
838+
llvm::StringLiteral command) {
839+
auto *const req = std::get_if<Request>(&pm);
840+
if (!req || req->command != command)
841+
return std::nullopt;
842+
843+
T args;
844+
llvm::json::Path::Root root;
845+
if (!fromJSON(req->arguments, args, root)) {
846+
return std::nullopt;
847+
}
848+
849+
return std::move(args);
850+
}
851+
785852
llvm::Error DAP::Loop() {
786-
auto cleanup = llvm::make_scope_exit([this]() {
853+
std::future<llvm::Error> queue_reader =
854+
std::async(std::launch::async, [&]() -> llvm::Error {
855+
llvm::set_thread_name(transport.GetClientName() + ".transport_handler");
856+
auto cleanup = llvm::make_scope_exit([&]() {
857+
// Ensure we're marked as disconnecting when the reader exits.
858+
disconnecting = true;
859+
m_queue_cv.notify_all();
860+
});
861+
862+
while (!disconnecting) {
863+
llvm::Expected<Message> next =
864+
transport.Read(std::chrono::seconds(1));
865+
if (next.errorIsA<EndOfFileError>()) {
866+
consumeError(next.takeError());
867+
break;
868+
}
869+
870+
// If the read timed out, continue to check if we should disconnect.
871+
if (next.errorIsA<TimeoutError>()) {
872+
consumeError(next.takeError());
873+
continue;
874+
}
875+
876+
if (llvm::Error err = next.takeError())
877+
return err;
878+
879+
if (const protocol::Request *req =
880+
std::get_if<protocol::Request>(&*next);
881+
req && req->command == "disconnect") {
882+
disconnecting = true;
883+
}
884+
885+
const std::optional<CancelArguments> cancel_args =
886+
getArgumentsIfRequest<CancelArguments>(*next, "cancel");
887+
if (cancel_args) {
888+
{
889+
std::lock_guard<std::mutex> guard(m_cancelled_requests_mutex);
890+
if (cancel_args->requestId)
891+
m_cancelled_requests.insert(*cancel_args->requestId);
892+
}
893+
894+
// If a cancel is requested for the active request, make a best
895+
// effort attempt to interrupt.
896+
std::lock_guard<std::mutex> guard(m_active_request_mutex);
897+
if (m_active_request &&
898+
cancel_args->requestId == m_active_request->seq) {
899+
DAP_LOG(
900+
log,
901+
"({0}) interrupting inflight request (command={1} seq={2})",
902+
transport.GetClientName(), m_active_request->command,
903+
m_active_request->seq);
904+
debugger.RequestInterrupt();
905+
}
906+
}
907+
908+
{
909+
std::lock_guard<std::mutex> guard(m_queue_mutex);
910+
m_queue.push_back(std::move(*next));
911+
}
912+
m_queue_cv.notify_one();
913+
}
914+
915+
return llvm::Error::success();
916+
});
917+
918+
auto cleanup = llvm::make_scope_exit([&]() {
787919
out.Stop();
788920
err.Stop();
789921
StopEventHandlers();
790922
});
923+
791924
while (!disconnecting) {
792-
llvm::Expected<std::optional<protocol::Message>> next = transport.Read();
793-
if (!next)
794-
return next.takeError();
925+
std::unique_lock<std::mutex> lock(m_queue_mutex);
926+
m_queue_cv.wait(lock, [&] { return disconnecting || !m_queue.empty(); });
795927

796-
// nullopt on EOF
797-
if (!*next)
928+
if (m_queue.empty())
798929
break;
799930

800-
if (!HandleObject(**next)) {
931+
Message next = m_queue.front();
932+
m_queue.pop_front();
933+
934+
if (!HandleObject(next))
801935
return llvm::createStringError(llvm::inconvertibleErrorCode(),
802936
"unhandled packet");
803-
}
804937
}
805938

806-
return llvm::Error::success();
939+
return queue_reader.get();
807940
}
808941

809942
lldb::SBError DAP::WaitForProcessToStop(uint32_t seconds) {
@@ -1128,8 +1261,8 @@ lldb::SBValue Variables::FindVariable(uint64_t variablesReference,
11281261
}
11291262
}
11301263
} else {
1131-
// This is not under the globals or locals scope, so there are no duplicated
1132-
// names.
1264+
// This is not under the globals or locals scope, so there are no
1265+
// duplicated names.
11331266

11341267
// We have a named item within an actual variable so we need to find it
11351268
// withing the container variable by name.

0 commit comments

Comments
 (0)