[Lldb-commits] [lldb] [lldb-dap] End the session before sending the disconnect response (PR #227106)
Ebuka Ezike via lldb-commits
lldb-commits at lists.llvm.org
Tue Sep 29 06:00:19 PDT 2026
https://github.com/da-viper updated https://github.com/llvm/llvm-project/pull/227106
>From 1ce86aeef9a20d3ea420fc190ffed47aa55981ef Mon Sep 17 00:00:00 2001
From: Ebuka Ezike <yerimyah1 at gmail.com>
Date: Tue, 29 Sep 2026 13:57:48 +0100
Subject: [PATCH] [lldb-dap] End the session before sending the disconnect
response
Previously the `disconnect` request asks the debug adapter to disconnect from the
debuggee, ending the debug session, and then to shut down. lldb-dap
responded right after killing or detaching the process. So the `exited`
event, terminatedCommands and exitedCommands can be sent after
`disconnect` could reaches the client.
`DisconnectRequestHandler` now stores its response in
`DAP::on_session_end`. `DAP::Loop()` sends it once the session ended.
The teardown/cleanup is now:
- The event threads stop, after they report the exit of a killed
process.
- `terminated` Event is sent, if the event thread didn't send it.
- The transport thread stops.
- If there is a pending `configuration_done` we reply.
- The requests queued behind `disconnect` request are cancelled.
- The `disconnect` response is sent last.
- The debugger is destroyed.
OnError and OnClosed callback from the transport will not terminate the loop,
instead it updates the queue state to InputClosed allowing DAP::Loop to do
the above cleanup. Also because we use the MainLoop to forward outputs
from stdout and stderr.
`Disconnect()` no longer sends the terminated Event itself. It used to wait in
`SendTerminatedEvent()`'s `call_once` while holding the API mutex. The
event thread, inside that `call_once`, needs the API mutex to run the
`terminateCommands`. So a process exiting during a `disconnect` could
deadlock. See https://github.com/llvm/llvm-project/issues/194670
The test no longer closes the connection when we receive the disconnect. This was
a workaround so we exit early when waiting for an exited event that the session
may not receive. Ignore `shutdown()` errors when the adapter already closed the
socket.
Fixes #194670
---
.../lldbsuite/test/tools/lldb_dap/session.py | 3 -
.../lldbsuite/test/tools/lldb_dap/utils.py | 4 +-
.../tools/lldb-dap/cancel/TestDAP_cancel.py | 34 +++-
.../lldb-dap/disconnect/TestDAP_disconnect.py | 39 ++++-
.../TestDAP_launch_terminate_commands.py | 20 ++-
.../launch/TestDAP_launch_termination.py | 10 +-
lldb/tools/lldb-dap/DAP.cpp | 145 +++++++++++++-----
lldb/tools/lldb-dap/DAP.h | 27 +++-
lldb/tools/lldb-dap/Handler/RequestHandler.h | 27 +++-
lldb/unittests/DAP/Handler/DisconnectTest.cpp | 92 +++++++++--
lldb/unittests/DAP/TestBase.h | 36 +++++
11 files changed, 365 insertions(+), 72 deletions(-)
diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/session.py b/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/session.py
index 5d4852e1c5e1e..5709f78ec749b 100644
--- a/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/session.py
+++ b/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/session.py
@@ -22,7 +22,6 @@
CapabilitiesEvent,
ContinueArgs,
DAPError,
- DisconnectArgs,
ErrorResponse,
Event,
ExitedEvent,
@@ -285,8 +284,6 @@ def _on_protocol_response(self, message: RawMessage) -> None:
if raw_capabilities := message.get("body"):
init_capabilities = dict_to_message(Capabilities, raw_capabilities)
self._state.update_capabilities(init_capabilities)
- if command == DisconnectArgs.command_:
- self._connection.stop()
def _on_protocol_event(self, message: RawMessage) -> None:
self._logger.debug("<-- %s", json.dumps(message))
diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/utils.py b/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/utils.py
index 3251475173866..17a5f77e7d6bc 100644
--- a/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/utils.py
+++ b/lldb/packages/Python/lldbsuite/test/tools/lldb_dap/utils.py
@@ -832,7 +832,9 @@ def close(self):
self._writer.flush()
if self._socket.fileno() != -1:
- self._socket.shutdown(socket.SHUT_RDWR)
+ # Fails if the adapter has already closed the connection.
+ with contextlib.suppress(OSError):
+ self._socket.shutdown(socket.SHUT_RDWR)
self._writer.close()
self._socket.close()
diff --git a/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py b/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py
index f639d01f851b2..7dbc5c095f545 100644
--- a/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py
+++ b/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py
@@ -6,7 +6,7 @@
from lldbsuite.test.tools.lldb_dap import DAPTestCaseBase
from lldbsuite.test.tools.lldb_dap.session_helpers import DAPTestSession
-from lldbsuite.test.tools.lldb_dap.types import CancelArgs, EvaluateArgs, LaunchArgs
+from lldbsuite.test.tools.lldb_dap.types import *
class TestDAP_cancel(DAPTestCaseBase):
@@ -91,3 +91,35 @@ def test_inflight_request(self):
self.assertEqual(cancel_resp.command, "cancel")
self.assertEqual(cancel_resp.success, True)
session.continue_to_exit()
+
+ def test_request_after_disconnect(self):
+ """
+ Tests a request queued after 'disconnect' is cancelled, before the
+ 'disconnect' response.
+ """
+ program = self.getBuildArtifact("a.out")
+ busy_loop = self.getSourcePath("busy_loop.py")
+ session = self.build_and_create_session(disconnect_automatically=False)
+ process_event = session.launch(
+ LaunchArgs(
+ program,
+ initCommands=[f"command script import {busy_loop}"],
+ stopOnEntry=True,
+ )
+ )
+ session.verify_stopped_on_entry(after=process_event)
+
+ # Block the request handling thread, so both requests are queued.
+ blocking_handle = self.async_blocking_request(session, count=1)
+ disconnect_handle = session.send_request(DisconnectArgs(terminateDebuggee=True))
+ threads_handle = session.send_request(ThreadsArgs())
+
+ blocking_resp = blocking_handle.result()
+ self.assertEqual(blocking_resp.success, True)
+
+ threads_resp = threads_handle.error()
+ self.assertEqual(threads_resp.request_seq, threads_handle.seq)
+ self.assertEqual(threads_resp.message, "cancelled")
+
+ disconnect_resp = disconnect_handle.result()
+ self.assertLess(threads_resp.seq, disconnect_resp.seq)
diff --git a/lldb/test/API/tools/lldb-dap/disconnect/TestDAP_disconnect.py b/lldb/test/API/tools/lldb-dap/disconnect/TestDAP_disconnect.py
index 4c82f34b9232f..aab952f6579e8 100644
--- a/lldb/test/API/tools/lldb-dap/disconnect/TestDAP_disconnect.py
+++ b/lldb/test/API/tools/lldb-dap/disconnect/TestDAP_disconnect.py
@@ -39,8 +39,18 @@ def test_launch(self):
# Verify we haven't produced the side effect file yet.
self.assertFalse(os.path.exists(side_effect))
- session.disconnect(terminateDebuggee=True)
- session.wait_for_event(TerminatedEvent, after=stop_event)
+ # Verify we receive the exited and terminated event before the disconnect response.
+ response = session.disconnect(terminateDebuggee=True)
+ exited = session.wait_for_exited_event(after=stop_event)
+ self.assertNotEqual(exited.body.exitCode, 0)
+ terminated = session.wait_for_terminated_event(after=stop_event)
+ self.assertLess(terminated.seq, response.seq)
+ self.assertLess(exited.seq, response.seq)
+
+ with self.assertRaises(DAPError):
+ # We should not receive a second terminated event. The check doesn't
+ # stall, since the event history is closed.
+ session.wait_for_terminated_event(after=terminated)
# Verify we didn't produce the side effect file.
self.assertFalse(os.path.exists(side_effect))
@@ -65,7 +75,6 @@ def test_attach(self):
proc = self.spawnSubprocess(
program, [sync_file_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
- self.addTearDownHook(lambda: proc.kill())
lldbutil.wait_for_file_on_target(self, sync_file_path)
@@ -84,9 +93,31 @@ def test_attach(self):
wait_for_attach_var = top_frame.evaluate("wait_for_attach", context="hover")
self.assertEqual(wait_for_attach_var.result, "false")
- session.disconnect()
+ # Verify we receive the terminated event before the disconnect response.
+ disconnect_resp = session.disconnect()
+ possible_events = (ExitedEvent, TerminatedEvent, StoppedEvent)
+ end_event = session.wait_for_any_event(possible_events, after=stop_event)
+ self.assertIsInstance(end_event, TerminatedEvent)
+ self.assertLess(end_event.seq, disconnect_resp.seq)
# Wait for the process to run to completion.
proc.wait(timeout=10)
self.assertTrue(os.path.exists(side_effect))
+
+ def test_disconnect_after_exit(self):
+ """
+ Disconnecting after the process exited doesn't send another
+ "terminated" event.
+ """
+ program = self.getBuildArtifact("a.out")
+ session = self.build_and_create_session(disconnect_automatically=False)
+ process_event = session.launch(LaunchArgs(program))
+ session.verify_process_exited(after=process_event)
+ terminated = session.wait_for_terminated_event(after=process_event)
+
+ session.disconnect()
+ with self.assertRaises(DAPError):
+ # We should not receive a second terminated event. The check doesn't
+ # stall, since the event history is closed.
+ session.wait_for_terminated_event(after=terminated)
diff --git a/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch_terminate_commands.py b/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch_terminate_commands.py
index 6ad05f82af0d6..a41524b4b49f4 100644
--- a/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch_terminate_commands.py
+++ b/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch_terminate_commands.py
@@ -3,14 +3,14 @@
"""
from lldbsuite.test.decorators import *
-from lldbsuite.test.tools.lldb_dap.types import LaunchArgs
+from lldbsuite.test.tools.lldb_dap.types import ExitedEvent, LaunchArgs
from lldbsuite.test.tools.lldb_dap import DAPTestCaseBase
class TestDAP_launch_terminate_commands(DAPTestCaseBase):
"""
- Tests that the "terminateCommands", that can be passed during launch,
- are run when the debugger is disconnected.
+ Tests that the "exitCommands" and "terminateCommands", that can be passed
+ during launch, are run when the debugger is disconnected.
"""
@skipIfNetBSD # Hangs on NetBSD as well
@@ -19,18 +19,26 @@ def test(self):
program = self.getBuildArtifact("a.out")
session = self.build_and_create_session(disconnect_automatically=False)
+ exit_commands = ["version"]
terminate_commands = ["history"]
process_event = session.launch(
LaunchArgs(
program=program,
stopOnEntry=True,
+ exitCommands=exit_commands,
terminateCommands=terminate_commands,
)
)
stop_event = session.verify_stopped_on_entry(after=process_event)
- # Once it's disconnected the console should contain the "terminateCommands".
- session.disconnect(terminateDebuggee=True)
- output = session.collect_console(after=stop_event, until=terminate_commands[0])
+ response = session.disconnect(terminateDebuggee=True)
+
+ exited = session.wait_for_event(ExitedEvent, after=stop_event)
+ terminated = session.wait_for_terminated_event(after=stop_event)
+ self.assertLess(terminated.seq, response.seq)
+
+ output = session.collect_console(after=stop_event, until=exited)
+ session.verify_commands("exitCommands", output.seen_texts, exit_commands)
+ output = session.collect_console(after=stop_event, until=terminated)
session.verify_commands(
"terminateCommands", output.seen_texts, terminate_commands
)
diff --git a/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch_termination.py b/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch_termination.py
index 13962b8cd47c0..64f26e049729e 100644
--- a/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch_termination.py
+++ b/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch_termination.py
@@ -4,6 +4,7 @@
from lldbsuite.test.decorators import requireSocketPermission
from lldbsuite.test.tools.lldb_dap import DAPTestCaseBase
+from lldbsuite.test.tools.lldb_dap.types import DAPError
from lldbsuite.test.tools.lldb_dap.utils import DebugAdapter
@@ -34,11 +35,16 @@ def do_test_termination(self, adapter: DebugAdapter):
session.initialize_sequence(session.initialize_args)
# The lldb-dap process should finish even though
# we didn't close the communication socket explicitly.
- session.disconnect()
+ disconnect_resp = session.disconnect()
+
+ # No event comes after the 'disconnect' response, so the wait ends when
+ # lldb-dap closes the connection and the session stops reading.
+ with self.assertRaises(DAPError):
+ session.wait_for_terminated_event(after=disconnect_resp)
+ self.assertFalse(session.is_running(), f"expected ended session.")
# Wait until the underlying lldb-dap process dies.
adapter.process.wait(timeout=self.DEFAULT_TIMEOUT)
- self.assertFalse(session.is_running(), f"expected ended session.")
# Check the return code.
self.assertEqual(adapter.process.poll(), 0)
diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp
index dda56e87b68d1..5a816ec4ccbb9 100644
--- a/lldb/tools/lldb-dap/DAP.cpp
+++ b/lldb/tools/lldb-dap/DAP.cpp
@@ -787,6 +787,16 @@ void DAP::SendTerminatedEvent() {
llvm::Error DAP::Disconnect() { return Disconnect(!is_attach); }
llvm::Error DAP::Disconnect(bool terminateDebuggee) {
+ // Serializes with the request handlers, and with a call from
+ // `DAPSessionManager::DisconnectAllSessions()` on another thread.
+ lldb::SBMutex api_mutex = GetAPIMutex();
+ std::lock_guard<lldb::SBMutex> api_guard(api_mutex);
+ {
+ std::lock_guard<std::mutex> guard(m_queue_mutex);
+ if (m_queue_state == QueueState::Disconnected)
+ return llvm::Error::success();
+ }
+
lldb::SBError error;
lldb::SBProcess process = target.GetProcess();
auto state = process.GetState();
@@ -810,8 +820,11 @@ llvm::Error DAP::Disconnect(bool terminateDebuggee) {
}
}
- SendTerminatedEvent();
- TerminateLoop();
+ // Sending the terminated event is handled by `EndSession()`.
+ {
+ std::lock_guard<std::mutex> guard(m_queue_mutex);
+ SetQueueState(QueueState::Disconnected);
+ }
return ToError(error);
}
@@ -890,21 +903,25 @@ void DAP::OnClosed() {
void DAP::TerminateLoop(bool failed) {
std::lock_guard<std::mutex> guard(m_queue_mutex);
- if (m_disconnecting)
- return; // Already disconnecting.
+ if (m_queue_state != QueueState::Running)
+ return; // Already ending the session.
m_error_occurred = failed;
- m_disconnecting = true;
- m_loop.AddPendingCallback(
- [](MainLoopBase &loop) { loop.RequestTermination(); });
+ // Keep the main loop running: it still reads the debugger's output while the
+ // session ends. `EndSession()` stops it.
+ SetQueueState(QueueState::InputClosed);
+}
+
+void DAP::SetQueueState(QueueState state) {
+ if (state > m_queue_state)
+ m_queue_state = state;
+ m_queue_cv.notify_all();
}
void DAP::TransportHandler() {
llvm::scope_exit scope_guard([this] {
std::lock_guard<std::mutex> guard(m_queue_mutex);
- // Ensure we're marked as disconnecting when the reader exits.
- m_disconnecting = true;
- m_queue_cv.notify_all();
+ SetQueueState(QueueState::InputClosed);
});
if (llvm::Error err = transport.RegisterMessageHandler(*this)) {
@@ -923,29 +940,76 @@ void DAP::TransportHandler() {
}
}
-llvm::Error DAP::Loop() {
+void DAP::EndSession(std::thread &transport_thread) {
+ // The event thread handles the events it already received before it stops.
+ // So the exit of a killed process is reported before "terminated".
+ StopEventHandlers();
+ SendTerminatedEvent();
+
+ // Stop reading from the client. Don't wait to join the transport thread if
+ // our callback wasn't added successfully, or we'll wait forever.
+ if (m_loop.AddPendingCallback(
+ [](MainLoopBase &loop) { loop.RequestTermination(); })) {
+ transport_thread.join();
+ } else {
+ DAP_LOG(log,
+ "failed to terminate stop the main loop in {}. Detaching the "
+ "Transport Handler thread.",
+ GetClientName());
+ transport_thread.detach();
+ }
+
+ // We may have a pending configuration done callback.
+ if (on_configuration_done) {
+ on_configuration_done();
+ on_configuration_done = nullptr;
+ }
+
+ // Cancel the queued requests. Pass the responses to their reverse request
+ // handlers.
+ std::deque<Message> queue;
{
- // Reset disconnect flag once we start the loop.
std::lock_guard<std::mutex> guard(m_queue_mutex);
- m_disconnecting = false;
+ queue.swap(m_queue);
+ }
+ for (const Message &message : queue) {
+ if (const auto *request = std::get_if<Request>(&message)) {
+ Response cancelled{
+ /*request_seq=*/request->seq,
+ /*command=*/request->command,
+ /*success=*/false,
+ /*message=*/eResponseMessageCancelled,
+ };
+ Send(cancelled);
+ } else {
+ HandleObject(message);
+ }
}
- auto thread = std::thread([this] { TransportHandler(); });
+ // The 'disconnect' response is the last message.
+ if (on_session_end) {
+ on_session_end();
+ on_session_end = nullptr;
+ }
- llvm::scope_exit cleanup([this]() {
- StopEventHandlers();
+ // Destroy the debugger when the session ends. This will trigger the
+ // debugger's destroy callbacks for earlier logging and clean-ups, rather
+ // than waiting for the termination of the lldb-dap process.
+ lldb::SBDebugger::Destroy(debugger);
+}
- // Destroy the debugger when the session ends. This will trigger the
- // debugger's destroy callbacks for earlier logging and clean-ups, rather
- // than waiting for the termination of the lldb-dap process.
- lldb::SBDebugger::Destroy(debugger);
- });
+llvm::Error DAP::Loop() {
+ auto transport_thread = std::thread([this] { TransportHandler(); });
+ bool unhandled_packet = false;
while (true) {
std::unique_lock<std::mutex> lock(m_queue_mutex);
- m_queue_cv.wait(lock, [&] { return m_disconnecting || !m_queue.empty(); });
+ m_queue_cv.wait(lock, [&] {
+ return m_queue_state != QueueState::Running || !m_queue.empty();
+ });
- if (m_disconnecting && m_queue.empty())
+ // Once the session ends, `EndSession()` cancels the queued requests.
+ if (m_queue_state != QueueState::Running)
break;
Message next = m_queue.front();
@@ -954,27 +1018,28 @@ llvm::Error DAP::Loop() {
// Unlock while we're processing the event.
lock.unlock();
- if (!HandleObject(next))
- return llvm::createStringError(llvm::inconvertibleErrorCode(),
- "unhandled packet");
+ if (!HandleObject(next)) {
+ unhandled_packet = true;
+ break;
+ }
}
- // Don't wait to join the mainloop thread if our callback wasn't added
- // successfully, or we'll wait forever.
- if (m_loop.AddPendingCallback(
- [](MainLoopBase &loop) { loop.RequestTermination(); })) {
- thread.join();
- } else {
- DAP_LOG(log,
- "failed to terminate stop the main loop in {}. Detaching the "
- "Transport Handler thread.",
- GetClientName());
- thread.detach();
- }
+ // A 'cancel' request may have interrupted the debugger. Clear it to run the
+ // "terminateCommands".
+ if (debugger.InterruptRequested())
+ debugger.CancelInterruptRequest();
+
+ // If the session didn't end with a 'disconnect' (the connection closed or the
+ // transport failed), end it the same way, with the default arguments.
+ if (llvm::Error error = Disconnect())
+ DAP_LOG_ERROR(log, std::move(error), "disconnect failed: {0}");
+
+ EndSession(transport_thread);
+ if (unhandled_packet)
+ return llvm::createStringError("unhandled packet");
if (m_error_occurred)
- return llvm::createStringError(llvm::inconvertibleErrorCode(),
- "DAP Loop terminated due to an internal "
+ return llvm::createStringError("DAP Loop terminated due to an internal "
"error, see DAP Logs for more information.");
return llvm::Error::success();
}
diff --git a/lldb/tools/lldb-dap/DAP.h b/lldb/tools/lldb-dap/DAP.h
index 99261860d5f1f..2d9729dbf06e5 100644
--- a/lldb/tools/lldb-dap/DAP.h
+++ b/lldb/tools/lldb-dap/DAP.h
@@ -148,6 +148,10 @@ struct DAP final : public DAPTransport::MessageHandler {
lldb::SBFormat thread_format;
llvm::unique_function<void()> on_configuration_done;
+ /// Called after the session ends, to send the 'disconnect' response as the
+ /// last message to the client.
+ llvm::unique_function<void()> on_session_end;
+
/// This is used to allow request_evaluate to handle empty expressions
/// (ie the user pressed 'return' and expects the previous expression to
/// repeat). If the previous expression was a command, it will be empty.
@@ -338,6 +342,9 @@ struct DAP final : public DAPTransport::MessageHandler {
llvm::Error Disconnect();
/// Disconnect the DAP session and optionally terminate the debuggee.
+ ///
+ /// The session ends once the current request handler returns, see `Loop()`.
+ /// Does nothing if the session is already disconnected.
llvm::Error Disconnect(bool terminateDebuggee);
/// Send a "terminated" event to indicate the process is done being debugged.
@@ -465,6 +472,24 @@ struct DAP final : public DAPTransport::MessageHandler {
void TransportHandler();
void TerminateLoop(bool failed = false);
+ /// What `Loop()` does with the queued messages. It only moves forward.
+ enum class QueueState {
+ Running,
+ /// The client closed the connection, or the transport failed.
+ InputClosed,
+ /// `Disconnect()` ended the session and we killed or detached the debuggee.
+ Disconnected,
+ };
+
+ /// Moves the queue to `state`. Only update the state if it is lesser than the
+ /// current state. Used in `Loop()` and requires `m_queue_mutex`.
+ void SetQueueState(QueueState state);
+
+ /// Ends the session and does cleanup.
+ /// Reports the end of the debuggee, stops reading from the
+ /// client, answers the queued requests, then calls `on_session_end`.
+ void EndSession(std::thread &transport_thread);
+
/// Registration of request handler.
/// @{
void RegisterRequests();
@@ -494,7 +519,7 @@ struct DAP final : public DAPTransport::MessageHandler {
std::deque<protocol::Message> m_queue;
std::mutex m_queue_mutex;
std::condition_variable m_queue_cv;
- bool m_disconnecting = false;
+ QueueState m_queue_state = QueueState::Running;
bool m_error_occurred = false;
// Loop for managing reading from the client.
diff --git a/lldb/tools/lldb-dap/Handler/RequestHandler.h b/lldb/tools/lldb-dap/Handler/RequestHandler.h
index e99b287b28f58..a182e6dc5ebdd 100644
--- a/lldb/tools/lldb-dap/Handler/RequestHandler.h
+++ b/lldb/tools/lldb-dap/Handler/RequestHandler.h
@@ -275,17 +275,34 @@ class ConfigurationDoneRequestHandler
void PostRun() const override;
};
-class DisconnectRequestHandler
- : public RequestHandler<std::optional<protocol::DisconnectArguments>,
- protocol::DisconnectResponse> {
+/// The response to 'disconnect' is sent once the session ended, as the last
+/// message to the client. See `DAP::Loop`.
+class DisconnectRequestHandler : public BaseRequestHandler {
public:
- using RequestHandler::RequestHandler;
+ using BaseRequestHandler::BaseRequestHandler;
static llvm::StringLiteral GetCommand() { return "disconnect"; }
FeatureSet GetSupportedFeatures() const override {
return {protocol::eAdapterFeatureTerminateDebuggee};
}
llvm::Error
- Run(const std::optional<protocol::DisconnectArguments> &args) const override;
+ Run(const std::optional<protocol::DisconnectArguments> &args) const;
+
+private:
+ void operator()(const protocol::Request &request) const override {
+ protocol::Response response;
+ response.request_seq = request.seq;
+ response.command = request.command;
+
+ llvm::Expected<std::optional<protocol::DisconnectArguments>> arguments =
+ parseArgs<std::optional<protocol::DisconnectArguments>>(request);
+ if (llvm::Error err = arguments.takeError())
+ return SendError(std::move(err), response);
+
+ BuildErrorResponse(Run(*arguments), response);
+ dap.on_session_end = [this, response = std::move(response)]() mutable {
+ Send(response);
+ };
+ }
};
class EvaluateRequestHandler
diff --git a/lldb/unittests/DAP/Handler/DisconnectTest.cpp b/lldb/unittests/DAP/Handler/DisconnectTest.cpp
index b4147b5b0552d..26eaac7d225bb 100644
--- a/lldb/unittests/DAP/Handler/DisconnectTest.cpp
+++ b/lldb/unittests/DAP/Handler/DisconnectTest.cpp
@@ -11,6 +11,7 @@
#include "Protocol/ProtocolBase.h"
#include "TestBase.h"
#include "lldb/API/SBDefines.h"
+#include "lldb/Host/JSONTransport.h"
#include "lldb/lldb-enumerations.h"
#include "llvm/Testing/Support/Error.h"
#include "gmock/gmock.h"
@@ -25,12 +26,82 @@ using namespace lldb_dap_tests;
using namespace lldb_dap::protocol;
using testing::_;
-class DisconnectRequestHandlerTest : public DAPTestBase {};
+class DisconnectRequestHandlerTest : public DAPTestBase {
+protected:
+ void QueueDisconnect() {
+ dap->Received(Request{"disconnect", std::nullopt, /*seq=*/1});
+ }
+
+ void QueueThreads(Id seq) {
+ dap->Received(Request{"threads", std::nullopt, seq});
+ }
+
+ /// Handles the queued messages until the session ends, then delivers the
+ /// messages sent after the transport thread stopped.
+ void RunSession() {
+ ASSERT_THAT_ERROR(dap->Loop(), Succeeded());
+ Run();
+ }
+};
TEST_F(DisconnectRequestHandlerTest, DisconnectTriggersTerminated) {
- DisconnectRequestHandler handler(*dap);
- ASSERT_THAT_ERROR(handler.Run(std::nullopt), Succeeded());
+ QueueDisconnect();
+
+ testing::InSequence sequence;
+ EXPECT_CALL(client, Received(IsEvent("terminated", _)));
+ EXPECT_CALL(client,
+ Received(SuccessResponse(HasSeq(1), HasCommand("disconnect"))));
+ RunSession();
+}
+
+TEST_F(DisconnectRequestHandlerTest, DisconnectCancelsQueuedRequests) {
+ QueueDisconnect();
+ QueueThreads(2);
+
+ testing::InSequence sequence;
+ EXPECT_CALL(client, Received(IsEvent("terminated", _)));
+ EXPECT_CALL(client,
+ Received(CancelledResponse(HasSeq(2), HasCommand("threads"))));
+ EXPECT_CALL(client,
+ Received(SuccessResponse(HasSeq(1), HasCommand("disconnect"))));
+ RunSession();
+}
+
+TEST_F(DisconnectRequestHandlerTest, ClosedConnectionCancelsQueuedRequests) {
+ QueueThreads(1);
+ dap->OnClosed();
+
+ // The session ends now, without a response.
+ testing::InSequence sequence;
EXPECT_CALL(client, Received(IsEvent("terminated", _)));
+ EXPECT_CALL(client,
+ Received(CancelledResponse(HasSeq(1), HasCommand("threads"))));
+ RunSession();
+}
+
+TEST_F(DisconnectRequestHandlerTest, ClosedConnectionCancelsQueuedDisconnect) {
+ QueueDisconnect();
+ QueueThreads(2);
+ dap->OnClosed();
+
+ testing::InSequence sequence;
+ EXPECT_CALL(client, Received(IsEvent("terminated", _)));
+ EXPECT_CALL(client,
+ Received(CancelledResponse(HasSeq(1), HasCommand("disconnect"))));
+ EXPECT_CALL(client,
+ Received(CancelledResponse(HasSeq(2), HasCommand("threads"))));
+ RunSession();
+}
+
+TEST_F(DisconnectRequestHandlerTest, TransportErrorEndsSession) {
+ QueueThreads(1);
+ dap->OnError(llvm::createStringError("read failed"));
+
+ testing::InSequence sequence;
+ EXPECT_CALL(client, Received(IsEvent("terminated", _)));
+ EXPECT_CALL(client,
+ Received(CancelledResponse(HasSeq(1), HasCommand("threads"))));
+ EXPECT_THAT_ERROR(dap->Loop(), Failed());
Run();
}
@@ -44,17 +115,20 @@ TEST_F(DisconnectRequestHandlerTest, DisconnectTriggersTerminateCommands) {
LoadCore();
- DisconnectRequestHandler handler(*dap);
-
dap->configuration.terminateCommands = {"?script print(1)",
"script print(2)"};
EXPECT_EQ(dap->target.GetProcess().GetState(), lldb::eStateStopped);
- ASSERT_THAT_ERROR(handler.Run(std::nullopt), Succeeded());
- EXPECT_CALL(client, Received(Output("1\n")));
- EXPECT_CALL(client, Received(Output("2\n"))).Times(2);
+ QueueDisconnect();
+
+ EXPECT_CALL(client, Received(Output("1\n"))).Times(testing::AtMost(1));
+ // Python print can also writes to the debugger's output file.
+ EXPECT_CALL(client, Received(Output("2\n"))).Times(testing::AtLeast(1));
+
EXPECT_CALL(client, Received(Output("(lldb) script print(2)\n")));
EXPECT_CALL(client, Received(Output("Running terminateCommands:\n")));
EXPECT_CALL(client, Received(IsEvent("terminated", _)));
- Run();
+ EXPECT_CALL(client,
+ Received(SuccessResponse(HasSeq(1), HasCommand("disconnect"))));
+ RunSession();
}
#endif
diff --git a/lldb/unittests/DAP/TestBase.h b/lldb/unittests/DAP/TestBase.h
index 7953e0fabfbe5..768a38db522d9 100644
--- a/lldb/unittests/DAP/TestBase.h
+++ b/lldb/unittests/DAP/TestBase.h
@@ -98,6 +98,42 @@ inline auto Output(llvm::StringRef o, llvm::StringRef cat = "console") {
llvm::json::Object{{"category", cat}, {"output", o}})));
}
+/// Matches a response to the request with sequence number `seq`.
+MATCHER_P(HasSeq, seq, "request_seq is " + testing::PrintToString(seq)) {
+ if (seq <= 0)
+ return false;
+ return arg.request_seq == (lldb_dap::protocol::Id)seq;
+}
+
+/// Matches a response to `command`.
+MATCHER_P(HasCommand, command,
+ "command is " + testing::PrintToString(command)) {
+ return arg.command == command;
+}
+
+/// Matches a successful response that also matches all of `fields`.
+template <typename... Fields>
+inline testing::Matcher<const lldb_dap::protocol::Response &>
+SuccessResponse(Fields &&...fields) {
+ return testing::AllOf(
+ testing::Field("success", &lldb_dap::protocol::Response::success, true),
+ std::forward<Fields>(fields)...);
+}
+
+/// Matches a 'cancelled' response that also matches all of `fields`.
+template <typename... Fields>
+inline testing::Matcher<const lldb_dap::protocol::Response &>
+CancelledResponse(Fields &&...fields) {
+ return testing::AllOf(
+ testing::Field("success", &lldb_dap::protocol::Response::success, false),
+ testing::Field(
+ "message", &lldb_dap::protocol::Response::message,
+ testing::Optional(
+ testing::VariantWith<lldb_dap::protocol::ResponseMessage>(
+ lldb_dap::protocol::eResponseMessageCancelled))),
+ std::forward<Fields>(fields)...);
+}
+
/// A base class for tests that interact with a `lldb_dap::DAP` instance.
class DAPTestBase : public TransportBase {
protected:
More information about the lldb-commits
mailing list