mirror of
https://gitlab.kitware.com/cmake/cmake.git
synced 2026-09-25 04:09:36 +03:00
debugger: fix infinite loop when debugger pipe is closed
When a DAP client closes the pipe (or crashes), getPayload() returns an empty functor without triggering the onError handler, so SessionActive is never set to false and the session thread spins in a tight loop at 100% CPU. Handle the empty-payload case with the same cleanup performed by the onError and DisconnectRequest handlers. testProtocolWithPipesAbruptDisconnect drives the DAP handshake and then closes the client side of the pipe without sending a DisconnectRequest. The test deliberately avoids ReportExitCode so that no concurrent write triggers the dap::Session error handler and masks the busy-loop condition; if the SessionThread spins on EOF, the adapter destructor blocks in SessionThread.join() and the test fails on a 10-second timeout. Fixes: #27743
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include <cm3p/cppdap/future.h>
|
||||
#include <cm3p/cppdap/io.h>
|
||||
@@ -173,7 +174,121 @@ bool testProtocolWithPipes()
|
||||
return true;
|
||||
}
|
||||
|
||||
bool testProtocolWithPipesAbruptDisconnect()
|
||||
{
|
||||
std::promise<void> debuggerConnectionCreatedPromise;
|
||||
std::future<void> debuggerConnectionCreatedFuture =
|
||||
debuggerConnectionCreatedPromise.get_future();
|
||||
|
||||
std::future<void> startedListeningFuture;
|
||||
|
||||
std::promise<void> adapterFinishedPromise;
|
||||
std::future<void> adapterFinishedFuture =
|
||||
adapterFinishedPromise.get_future();
|
||||
|
||||
std::promise<void> pipeClosedPromise;
|
||||
std::future<void> pipeClosedFuture = pipeClosedPromise.get_future();
|
||||
|
||||
std::promise<bool> initializedEventReceivedPromise;
|
||||
std::future<bool> 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<dap::Session> 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<cmDebugger::cmDebuggerPipeConnection>(namedPipe);
|
||||
startedListeningFuture = connection->StartedListening.get_future();
|
||||
debuggerConnectionCreatedPromise.set_value();
|
||||
std::shared_ptr<cmDebugger::cmDebuggerAdapter> debuggerAdapter =
|
||||
std::make_shared<cmDebugger::cmDebuggerAdapter>(
|
||||
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<cmDebugger::cmDebuggerPipeClient>(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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user