diff --git a/Source/cmDebuggerAdapter.cxx b/Source/cmDebuggerAdapter.cxx index 7cf9efe45b..7dfb3346ba 100644 --- a/Source/cmDebuggerAdapter.cxx +++ b/Source/cmDebuggerAdapter.cxx @@ -312,6 +312,14 @@ cmDebuggerAdapter::cmDebuggerAdapter( while (SessionActive.load()) { if (auto payload = Session->getPayload()) { payload(); + } else { + // Connection closed or unrecoverable error. + BreakpointManager->ClearAll(); + ExceptionManager->ClearAll(); + ClearStepRequests(); + ContinueSem->Notify(); + DisconnectEvent->Fire(); + SessionActive.store(false); } } }); diff --git a/Tests/CMakeLib/testDebuggerAdapterPipe.cxx b/Tests/CMakeLib/testDebuggerAdapterPipe.cxx index d7c41341da..92ea047e74 100644 --- a/Tests/CMakeLib/testDebuggerAdapterPipe.cxx +++ b/Tests/CMakeLib/testDebuggerAdapterPipe.cxx @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -173,7 +174,121 @@ bool testProtocolWithPipes() return true; } +bool testProtocolWithPipesAbruptDisconnect() +{ + std::promise debuggerConnectionCreatedPromise; + std::future debuggerConnectionCreatedFuture = + debuggerConnectionCreatedPromise.get_future(); + + std::future startedListeningFuture; + + std::promise adapterFinishedPromise; + std::future adapterFinishedFuture = + adapterFinishedPromise.get_future(); + + std::promise pipeClosedPromise; + std::future pipeClosedFuture = pipeClosedPromise.get_future(); + + std::promise initializedEventReceivedPromise; + std::future initializedEventReceivedFuture = + initializedEventReceivedPromise.get_future(); + + auto futureTimeout = std::chrono::seconds(60); + auto disconnectTimeout = std::chrono::seconds(10); + +#ifdef _WIN32 + std::string namedPipe = R"(\\.\pipe\LOCAL\CMakeDebuggerPipe3_)" + + cmCryptoHash(cmCryptoHash::AlgoSHA256) + .HashString(cmsys::SystemTools::GetCurrentWorkingDirectory()); +#else + std::string namedPipe = "CMakeDebuggerPipe3"; +#endif + + std::unique_ptr client = dap::Session::create(); + client->registerHandler([&](dap::InitializedEvent /*unused*/) { + initializedEventReceivedPromise.set_value(true); + }); + + // Raw thread (not ScopedThread): we need to be able to detach on + // failure so the test process can exit even if the bug is present. + // + // Note: we deliberately do NOT call ReportExitCode() here. With the + // bug present, an attempted write to a closed pipe would trigger the + // dap::Session error handler, which masks the busy-loop condition we + // are trying to test for. Instead we let the adapter destructor join + // the SessionThread directly: if SessionThread is spinning on EOF the + // join will hang, the adapterFinishedFuture wait below will time out, + // and the test will fail. + std::thread debuggerThread([&]() { + try { + auto connection = + std::make_shared(namedPipe); + startedListeningFuture = connection->StartedListening.get_future(); + debuggerConnectionCreatedPromise.set_value(); + std::shared_ptr debuggerAdapter = + std::make_shared( + connection, dap::file(stdout, false)); + // Hold the adapter until the test signals that it has closed the + // client side of the pipe. + pipeClosedFuture.wait(); + // Adapter destructed here; joins SessionThread. + } catch (std::runtime_error const&) { + // Swallowed: connection failures shouldn't hang the test. + } + adapterFinishedPromise.set_value(); + }); + + ASSERT_TRUE(debuggerConnectionCreatedFuture.wait_for(futureTimeout) == + std::future_status::ready); + ASSERT_TRUE(startedListeningFuture.wait_for(futureTimeout) == + std::future_status::ready); + + auto client2Debugger = + std::make_shared(namedPipe); + client2Debugger->WaitForConnection(); + client->bind(client2Debugger, client2Debugger); + + // Drive the full handshake so that the debugger SessionThread is up + // and blocked reading the pipe. + dap::CMakeInitializeRequest initializeRequest; + auto initializeResponse = client->send(initializeRequest).get(); + ASSERT_TRUE(!initializeResponse.error); + + dap::LaunchRequest launchRequest; + auto launchResponse = client->send(launchRequest).get(); + ASSERT_TRUE(!launchResponse.error); + + dap::ConfigurationDoneRequest configurationDoneRequest; + auto configurationDoneResponse = + client->send(configurationDoneRequest).get(); + ASSERT_TRUE(!configurationDoneResponse.error); + + ASSERT_TRUE(initializedEventReceivedFuture.wait_for(futureTimeout) == + std::future_status::ready); + + // Abruptly close the client side without sending DisconnectRequest. + // Regression check for the busy-loop bug: the debugger adapter must + // detect EOF on the pipe and shut down on its own. + client2Debugger->close(); + pipeClosedPromise.set_value(); + + bool finishedInTime = adapterFinishedFuture.wait_for(disconnectTimeout) == + std::future_status::ready; + if (!finishedInTime) { + // Bug reproduced: the SessionThread is spinning on EOF and the + // adapter destructor is blocked in SessionThread.join(). Detach so + // the test process can exit instead of hanging in std::thread's + // destructor. + debuggerThread.detach(); + ASSERT_TRUE(finishedInTime); + } + debuggerThread.join(); + + return true; +} + int testDebuggerAdapterPipe(int, char*[]) { - return runTests({ testProtocolWithPipes }); + return runTests( + { testProtocolWithPipes, testProtocolWithPipesAbruptDisconnect }); }