[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