mirror of
https://gitlab.kitware.com/cmake/cmake.git
synced 2026-09-25 04:09:36 +03:00
The POSIX pipe read wrappers stored the return of ::read() into an unsigned result, so a negative count (e.g. EBADF from a concurrent close on another thread) became SIZE_MAX. ContentReader::buffer() would then try to grow its deque by ~18 exabytes and crash with either std::length_error (glibc) or a stack smash (libc++). Treat any non-positive ::read() return as EOF/error, close the pipe, and return 0 so the peer's SessionThread observes a clean empty payload. cmDebuggerPipeClient is test-only infrastructure; production cmake is always the pipe server. Production close() assumes sequential access, which holds in normal use (the adapter destructor joins SessionThread before closing the connection). The new abrupt- disconnect test however needs to wake a sibling thread blocked in read() on the same fd, which Linux ::close() does not do. Add a ShutdownForTesting() method that calls shutdown(SHUT_RDWR) without freeing the fd, so any concurrent blocking read wakes with a clean zero-length return. On Windows, CloseHandle already cancels pending overlapped I/O, so the helper just forwards to close(). Use it from testProtocolWithPipesAbruptDisconnect in place of close().
331 lines
12 KiB
C++
331 lines
12 KiB
C++
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
|
file LICENSE.rst or https://cmake.org/licensing for details. */
|
|
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <future>
|
|
#include <memory>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
#include <cm3p/cppdap/future.h>
|
|
#include <cm3p/cppdap/io.h>
|
|
#include <cm3p/cppdap/optional.h>
|
|
#include <cm3p/cppdap/protocol.h>
|
|
#include <cm3p/cppdap/session.h>
|
|
#include <cm3p/cppdap/types.h>
|
|
|
|
#include "cmDebuggerAdapter.h"
|
|
#include "cmDebuggerProtocol.h"
|
|
#include "cmVersionConfig.h"
|
|
|
|
#ifdef _WIN32
|
|
# include "cmsys/SystemTools.hxx"
|
|
|
|
# include "cmCryptoHash.h"
|
|
# include "cmDebuggerWindowsPipeConnection.h"
|
|
#else
|
|
# include "cmDebuggerPosixPipeConnection.h"
|
|
#endif
|
|
|
|
#include "testCommon.h"
|
|
#include "testDebugger.h"
|
|
|
|
bool testProtocolWithPipes()
|
|
{
|
|
std::promise<void> debuggerConnectionCreatedPromise;
|
|
std::future<void> debuggerConnectionCreatedFuture =
|
|
debuggerConnectionCreatedPromise.get_future();
|
|
|
|
std::future<void> startedListeningFuture;
|
|
|
|
std::promise<bool> debuggerAdapterInitializedPromise;
|
|
std::future<bool> debuggerAdapterInitializedFuture =
|
|
debuggerAdapterInitializedPromise.get_future();
|
|
|
|
std::promise<bool> initializedEventReceivedPromise;
|
|
std::future<bool> initializedEventReceivedFuture =
|
|
initializedEventReceivedPromise.get_future();
|
|
|
|
std::promise<bool> exitedEventReceivedPromise;
|
|
std::future<bool> exitedEventReceivedFuture =
|
|
exitedEventReceivedPromise.get_future();
|
|
|
|
std::promise<bool> terminatedEventReceivedPromise;
|
|
std::future<bool> terminatedEventReceivedFuture =
|
|
terminatedEventReceivedPromise.get_future();
|
|
|
|
std::promise<bool> threadStartedPromise;
|
|
std::future<bool> threadStartedFuture = threadStartedPromise.get_future();
|
|
|
|
std::promise<bool> threadExitedPromise;
|
|
std::future<bool> threadExitedFuture = threadExitedPromise.get_future();
|
|
|
|
std::promise<bool> disconnectResponseReceivedPromise;
|
|
std::future<bool> disconnectResponseReceivedFuture =
|
|
disconnectResponseReceivedPromise.get_future();
|
|
|
|
auto futureTimeout = std::chrono::seconds(60);
|
|
|
|
#ifdef _WIN32
|
|
std::string namedPipe = R"(\\.\pipe\LOCAL\CMakeDebuggerPipe2_)" +
|
|
cmCryptoHash(cmCryptoHash::AlgoSHA256)
|
|
.HashString(cmsys::SystemTools::GetCurrentWorkingDirectory());
|
|
#else
|
|
std::string namedPipe = "CMakeDebuggerPipe2";
|
|
#endif
|
|
|
|
std::unique_ptr<dap::Session> client = dap::Session::create();
|
|
client->registerHandler([&](dap::InitializedEvent /*unused*/) {
|
|
initializedEventReceivedPromise.set_value(true);
|
|
});
|
|
client->registerHandler([&](dap::ExitedEvent /*unused*/) {
|
|
exitedEventReceivedPromise.set_value(true);
|
|
});
|
|
client->registerHandler([&](dap::TerminatedEvent const& /*unused*/) {
|
|
terminatedEventReceivedPromise.set_value(true);
|
|
});
|
|
client->registerHandler([&](dap::ThreadEvent const& e) {
|
|
if (e.reason == "started") {
|
|
threadStartedPromise.set_value(true);
|
|
} else if (e.reason == "exited") {
|
|
threadExitedPromise.set_value(true);
|
|
}
|
|
});
|
|
|
|
ScopedThread debuggerThread([&]() -> int {
|
|
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));
|
|
|
|
debuggerAdapterInitializedPromise.set_value(true);
|
|
debuggerAdapter->ReportExitCode(0);
|
|
|
|
// Ensure the disconnectResponse has been received before
|
|
// destructing debuggerAdapter.
|
|
ASSERT_TRUE(disconnectResponseReceivedFuture.wait_for(futureTimeout) ==
|
|
std::future_status::ready);
|
|
return 0;
|
|
} catch (std::runtime_error const& error) {
|
|
std::cerr << "Error: Failed to create debugger adapter.\n";
|
|
std::cerr << error.what() << "\n";
|
|
return -1;
|
|
}
|
|
});
|
|
|
|
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);
|
|
|
|
dap::CMakeInitializeRequest initializeRequest;
|
|
auto response = client->send(initializeRequest);
|
|
auto initializeResponse = response.get();
|
|
ASSERT_TRUE(!initializeResponse.error);
|
|
ASSERT_TRUE(initializeResponse.response.cmakeVersion.full == CMake_VERSION);
|
|
ASSERT_TRUE(initializeResponse.response.cmakeVersion.major ==
|
|
CMake_VERSION_MAJOR);
|
|
ASSERT_TRUE(initializeResponse.response.cmakeVersion.minor ==
|
|
CMake_VERSION_MINOR);
|
|
ASSERT_TRUE(initializeResponse.response.cmakeVersion.patch ==
|
|
CMake_VERSION_PATCH);
|
|
ASSERT_TRUE(initializeResponse.response.supportsExceptionInfoRequest);
|
|
ASSERT_TRUE(
|
|
initializeResponse.response.exceptionBreakpointFilters.has_value());
|
|
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(debuggerAdapterInitializedFuture.wait_for(futureTimeout) ==
|
|
std::future_status::ready);
|
|
ASSERT_TRUE(initializedEventReceivedFuture.wait_for(futureTimeout) ==
|
|
std::future_status::ready);
|
|
ASSERT_TRUE(terminatedEventReceivedFuture.wait_for(futureTimeout) ==
|
|
std::future_status::ready);
|
|
ASSERT_TRUE(threadStartedFuture.wait_for(futureTimeout) ==
|
|
std::future_status::ready);
|
|
ASSERT_TRUE(threadExitedFuture.wait_for(futureTimeout) ==
|
|
std::future_status::ready);
|
|
ASSERT_TRUE(exitedEventReceivedFuture.wait_for(futureTimeout) ==
|
|
std::future_status::ready);
|
|
|
|
dap::DisconnectRequest disconnectRequest;
|
|
auto disconnectResponse = client->send(disconnectRequest).get();
|
|
disconnectResponseReceivedPromise.set_value(true);
|
|
ASSERT_TRUE(!disconnectResponse.error);
|
|
|
|
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<bool> initializedEventReceivedPromise;
|
|
std::future<bool> initializedEventReceivedFuture =
|
|
initializedEventReceivedPromise.get_future();
|
|
|
|
std::promise<bool> exitedEventReceivedPromise;
|
|
std::future<bool> exitedEventReceivedFuture =
|
|
exitedEventReceivedPromise.get_future();
|
|
|
|
std::promise<bool> terminatedEventReceivedPromise;
|
|
std::future<bool> terminatedEventReceivedFuture =
|
|
terminatedEventReceivedPromise.get_future();
|
|
|
|
std::promise<bool> threadStartedPromise;
|
|
std::future<bool> threadStartedFuture = threadStartedPromise.get_future();
|
|
|
|
std::promise<bool> threadExitedPromise;
|
|
std::future<bool> threadExitedFuture = threadExitedPromise.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);
|
|
});
|
|
client->registerHandler([&](dap::ExitedEvent /*unused*/) {
|
|
exitedEventReceivedPromise.set_value(true);
|
|
});
|
|
client->registerHandler([&](dap::TerminatedEvent const& /*unused*/) {
|
|
terminatedEventReceivedPromise.set_value(true);
|
|
});
|
|
client->registerHandler([&](dap::ThreadEvent const& e) {
|
|
if (e.reason == "started") {
|
|
threadStartedPromise.set_value(true);
|
|
} else if (e.reason == "exited") {
|
|
threadExitedPromise.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 regression
|
|
// returns. With the fix in place, ReportExitCode()'s wait on
|
|
// DisconnectEvent unblocks when the SessionThread detects EOF on the
|
|
// pipe; without the fix, it blocks forever and the adapter
|
|
// destructor hangs in SessionThread.join().
|
|
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));
|
|
// Sends thread-exited / exited / terminated events and then
|
|
// blocks on DisconnectEvent. The test closes the client side of
|
|
// the pipe instead of sending a DisconnectRequest, so the only
|
|
// thing that will unblock this wait is the SessionThread's EOF
|
|
// handling in cmDebuggerAdapter.
|
|
debuggerAdapter->ReportExitCode(0);
|
|
// 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 ReportExitCode is blocked on DisconnectEvent.
|
|
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);
|
|
ASSERT_TRUE(terminatedEventReceivedFuture.wait_for(futureTimeout) ==
|
|
std::future_status::ready);
|
|
ASSERT_TRUE(threadStartedFuture.wait_for(futureTimeout) ==
|
|
std::future_status::ready);
|
|
ASSERT_TRUE(threadExitedFuture.wait_for(futureTimeout) ==
|
|
std::future_status::ready);
|
|
ASSERT_TRUE(exitedEventReceivedFuture.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.
|
|
//
|
|
// Use ShutdownForTesting() rather than close(). The client dap::Session
|
|
// has its own recvThread still blocked in read() on this same socket;
|
|
// on Linux, ::close() on an fd does not wake a sibling thread's
|
|
// in-flight ::read(), which would deadlock the test. ShutdownForTesting
|
|
// calls shutdown(SHUT_RDWR) to signal EOF on the socket endpoint
|
|
// without freeing the fd, so both reads wake up naturally.
|
|
client2Debugger->ShutdownForTesting();
|
|
|
|
bool finishedInTime = adapterFinishedFuture.wait_for(disconnectTimeout) ==
|
|
std::future_status::ready;
|
|
if (!finishedInTime) {
|
|
// Bug reproduced: the SessionThread is spinning on EOF and
|
|
// ReportExitCode is blocked on DisconnectEvent. 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, testProtocolWithPipesAbruptDisconnect });
|
|
}
|