From a9a592f96e6498da302f8e968be1db0ad3c32123 Mon Sep 17 00:00:00 2001 From: Glen Chung Date: Wed, 15 Mar 2023 17:50:08 -0700 Subject: cmake: Add debugger - Depends on cppdap and jsoncpp. - Add --debugger argument to enable the Debugger. - Add --debugger-pipe argument for DAP traffics over named pipes. - Support breakpoints by filenames and line numbers. - Support exception breakpoints. - Call stack shows filenames and line numbers. - Show Cache Variables. - Show the state of currently defined targets, tests and directories with their properties. - Add cmakeVersion to DAP initialize response. - Include unit tests. Co-authored-by: Ben McMorran --- CMakeLists.txt | 14 +- Help/manual/cmake.1.rst | 53 ++ Help/release/dev/cmake-debugger.rst | 5 + Source/CMakeLists.txt | 32 + Source/Modules/CMakeBuildUtilities.cmake | 2 +- Source/cmConfigure.cmake.h.in | 1 + Source/cmDebuggerAdapter.cxx | 462 +++++++++++++++ Source/cmDebuggerAdapter.h | 93 +++ Source/cmDebuggerBreakpointManager.cxx | 200 +++++++ Source/cmDebuggerBreakpointManager.h | 61 ++ Source/cmDebuggerExceptionManager.cxx | 129 +++++ Source/cmDebuggerExceptionManager.h | 70 +++ Source/cmDebuggerPipeConnection.cxx | 293 ++++++++++ Source/cmDebuggerPipeConnection.h | 139 +++++ Source/cmDebuggerProtocol.cxx | 80 +++ Source/cmDebuggerProtocol.h | 191 ++++++ Source/cmDebuggerSourceBreakpoint.cxx | 14 + Source/cmDebuggerSourceBreakpoint.h | 26 + Source/cmDebuggerStackFrame.cxx | 28 + Source/cmDebuggerStackFrame.h | 33 ++ Source/cmDebuggerThread.cxx | 150 +++++ Source/cmDebuggerThread.h | 59 ++ Source/cmDebuggerThreadManager.cxx | 47 ++ Source/cmDebuggerThreadManager.h | 38 ++ Source/cmDebuggerVariables.cxx | 133 +++++ Source/cmDebuggerVariables.h | 124 ++++ Source/cmDebuggerVariablesHelper.cxx | 644 +++++++++++++++++++++ Source/cmDebuggerVariablesHelper.h | 106 ++++ Source/cmDebuggerVariablesManager.cxx | 38 ++ Source/cmDebuggerVariablesManager.h | 40 ++ Source/cmMakefile.cxx | 95 +++ Source/cmMessageCommand.cxx | 11 + Source/cmMessenger.cxx | 10 + Source/cmMessenger.h | 17 + Source/cmake.cxx | 115 +++- Source/cmake.h | 31 + Source/cmakemain.cxx | 6 + Tests/CMakeLib/CMakeLists.txt | 25 + Tests/CMakeLib/DebuggerSample/CMakeLists.txt | 9 + Tests/CMakeLib/DebuggerSample/script.cmake | 1 + Tests/CMakeLib/testCommon.h | 30 + Tests/CMakeLib/testDebugger.h | 99 ++++ Tests/CMakeLib/testDebuggerAdapter.cxx | 173 ++++++ Tests/CMakeLib/testDebuggerAdapterPipe.cxx | 184 ++++++ Tests/CMakeLib/testDebuggerBreakpointManager.cxx | 172 ++++++ Tests/CMakeLib/testDebuggerNamedPipe.cxx | 218 +++++++ Tests/CMakeLib/testDebuggerVariables.cxx | 185 ++++++ Tests/CMakeLib/testDebuggerVariablesHelper.cxx | 587 +++++++++++++++++++ Tests/CMakeLib/testDebuggerVariablesManager.cxx | 50 ++ .../DebuggerArgMissingDapLog-result.txt | 1 + .../DebuggerArgMissingDapLog-stderr.txt | 2 + .../CommandLine/DebuggerArgMissingDapLog.cmake | 1 + .../CommandLine/DebuggerArgMissingPipe-result.txt | 1 + .../CommandLine/DebuggerArgMissingPipe-stderr.txt | 2 + .../CommandLine/DebuggerArgMissingPipe.cmake | 1 + .../DebuggerCapabilityInspect-check.cmake | 5 + .../CommandLine/DebuggerNotSupported-result.txt | 1 + .../CommandLine/DebuggerNotSupported-stderr.txt | 2 + .../CommandLine/DebuggerNotSupported.cmake | 1 + .../DebuggerNotSupportedDapLog-result.txt | 1 + .../DebuggerNotSupportedDapLog-stderr.txt | 2 + .../CommandLine/DebuggerNotSupportedDapLog.cmake | 1 + .../DebuggerNotSupportedPipe-result.txt | 1 + .../DebuggerNotSupportedPipe-stderr.txt | 2 + .../CommandLine/DebuggerNotSupportedPipe.cmake | 1 + .../RunCMake/CommandLine/E_capabilities-stdout.txt | 2 +- Tests/RunCMake/CommandLine/RunCMakeTest.cmake | 11 + Utilities/IWYU/mapping.imp | 2 + bootstrap | 11 + 69 files changed, 5364 insertions(+), 10 deletions(-) create mode 100644 Help/release/dev/cmake-debugger.rst create mode 100644 Source/cmDebuggerAdapter.cxx create mode 100644 Source/cmDebuggerAdapter.h create mode 100644 Source/cmDebuggerBreakpointManager.cxx create mode 100644 Source/cmDebuggerBreakpointManager.h create mode 100644 Source/cmDebuggerExceptionManager.cxx create mode 100644 Source/cmDebuggerExceptionManager.h create mode 100644 Source/cmDebuggerPipeConnection.cxx create mode 100644 Source/cmDebuggerPipeConnection.h create mode 100644 Source/cmDebuggerProtocol.cxx create mode 100644 Source/cmDebuggerProtocol.h create mode 100644 Source/cmDebuggerSourceBreakpoint.cxx create mode 100644 Source/cmDebuggerSourceBreakpoint.h create mode 100644 Source/cmDebuggerStackFrame.cxx create mode 100644 Source/cmDebuggerStackFrame.h create mode 100644 Source/cmDebuggerThread.cxx create mode 100644 Source/cmDebuggerThread.h create mode 100644 Source/cmDebuggerThreadManager.cxx create mode 100644 Source/cmDebuggerThreadManager.h create mode 100644 Source/cmDebuggerVariables.cxx create mode 100644 Source/cmDebuggerVariables.h create mode 100644 Source/cmDebuggerVariablesHelper.cxx create mode 100644 Source/cmDebuggerVariablesHelper.h create mode 100644 Source/cmDebuggerVariablesManager.cxx create mode 100644 Source/cmDebuggerVariablesManager.h create mode 100644 Tests/CMakeLib/DebuggerSample/CMakeLists.txt create mode 100644 Tests/CMakeLib/DebuggerSample/script.cmake create mode 100644 Tests/CMakeLib/testCommon.h create mode 100644 Tests/CMakeLib/testDebugger.h create mode 100644 Tests/CMakeLib/testDebuggerAdapter.cxx create mode 100644 Tests/CMakeLib/testDebuggerAdapterPipe.cxx create mode 100644 Tests/CMakeLib/testDebuggerBreakpointManager.cxx create mode 100644 Tests/CMakeLib/testDebuggerNamedPipe.cxx create mode 100644 Tests/CMakeLib/testDebuggerVariables.cxx create mode 100644 Tests/CMakeLib/testDebuggerVariablesHelper.cxx create mode 100644 Tests/CMakeLib/testDebuggerVariablesManager.cxx create mode 100644 Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog-result.txt create mode 100644 Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog-stderr.txt create mode 100644 Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog.cmake create mode 100644 Tests/RunCMake/CommandLine/DebuggerArgMissingPipe-result.txt create mode 100644 Tests/RunCMake/CommandLine/DebuggerArgMissingPipe-stderr.txt create mode 100644 Tests/RunCMake/CommandLine/DebuggerArgMissingPipe.cmake create mode 100644 Tests/RunCMake/CommandLine/DebuggerCapabilityInspect-check.cmake create mode 100644 Tests/RunCMake/CommandLine/DebuggerNotSupported-result.txt create mode 100644 Tests/RunCMake/CommandLine/DebuggerNotSupported-stderr.txt create mode 100644 Tests/RunCMake/CommandLine/DebuggerNotSupported.cmake create mode 100644 Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog-result.txt create mode 100644 Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog-stderr.txt create mode 100644 Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog.cmake create mode 100644 Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe-result.txt create mode 100644 Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe-stderr.txt create mode 100644 Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ec6267..d559c08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -131,21 +131,21 @@ if(CMake_BUILD_LTO) endif() endif() -# Check whether to build cppdap. +# Check whether to build support for the debugger mode. if(NOT CMake_TEST_EXTERNAL_CMAKE) - if(NOT DEFINED CMake_ENABLE_CPPDAP) - # cppdap does not compile everywhere. + if(NOT DEFINED CMake_ENABLE_DEBUGGER) + # The debugger uses cppdap, which does not compile everywhere. if(CMAKE_SYSTEM_NAME MATCHES "Windows|Darwin|Linux|BSD|DragonFly|CYGWIN|MSYS" AND NOT (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 19.16) AND NOT (CMAKE_CXX_COMPILER_ID STREQUAL "XLClang" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16.1) ) - set(CMake_ENABLE_CPPDAP 1) + set(CMake_ENABLE_DEBUGGER 1) else() - set(CMake_ENABLE_CPPDAP 0) + set(CMake_ENABLE_DEBUGGER 0) endif() endif() else() - set(CMake_ENABLE_CPPDAP 0) + set(CMake_ENABLE_DEBUGGER 0) endif() #----------------------------------------------------------------------- @@ -186,7 +186,7 @@ macro(CMAKE_HANDLE_SYSTEM_LIBRARIES) # Optionally use system utility libraries. option(CMAKE_USE_SYSTEM_LIBARCHIVE "Use system-installed libarchive" "${CMAKE_USE_SYSTEM_LIBRARY_LIBARCHIVE}") - if(CMake_ENABLE_CPPDAP) + if(CMake_ENABLE_DEBUGGER) option(CMAKE_USE_SYSTEM_CPPDAP "Use system-installed cppdap" "${CMAKE_USE_SYSTEM_LIBRARY_CPPDAP}") endif() option(CMAKE_USE_SYSTEM_CURL "Use system-installed curl" "${CMAKE_USE_SYSTEM_LIBRARY_CURL}") diff --git a/Help/manual/cmake.1.rst b/Help/manual/cmake.1.rst index 1ea7626..b5848f7 100644 --- a/Help/manual/cmake.1.rst +++ b/Help/manual/cmake.1.rst @@ -517,6 +517,53 @@ Options If ```` is omitted, ``configure`` is assumed. The current working directory must contain CMake preset files. +.. option:: --debugger + + Enables interactive debugging of the CMake language. CMake exposes a debugging + interface on the pipe named by :option:`--debugger-pipe ` + that conforms to the `Debug Adapter Protocol`_ specification with the following + modifications. + + The ``initialize`` response includes an additional field named ``cmakeVersion`` + which specifies the version of CMake being debugged. + + .. code-block:: json + :caption: Debugger initialize response + + { + "cmakeVersion": { + "major": 3, + "minor": 27, + "patch": 0, + "full": "3.27.0" + } + } + + The members are: + + ``major`` + An integer specifying the major version number. + + ``minor`` + An integer specifying the minor version number. + + ``patch`` + An integer specifying the patch version number. + + ``full`` + A string specifying the full CMake version. + +.. _`Debug Adapter Protocol`: https://microsoft.github.io/debug-adapter-protocol/ + +.. option:: --debugger-pipe , --debugger-pipe= + + Name of the pipe (on Windows) or domain socket (on Unix) to use for + debugger communication. + +.. option:: --debugger-dap-log , --debugger-dap-log= + + Logs all debugger communication to the specified file. + .. _`Build Tool Mode`: Build a Project @@ -809,6 +856,12 @@ Available commands are: ``true`` if TLS support is enabled and ``false`` otherwise. + ``debugger`` + .. versionadded:: 3.27 + + ``true`` if the :option:`--debugger ` mode + is supported and ``false`` otherwise. + .. option:: cat [--] ... .. versionadded:: 3.18 diff --git a/Help/release/dev/cmake-debugger.rst b/Help/release/dev/cmake-debugger.rst new file mode 100644 index 0000000..bfc4f6c --- /dev/null +++ b/Help/release/dev/cmake-debugger.rst @@ -0,0 +1,5 @@ +cmake-debugger +-------------- + +* :manual:`cmake(1)` now supports interactive debugging of the CMake language. + See the :option:`--debugger ` option. diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 2354f3d..bcaf890 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -762,6 +762,38 @@ target_link_libraries( ZLIB::ZLIB ) +if(CMake_ENABLE_DEBUGGER) + target_sources( + CMakeLib + PRIVATE + cmDebuggerAdapter.cxx + cmDebuggerAdapter.h + cmDebuggerBreakpointManager.cxx + cmDebuggerBreakpointManager.h + cmDebuggerExceptionManager.cxx + cmDebuggerExceptionManager.h + cmDebuggerPipeConnection.cxx + cmDebuggerPipeConnection.h + cmDebuggerProtocol.cxx + cmDebuggerProtocol.h + cmDebuggerSourceBreakpoint.cxx + cmDebuggerSourceBreakpoint.h + cmDebuggerStackFrame.cxx + cmDebuggerStackFrame.h + cmDebuggerThread.cxx + cmDebuggerThread.h + cmDebuggerThreadManager.cxx + cmDebuggerThreadManager.h + cmDebuggerVariables.cxx + cmDebuggerVariables.h + cmDebuggerVariablesHelper.cxx + cmDebuggerVariablesHelper.h + cmDebuggerVariablesManager.cxx + cmDebuggerVariablesManager.h + ) + target_link_libraries(CMakeLib PUBLIC cppdap::cppdap) +endif() + # Check if we can build the Mach-O parser. if(CMake_USE_MACH_PARSER) target_sources( diff --git a/Source/Modules/CMakeBuildUtilities.cmake b/Source/Modules/CMakeBuildUtilities.cmake index 7d1e7da..c891fe9 100644 --- a/Source/Modules/CMakeBuildUtilities.cmake +++ b/Source/Modules/CMakeBuildUtilities.cmake @@ -379,7 +379,7 @@ endif() #--------------------------------------------------------------------- # Build cppdap library. -if(CMake_ENABLE_CPPDAP) +if(CMake_ENABLE_DEBUGGER) if(CMAKE_USE_SYSTEM_CPPDAP) find_package(cppdap CONFIG) if(NOT cppdap_FOUND) diff --git a/Source/cmConfigure.cmake.h.in b/Source/cmConfigure.cmake.h.in index 3f19a11..de74716 100644 --- a/Source/cmConfigure.cmake.h.in +++ b/Source/cmConfigure.cmake.h.in @@ -20,6 +20,7 @@ #cmakedefine HAVE_ENVIRON_NOT_REQUIRE_PROTOTYPE #cmakedefine HAVE_UNSETENV +#cmakedefine CMake_ENABLE_DEBUGGER #cmakedefine CMake_USE_MACH_PARSER #cmakedefine CMake_USE_XCOFF_PARSER #cmakedefine CMAKE_USE_WMAKE diff --git a/Source/cmDebuggerAdapter.cxx b/Source/cmDebuggerAdapter.cxx new file mode 100644 index 0000000..d03f79d --- /dev/null +++ b/Source/cmDebuggerAdapter.cxx @@ -0,0 +1,462 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ + +#include "cmConfigure.h" // IWYU pragma: keep + +#include "cmDebuggerAdapter.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include // IWYU pragma: keep +#include +#include + +#include "cmDebuggerBreakpointManager.h" +#include "cmDebuggerExceptionManager.h" +#include "cmDebuggerProtocol.h" +#include "cmDebuggerSourceBreakpoint.h" // IWYU pragma: keep +#include "cmDebuggerStackFrame.h" +#include "cmDebuggerThread.h" +#include "cmDebuggerThreadManager.h" +#include "cmListFileCache.h" +#include "cmMakefile.h" +#include "cmValue.h" +#include "cmVersionConfig.h" +#include +#include + +namespace cmDebugger { + +// Event provides a basic wait and signal synchronization primitive. +class SyncEvent +{ +public: + // Wait() blocks until the event is fired. + void Wait() + { + std::unique_lock lock(Mutex); + Cv.wait(lock, [&] { return Fired; }); + } + + // Fire() sets signals the event, and unblocks any calls to Wait(). + void Fire() + { + std::unique_lock lock(Mutex); + Fired = true; + Cv.notify_all(); + } + +private: + std::mutex Mutex; + std::condition_variable Cv; + bool Fired = false; +}; + +class Semaphore +{ +public: + Semaphore(int count_ = 0) + : Count(count_) + { + } + + inline void Notify() + { + std::unique_lock lock(Mutex); + Count++; + // notify the waiting thread + Cv.notify_one(); + } + + inline void Wait() + { + std::unique_lock lock(Mutex); + while (Count == 0) { + // wait on the mutex until notify is called + Cv.wait(lock); + } + Count--; + } + +private: + std::mutex Mutex; + std::condition_variable Cv; + int Count; +}; + +cmDebuggerAdapter::cmDebuggerAdapter( + std::shared_ptr connection, + std::string const& dapLogPath) + : cmDebuggerAdapter(std::move(connection), + dapLogPath.empty() + ? cm::nullopt + : cm::optional>( + dap::file(dapLogPath.c_str()))) +{ +} + +cmDebuggerAdapter::cmDebuggerAdapter( + std::shared_ptr connection, + cm::optional> logger) + : Connection(std::move(connection)) + , SessionActive(true) + , DisconnectEvent(cm::make_unique()) + , ConfigurationDoneEvent(cm::make_unique()) + , ContinueSem(cm::make_unique()) + , ThreadManager(cm::make_unique()) +{ + if (logger.has_value()) { + SessionLog = std::move(logger.value()); + } + ClearStepRequests(); + + Session = dap::Session::create(); + BreakpointManager = + cm::make_unique(Session.get()); + ExceptionManager = + cm::make_unique(Session.get()); + + // Handle errors reported by the Session. These errors include protocol + // parsing errors and receiving messages with no handler. + Session->onError([this](const char* msg) { + if (SessionLog) { + dap::writef(SessionLog, "dap::Session error: %s\n", msg); + } + + std::cout << "[CMake Debugger] DAP session error: " << msg << std::endl; + + BreakpointManager->ClearAll(); + ExceptionManager->ClearAll(); + ClearStepRequests(); + ContinueSem->Notify(); + DisconnectEvent->Fire(); + SessionActive.store(false); + }); + + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize + Session->registerHandler([this](const dap::CMakeInitializeRequest& req) { + SupportsVariableType = req.supportsVariableType.value(false); + dap::CMakeInitializeResponse response; + response.supportsConfigurationDoneRequest = true; + response.cmakeVersion.major = CMake_VERSION_MAJOR; + response.cmakeVersion.minor = CMake_VERSION_MINOR; + response.cmakeVersion.patch = CMake_VERSION_PATCH; + response.cmakeVersion.full = CMake_VERSION; + ExceptionManager->HandleInitializeRequest(response); + return response; + }); + + // https://microsoft.github.io/debug-adapter-protocol/specification#Events_Initialized + Session->registerSentHandler( + [&](const dap::ResponseOrError&) { + Session->send(dap::InitializedEvent()); + }); + + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Threads + Session->registerHandler([this](const dap::ThreadsRequest& req) { + (void)req; + std::unique_lock lock(Mutex); + dap::ThreadsResponse response; + dap::Thread thread; + thread.id = DefaultThread->GetId(); + thread.name = DefaultThread->GetName(); + response.threads.push_back(thread); + return response; + }); + + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StackTrace + Session->registerHandler([this](const dap::StackTraceRequest& request) + -> dap::ResponseOrError { + std::unique_lock lock(Mutex); + + cm::optional response = + ThreadManager->GetThreadStackTraceResponse(request.threadId); + if (response.has_value()) { + return response.value(); + } + + return dap::Error("Unknown threadId '%d'", int(request.threadId)); + }); + + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Scopes + Session->registerHandler([this](const dap::ScopesRequest& request) + -> dap::ResponseOrError { + std::unique_lock lock(Mutex); + return DefaultThread->GetScopesResponse(request.frameId, + SupportsVariableType); + }); + + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Variables + Session->registerHandler([this](const dap::VariablesRequest& request) + -> dap::ResponseOrError { + return DefaultThread->GetVariablesResponse(request); + }); + + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Pause + Session->registerHandler([this](const dap::PauseRequest& req) { + (void)req; + PauseRequest.store(true); + return dap::PauseResponse(); + }); + + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Continue + Session->registerHandler([this](const dap::ContinueRequest& req) { + (void)req; + ContinueSem->Notify(); + return dap::ContinueResponse(); + }); + + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Next + Session->registerHandler([this](const dap::NextRequest& req) { + (void)req; + NextStepFrom.store(DefaultThread->GetStackFrameSize()); + ContinueSem->Notify(); + return dap::NextResponse(); + }); + + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepIn + Session->registerHandler([this](const dap::StepInRequest& req) { + (void)req; + // This would stop after stepped in, single line stepped or stepped out. + StepInRequest.store(true); + ContinueSem->Notify(); + return dap::StepInResponse(); + }); + + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepOut + Session->registerHandler([this](const dap::StepOutRequest& req) { + (void)req; + StepOutDepth.store(DefaultThread->GetStackFrameSize() - 1); + ContinueSem->Notify(); + return dap::StepOutResponse(); + }); + + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Launch + Session->registerHandler([](const dap::LaunchRequest& req) { + (void)req; + return dap::LaunchResponse(); + }); + + // Handler for disconnect requests + Session->registerHandler([this](const dap::DisconnectRequest& request) { + (void)request; + BreakpointManager->ClearAll(); + ExceptionManager->ClearAll(); + ClearStepRequests(); + ContinueSem->Notify(); + DisconnectEvent->Fire(); + SessionActive.store(false); + return dap::DisconnectResponse(); + }); + + Session->registerHandler([this](const dap::EvaluateRequest& request) { + dap::EvaluateResponse response; + if (request.frameId.has_value()) { + std::shared_ptr frame = + DefaultThread->GetStackFrame(request.frameId.value()); + + auto var = frame->GetMakefile()->GetDefinition(request.expression); + if (var) { + response.type = "string"; + response.result = var; + return response; + } + } + + return response; + }); + + // The ConfigurationDone request is made by the client once all configuration + // requests have been made. + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_ConfigurationDone + Session->registerHandler([this](const dap::ConfigurationDoneRequest& req) { + (void)req; + ConfigurationDoneEvent->Fire(); + return dap::ConfigurationDoneResponse(); + }); + + std::string errorMessage; + if (!Connection->StartListening(errorMessage)) { + throw std::runtime_error(errorMessage); + } + + // Connect to the client. Write a well-known message to stdout so that + // clients know it is safe to attempt to connect. + std::cout << "Waiting for debugger client to connect..." << std::endl; + Connection->WaitForConnection(); + std::cout << "Debugger client connected." << std::endl; + + if (SessionLog) { + Session->connect(spy(Connection->GetReader(), SessionLog), + spy(Connection->GetWriter(), SessionLog)); + } else { + Session->connect(Connection->GetReader(), Connection->GetWriter()); + } + + // Start the processing thread. + SessionThread = std::thread([this] { + while (SessionActive.load()) { + if (auto payload = Session->getPayload()) { + payload(); + } + } + }); + + ConfigurationDoneEvent->Wait(); + + DefaultThread = ThreadManager->StartThread("CMake script"); + dap::ThreadEvent threadEvent; + threadEvent.reason = "started"; + threadEvent.threadId = DefaultThread->GetId(); + Session->send(threadEvent); +} + +cmDebuggerAdapter::~cmDebuggerAdapter() +{ + if (SessionThread.joinable()) { + SessionThread.join(); + } + + Session.reset(nullptr); + + if (SessionLog) { + SessionLog->close(); + } +} + +void cmDebuggerAdapter::ReportExitCode(int exitCode) +{ + ThreadManager->EndThread(DefaultThread); + dap::ThreadEvent threadEvent; + threadEvent.reason = "exited"; + threadEvent.threadId = DefaultThread->GetId(); + DefaultThread.reset(); + + dap::ExitedEvent exitEvent; + exitEvent.exitCode = exitCode; + + dap::TerminatedEvent terminatedEvent; + + if (SessionActive.load()) { + Session->send(threadEvent); + Session->send(exitEvent); + Session->send(terminatedEvent); + } + + // Wait until disconnected or error. + DisconnectEvent->Wait(); +} + +void cmDebuggerAdapter::OnFileParsedSuccessfully( + std::string const& sourcePath, + std::vector const& functions) +{ + BreakpointManager->SourceFileLoaded(sourcePath, functions); +} + +void cmDebuggerAdapter::OnBeginFunctionCall(cmMakefile* mf, + std::string const& sourcePath, + cmListFileFunction const& lff) +{ + std::unique_lock lock(Mutex); + DefaultThread->PushStackFrame(mf, sourcePath, lff); + + if (lff.Line() == 0) { + // File just loaded, continue to first valid function call. + return; + } + + auto hits = BreakpointManager->GetBreakpoints(sourcePath, lff.Line()); + lock.unlock(); + + bool waitSem = false; + dap::StoppedEvent stoppedEvent; + stoppedEvent.allThreadsStopped = true; + stoppedEvent.threadId = DefaultThread->GetId(); + if (!hits.empty()) { + ClearStepRequests(); + waitSem = true; + + dap::array hitBreakpoints; + hitBreakpoints.resize(hits.size()); + std::transform(hits.begin(), hits.end(), hitBreakpoints.begin(), + [&](const int64_t& id) { return dap::integer(id); }); + stoppedEvent.reason = "breakpoint"; + stoppedEvent.hitBreakpointIds = hitBreakpoints; + } + + if (long(DefaultThread->GetStackFrameSize()) <= NextStepFrom.load() || + StepInRequest.load() || + long(DefaultThread->GetStackFrameSize()) <= StepOutDepth.load()) { + ClearStepRequests(); + waitSem = true; + + stoppedEvent.reason = "step"; + } + + if (PauseRequest.load()) { + ClearStepRequests(); + waitSem = true; + + stoppedEvent.reason = "pause"; + } + + if (waitSem) { + Session->send(stoppedEvent); + ContinueSem->Wait(); + } +} + +void cmDebuggerAdapter::OnEndFunctionCall() +{ + DefaultThread->PopStackFrame(); +} + +static std::shared_ptr listFileFunction; + +void cmDebuggerAdapter::OnBeginFileParse(cmMakefile* mf, + std::string const& sourcePath) +{ + std::unique_lock lock(Mutex); + + listFileFunction = std::make_shared( + sourcePath, 0, 0, std::vector()); + DefaultThread->PushStackFrame(mf, sourcePath, *listFileFunction); +} + +void cmDebuggerAdapter::OnEndFileParse() +{ + DefaultThread->PopStackFrame(); + listFileFunction = nullptr; +} + +void cmDebuggerAdapter::OnMessageOutput(MessageType t, std::string const& text) +{ + cm::optional stoppedEvent = + ExceptionManager->RaiseExceptionIfAny(t, text); + if (stoppedEvent.has_value()) { + stoppedEvent->threadId = DefaultThread->GetId(); + Session->send(*stoppedEvent); + ContinueSem->Wait(); + } +} + +void cmDebuggerAdapter::ClearStepRequests() +{ + NextStepFrom.store(INT_MIN); + StepInRequest.store(false); + StepOutDepth.store(INT_MIN); + PauseRequest.store(false); +} + +} // namespace cmDebugger diff --git a/Source/cmDebuggerAdapter.h b/Source/cmDebuggerAdapter.h new file mode 100644 index 0000000..f261d88 --- /dev/null +++ b/Source/cmDebuggerAdapter.h @@ -0,0 +1,93 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include // IWYU pragma: keep + +#include "cmMessageType.h" + +class cmListFileFunction; +class cmMakefile; + +namespace cmDebugger { +class Semaphore; +class SyncEvent; +class cmDebuggerBreakpointManager; +class cmDebuggerExceptionManager; +class cmDebuggerThread; +class cmDebuggerThreadManager; +} + +namespace dap { +class Session; +} + +namespace cmDebugger { + +class cmDebuggerConnection +{ +public: + virtual ~cmDebuggerConnection() = default; + virtual bool StartListening(std::string& errorMessage) = 0; + virtual void WaitForConnection() = 0; + virtual std::shared_ptr GetReader() = 0; + virtual std::shared_ptr GetWriter() = 0; +}; + +class cmDebuggerAdapter +{ +public: + cmDebuggerAdapter(std::shared_ptr connection, + std::string const& dapLogPath); + cmDebuggerAdapter(std::shared_ptr connection, + cm::optional> logger); + ~cmDebuggerAdapter(); + + void ReportExitCode(int exitCode); + + void OnFileParsedSuccessfully( + std::string const& sourcePath, + std::vector const& functions); + void OnBeginFunctionCall(cmMakefile* mf, std::string const& sourcePath, + cmListFileFunction const& lff); + void OnEndFunctionCall(); + void OnBeginFileParse(cmMakefile* mf, std::string const& sourcePath); + void OnEndFileParse(); + + void OnMessageOutput(MessageType t, std::string const& text); + +private: + void ClearStepRequests(); + std::shared_ptr Connection; + std::unique_ptr Session; + std::shared_ptr SessionLog; + std::thread SessionThread; + std::atomic SessionActive; + std::mutex Mutex; + std::unique_ptr DisconnectEvent; + std::unique_ptr ConfigurationDoneEvent; + std::unique_ptr ContinueSem; + std::atomic NextStepFrom; + std::atomic StepInRequest; + std::atomic StepOutDepth; + std::atomic PauseRequest; + std::unique_ptr ThreadManager; + std::shared_ptr DefaultThread; + std::unique_ptr BreakpointManager; + std::unique_ptr ExceptionManager; + bool SupportsVariableType; +}; + +} // namespace cmDebugger diff --git a/Source/cmDebuggerBreakpointManager.cxx b/Source/cmDebuggerBreakpointManager.cxx new file mode 100644 index 0000000..152f0f5 --- /dev/null +++ b/Source/cmDebuggerBreakpointManager.cxx @@ -0,0 +1,200 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#include "cmDebuggerBreakpointManager.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include "cmDebuggerSourceBreakpoint.h" +#include "cmListFileCache.h" +#include "cmSystemTools.h" + +namespace cmDebugger { + +cmDebuggerBreakpointManager::cmDebuggerBreakpointManager( + dap::Session* dapSession) + : DapSession(dapSession) +{ + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetBreakpoints + DapSession->registerHandler([&](const dap::SetBreakpointsRequest& request) { + return HandleSetBreakpointsRequest(request); + }); +} + +int64_t cmDebuggerBreakpointManager::FindFunctionStartLine( + std::string const& sourcePath, int64_t line) +{ + auto location = + find_if(ListFileFunctionLines[sourcePath].begin(), + ListFileFunctionLines[sourcePath].end(), + [=](cmDebuggerFunctionLocation const& loc) { + return loc.StartLine <= line && loc.EndLine >= line; + }); + + if (location != ListFileFunctionLines[sourcePath].end()) { + return location->StartLine; + } + + return 0; +} + +int64_t cmDebuggerBreakpointManager::CalibrateBreakpointLine( + std::string const& sourcePath, int64_t line) +{ + auto location = find_if(ListFileFunctionLines[sourcePath].begin(), + ListFileFunctionLines[sourcePath].end(), + [=](cmDebuggerFunctionLocation const& loc) { + return loc.StartLine >= line; + }); + + if (location != ListFileFunctionLines[sourcePath].end()) { + return location->StartLine; + } + + if (!ListFileFunctionLines[sourcePath].empty() && + ListFileFunctionLines[sourcePath].back().EndLine <= line) { + // return last function start line for any breakpoints after. + return ListFileFunctionLines[sourcePath].back().StartLine; + } + + return 0; +} + +dap::SetBreakpointsResponse +cmDebuggerBreakpointManager::HandleSetBreakpointsRequest( + dap::SetBreakpointsRequest const& request) +{ + std::unique_lock lock(Mutex); + + dap::SetBreakpointsResponse response; + + auto sourcePath = + cmSystemTools::GetActualCaseForPath(request.source.path.value()); + const dap::array defaultValue{}; + const auto& breakpoints = request.breakpoints.value(defaultValue); + if (ListFileFunctionLines.find(sourcePath) != ListFileFunctionLines.end()) { + // The file has loaded, we can validate breakpoints. + if (Breakpoints.find(sourcePath) != Breakpoints.end()) { + Breakpoints[sourcePath].clear(); + } + response.breakpoints.resize(breakpoints.size()); + for (size_t i = 0; i < breakpoints.size(); i++) { + int64_t correctedLine = + CalibrateBreakpointLine(sourcePath, breakpoints[i].line); + if (correctedLine > 0) { + Breakpoints[sourcePath].emplace_back(NextBreakpointId++, + correctedLine); + response.breakpoints[i].id = Breakpoints[sourcePath].back().GetId(); + response.breakpoints[i].line = + Breakpoints[sourcePath].back().GetLine(); + response.breakpoints[i].verified = true; + } else { + response.breakpoints[i].verified = false; + response.breakpoints[i].line = breakpoints[i].line; + } + dap::Source dapSrc; + dapSrc.path = sourcePath; + response.breakpoints[i].source = dapSrc; + } + } else { + // The file has not loaded, validate breakpoints later. + ListFilePendingValidations.emplace(sourcePath); + + response.breakpoints.resize(breakpoints.size()); + for (size_t i = 0; i < breakpoints.size(); i++) { + Breakpoints[sourcePath].emplace_back(NextBreakpointId++, + breakpoints[i].line); + response.breakpoints[i].id = Breakpoints[sourcePath].back().GetId(); + response.breakpoints[i].line = Breakpoints[sourcePath].back().GetLine(); + response.breakpoints[i].verified = false; + dap::Source dapSrc; + dapSrc.path = sourcePath; + response.breakpoints[i].source = dapSrc; + } + } + + return response; +} + +void cmDebuggerBreakpointManager::SourceFileLoaded( + std::string const& sourcePath, + std::vector const& functions) +{ + std::unique_lock lock(Mutex); + if (ListFileFunctionLines.find(sourcePath) != ListFileFunctionLines.end()) { + // this is not expected. + return; + } + + for (cmListFileFunction const& func : functions) { + ListFileFunctionLines[sourcePath].emplace_back( + cmDebuggerFunctionLocation{ func.Line(), func.LineEnd() }); + } + + if (ListFilePendingValidations.find(sourcePath) == + ListFilePendingValidations.end()) { + return; + } + + ListFilePendingValidations.erase(sourcePath); + + for (size_t i = 0; i < Breakpoints[sourcePath].size(); i++) { + dap::BreakpointEvent breakpointEvent; + breakpointEvent.breakpoint.id = Breakpoints[sourcePath][i].GetId(); + breakpointEvent.breakpoint.line = Breakpoints[sourcePath][i].GetLine(); + auto source = dap::Source(); + source.path = sourcePath; + breakpointEvent.breakpoint.source = source; + int64_t correctedLine = CalibrateBreakpointLine( + sourcePath, Breakpoints[sourcePath][i].GetLine()); + if (correctedLine != Breakpoints[sourcePath][i].GetLine()) { + Breakpoints[sourcePath][i].ChangeLine(correctedLine); + } + breakpointEvent.reason = "changed"; + breakpointEvent.breakpoint.verified = (correctedLine > 0); + if (breakpointEvent.breakpoint.verified) { + breakpointEvent.breakpoint.line = correctedLine; + } else { + Breakpoints[sourcePath][i].Invalid(); + } + + DapSession->send(breakpointEvent); + } +} + +std::vector cmDebuggerBreakpointManager::GetBreakpoints( + std::string const& sourcePath, int64_t line) +{ + std::unique_lock lock(Mutex); + const auto& all = Breakpoints[sourcePath]; + std::vector breakpoints; + if (all.empty()) { + return breakpoints; + } + + auto it = all.begin(); + + while ((it = std::find_if( + it, all.end(), [&](const cmDebuggerSourceBreakpoint& breakpoint) { + return (breakpoint.GetIsValid() && breakpoint.GetLine() == line); + })) != all.end()) { + breakpoints.emplace_back(it->GetId()); + ++it; + } + + return breakpoints; +} + +void cmDebuggerBreakpointManager::ClearAll() +{ + std::unique_lock lock(Mutex); + Breakpoints.clear(); +} + +} // namespace cmDebugger diff --git a/Source/cmDebuggerBreakpointManager.h b/Source/cmDebuggerBreakpointManager.h new file mode 100644 index 0000000..a4e5df5 --- /dev/null +++ b/Source/cmDebuggerBreakpointManager.h @@ -0,0 +1,61 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +#include +#include +#include +#include +#include +#include + +#include + +class cmListFileFunction; + +namespace cmDebugger { +class cmDebuggerSourceBreakpoint; +} + +namespace dap { +class Session; +} + +namespace cmDebugger { + +struct cmDebuggerFunctionLocation +{ + int64_t StartLine; + int64_t EndLine; +}; + +/** The breakpoint manager. */ +class cmDebuggerBreakpointManager +{ + dap::Session* DapSession; + std::mutex Mutex; + std::unordered_map> + Breakpoints; + std::unordered_map> + ListFileFunctionLines; + std::unordered_set ListFilePendingValidations; + int64_t NextBreakpointId = 0; + + dap::SetBreakpointsResponse HandleSetBreakpointsRequest( + dap::SetBreakpointsRequest const& request); + int64_t FindFunctionStartLine(std::string const& sourcePath, int64_t line); + int64_t CalibrateBreakpointLine(std::string const& sourcePath, int64_t line); + +public: + cmDebuggerBreakpointManager(dap::Session* dapSession); + void SourceFileLoaded(std::string const& sourcePath, + std::vector const& functions); + std::vector GetBreakpoints(std::string const& sourcePath, + int64_t line); + void ClearAll(); +}; + +} // namespace cmDebugger diff --git a/Source/cmDebuggerExceptionManager.cxx b/Source/cmDebuggerExceptionManager.cxx new file mode 100644 index 0000000..a27426c --- /dev/null +++ b/Source/cmDebuggerExceptionManager.cxx @@ -0,0 +1,129 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#include "cmDebuggerExceptionManager.h" + +#include +#include + +#include +#include +#include + +#include "cmDebuggerProtocol.h" +#include "cmMessageType.h" + +namespace cmDebugger { + +cmDebuggerExceptionManager::cmDebuggerExceptionManager( + dap::Session* dapSession) + : DapSession(dapSession) +{ + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetExceptionBreakpoints + DapSession->registerHandler( + [&](const dap::SetExceptionBreakpointsRequest& request) { + return HandleSetExceptionBreakpointsRequest(request); + }); + + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_ExceptionInfo + DapSession->registerHandler([&](const dap::ExceptionInfoRequest& request) { + (void)request; + return HandleExceptionInfoRequest(); + }); + + ExceptionMap[MessageType::AUTHOR_WARNING] = + cmDebuggerExceptionFilter{ "AUTHOR_WARNING", "Warning (dev)" }; + ExceptionMap[MessageType::AUTHOR_ERROR] = + cmDebuggerExceptionFilter{ "AUTHOR_ERROR", "Error (dev)" }; + ExceptionMap[MessageType::FATAL_ERROR] = + cmDebuggerExceptionFilter{ "FATAL_ERROR", "Fatal error" }; + ExceptionMap[MessageType::INTERNAL_ERROR] = + cmDebuggerExceptionFilter{ "INTERNAL_ERROR", "Internal error" }; + ExceptionMap[MessageType::MESSAGE] = + cmDebuggerExceptionFilter{ "MESSAGE", "Other messages" }; + ExceptionMap[MessageType::WARNING] = + cmDebuggerExceptionFilter{ "WARNING", "Warning" }; + ExceptionMap[MessageType::LOG] = + cmDebuggerExceptionFilter{ "LOG", "Debug log" }; + ExceptionMap[MessageType::DEPRECATION_ERROR] = + cmDebuggerExceptionFilter{ "DEPRECATION_ERROR", "Deprecation error" }; + ExceptionMap[MessageType::DEPRECATION_WARNING] = + cmDebuggerExceptionFilter{ "DEPRECATION_WARNING", "Deprecation warning" }; + RaiseExceptions["AUTHOR_ERROR"] = true; + RaiseExceptions["FATAL_ERROR"] = true; + RaiseExceptions["INTERNAL_ERROR"] = true; + RaiseExceptions["DEPRECATION_ERROR"] = true; +} + +dap::SetExceptionBreakpointsResponse +cmDebuggerExceptionManager::HandleSetExceptionBreakpointsRequest( + dap::SetExceptionBreakpointsRequest const& request) +{ + std::unique_lock lock(Mutex); + dap::SetExceptionBreakpointsResponse response; + RaiseExceptions.clear(); + for (const auto& filter : request.filters) { + RaiseExceptions[filter] = true; + } + + return response; +} + +dap::ExceptionInfoResponse +cmDebuggerExceptionManager::HandleExceptionInfoRequest() +{ + std::unique_lock lock(Mutex); + + dap::ExceptionInfoResponse response; + if (TheException.has_value()) { + response.exceptionId = TheException->Id; + response.breakMode = "always"; + response.description = TheException->Description; + TheException = {}; + } + return response; +} + +void cmDebuggerExceptionManager::HandleInitializeRequest( + dap::CMakeInitializeResponse& response) +{ + std::unique_lock lock(Mutex); + response.supportsExceptionInfoRequest = true; + + dap::array exceptionBreakpointFilters; + for (auto& pair : ExceptionMap) { + dap::ExceptionBreakpointsFilter filter; + filter.filter = pair.second.Filter; + filter.label = pair.second.Label; + filter.def = RaiseExceptions[filter.filter]; + exceptionBreakpointFilters.emplace_back(filter); + } + + response.exceptionBreakpointFilters = exceptionBreakpointFilters; +} + +cm::optional +cmDebuggerExceptionManager::RaiseExceptionIfAny(MessageType t, + std::string const& text) +{ + cm::optional maybeStoppedEvent; + std::unique_lock lock(Mutex); + if (RaiseExceptions[ExceptionMap[t].Filter]) { + dap::StoppedEvent stoppedEvent; + stoppedEvent.allThreadsStopped = true; + stoppedEvent.reason = "exception"; + stoppedEvent.description = "Pause on exception"; + stoppedEvent.text = text; + TheException = cmDebuggerException{ ExceptionMap[t].Filter, text }; + maybeStoppedEvent = std::move(stoppedEvent); + } + + return maybeStoppedEvent; +} + +void cmDebuggerExceptionManager::ClearAll() +{ + std::unique_lock lock(Mutex); + RaiseExceptions.clear(); +} + +} // namespace cmDebugger diff --git a/Source/cmDebuggerExceptionManager.h b/Source/cmDebuggerExceptionManager.h new file mode 100644 index 0000000..b819128 --- /dev/null +++ b/Source/cmDebuggerExceptionManager.h @@ -0,0 +1,70 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +#include +#include +#include +#include +#include + +#include + +#include + +#include "cmMessageType.h" + +namespace dap { +class Session; +struct CMakeInitializeResponse; +} + +namespace cmDebugger { + +struct cmDebuggerException +{ + std::string Id; + std::string Description; +}; + +struct cmDebuggerExceptionFilter +{ + std::string Filter; + std::string Label; +}; + +/** The exception manager. */ +class cmDebuggerExceptionManager +{ + // Some older C++ standard libraries cannot hash an enum class by default. + struct MessageTypeHash + { + std::size_t operator()(MessageType t) const + { + return std::hash{}(static_cast(t)); + } + }; + + dap::Session* DapSession; + std::mutex Mutex; + std::unordered_map RaiseExceptions; + std::unordered_map + ExceptionMap; + cm::optional TheException; + + dap::SetExceptionBreakpointsResponse HandleSetExceptionBreakpointsRequest( + dap::SetExceptionBreakpointsRequest const& request); + + dap::ExceptionInfoResponse HandleExceptionInfoRequest(); + +public: + cmDebuggerExceptionManager(dap::Session* dapSession); + void HandleInitializeRequest(dap::CMakeInitializeResponse& response); + cm::optional RaiseExceptionIfAny(MessageType t, + std::string const& text); + void ClearAll(); +}; + +} // namespace cmDebugger diff --git a/Source/cmDebuggerPipeConnection.cxx b/Source/cmDebuggerPipeConnection.cxx new file mode 100644 index 0000000..1b54346 --- /dev/null +++ b/Source/cmDebuggerPipeConnection.cxx @@ -0,0 +1,293 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#include "cmDebuggerPipeConnection.h" + +#include +#include +#include +#include +#include + +namespace cmDebugger { + +struct write_req_t +{ + uv_write_t req; + uv_buf_t buf; +}; + +cmDebuggerPipeBase::cmDebuggerPipeBase(std::string name) + : PipeName(std::move(name)) +{ + Loop.init(); + LoopExit.init( + *Loop, [](uv_async_t* handle) { uv_stop((uv_loop_t*)handle->data); }, + Loop); + WriteEvent.init( + *Loop, + [](uv_async_t* handle) { + auto* conn = static_cast(handle->data); + conn->WriteInternal(); + }, + this); + PipeClose.init( + *Loop, + [](uv_async_t* handle) { + auto* conn = static_cast(handle->data); + if (conn->Pipe.get()) { + conn->Pipe->data = nullptr; + conn->Pipe.reset(); + } + }, + this); +} + +void cmDebuggerPipeBase::WaitForConnection() +{ + std::unique_lock lock(Mutex); + Connected.wait(lock, [this] { return isOpen() || FailedToOpen; }); + if (FailedToOpen) { + throw std::runtime_error("Failed to open debugger connection."); + } +} + +void cmDebuggerPipeBase::close() +{ + std::unique_lock lock(Mutex); + + CloseConnection(); + PipeClose.send(); + lock.unlock(); + ReadReady.notify_all(); +} + +size_t cmDebuggerPipeBase::read(void* buffer, size_t n) +{ + std::unique_lock lock(Mutex); + ReadReady.wait(lock, [this] { return !isOpen() || !ReadBuffer.empty(); }); + + if (!isOpen() && ReadBuffer.empty()) { + return 0; + } + + auto size = std::min(n, ReadBuffer.size()); + memcpy(buffer, ReadBuffer.data(), size); + ReadBuffer.erase(0, size); + return size; +} + +bool cmDebuggerPipeBase::write(const void* buffer, size_t n) +{ + std::unique_lock lock(Mutex); + WriteBuffer.append(static_cast(buffer), n); + lock.unlock(); + WriteEvent.send(); + + lock.lock(); + WriteComplete.wait(lock, [this] { return WriteBuffer.empty(); }); + return true; +} + +void cmDebuggerPipeBase::StopLoop() +{ + LoopExit.send(); + + if (LoopThread.joinable()) { + LoopThread.join(); + } +} + +void cmDebuggerPipeBase::BufferData(const std::string& data) +{ + std::unique_lock lock(Mutex); + ReadBuffer += data; + lock.unlock(); + ReadReady.notify_all(); +} + +void cmDebuggerPipeBase::WriteInternal() +{ + std::unique_lock lock(Mutex); + auto n = WriteBuffer.length(); + assert(this->Pipe.get()); + write_req_t* req = new write_req_t; + req->req.data = &WriteComplete; + char* rawBuffer = new char[n]; + req->buf = uv_buf_init(rawBuffer, static_cast(n)); + memcpy(req->buf.base, WriteBuffer.data(), n); + WriteBuffer.clear(); + lock.unlock(); + + uv_write( + reinterpret_cast(req), this->Pipe, &req->buf, 1, + [](uv_write_t* cb_req, int status) { + (void)status; // We need to free memory even if the write failed. + write_req_t* wr = reinterpret_cast(cb_req); + reinterpret_cast(wr->req.data)->notify_all(); + delete[] (wr->buf.base); + delete wr; + }); + +#ifdef __clang_analyzer__ + // Tell clang-analyzer that 'rawBuffer' does not leak. + // We pass ownership to the closure. + delete[] rawBuffer; +#endif +} + +cmDebuggerPipeConnection::cmDebuggerPipeConnection(std::string name) + : cmDebuggerPipeBase(std::move(name)) +{ + ServerPipeClose.init( + *Loop, + [](uv_async_t* handle) { + auto* conn = static_cast(handle->data); + if (conn->ServerPipe.get()) { + conn->ServerPipe->data = nullptr; + conn->ServerPipe.reset(); + } + }, + this); +} + +cmDebuggerPipeConnection::~cmDebuggerPipeConnection() +{ + StopLoop(); +} + +bool cmDebuggerPipeConnection::StartListening(std::string& errorMessage) +{ + this->ServerPipe.init(*Loop, 0, + static_cast(this)); + + int r; + if ((r = uv_pipe_bind(this->ServerPipe, this->PipeName.c_str())) != 0) { + errorMessage = + "Internal Error with " + this->PipeName + ": " + uv_err_name(r); + return false; + } + + r = uv_listen(this->ServerPipe, 1, [](uv_stream_t* stream, int status) { + if (status >= 0) { + auto* conn = static_cast(stream->data); + if (conn) { + conn->Connect(stream); + } + } + }); + + if (r != 0) { + errorMessage = + "Internal Error listening on " + this->PipeName + ": " + uv_err_name(r); + return false; + } + + // Start the libuv event loop thread so that a client can connect. + LoopThread = std::thread([this] { uv_run(Loop, UV_RUN_DEFAULT); }); + + StartedListening.set_value(); + + return true; +} + +std::shared_ptr cmDebuggerPipeConnection::GetReader() +{ + return std::static_pointer_cast(shared_from_this()); +} + +std::shared_ptr cmDebuggerPipeConnection::GetWriter() +{ + return std::static_pointer_cast(shared_from_this()); +} + +bool cmDebuggerPipeConnection::isOpen() +{ + return this->Pipe.get() != nullptr; +} + +void cmDebuggerPipeConnection::CloseConnection() +{ + ServerPipeClose.send(); +} + +void cmDebuggerPipeConnection::Connect(uv_stream_t* server) +{ + if (this->Pipe.get()) { + // Accept and close all pipes but the first: + cm::uv_pipe_ptr rejectPipe; + + rejectPipe.init(*Loop, 0); + uv_accept(server, rejectPipe); + + return; + } + + cm::uv_pipe_ptr ClientPipe; + ClientPipe.init(*Loop, 0, static_cast(this)); + + if (uv_accept(server, ClientPipe) != 0) { + return; + } + + StartReading(ClientPipe); + + std::unique_lock lock(Mutex); + Pipe = std::move(ClientPipe); + lock.unlock(); + Connected.notify_all(); +} + +cmDebuggerPipeClient::~cmDebuggerPipeClient() +{ + StopLoop(); +} + +void cmDebuggerPipeClient::Start() +{ + this->Pipe.init(*Loop, 0, static_cast(this)); + + uv_connect_t* connect = new uv_connect_t; + connect->data = this; + uv_pipe_connect( + connect, Pipe, PipeName.c_str(), [](uv_connect_t* cb_connect, int status) { + auto* conn = static_cast(cb_connect->data); + if (status >= 0) { + conn->Connect(); + } else { + conn->FailConnection(); + } + delete cb_connect; + }); + + // Start the libuv event loop so that the pipe can connect. + LoopThread = std::thread([this] { uv_run(Loop, UV_RUN_DEFAULT); }); +} + +bool cmDebuggerPipeClient::isOpen() +{ + return IsConnected; +} + +void cmDebuggerPipeClient::CloseConnection() +{ + IsConnected = false; +} + +void cmDebuggerPipeClient::Connect() +{ + StartReading(Pipe); + std::unique_lock lock(Mutex); + IsConnected = true; + lock.unlock(); + Connected.notify_all(); +} + +void cmDebuggerPipeClient::FailConnection() +{ + std::unique_lock lock(Mutex); + FailedToOpen = true; + lock.unlock(); + Connected.notify_all(); +} + +} // namespace cmDebugger diff --git a/Source/cmDebuggerPipeConnection.h b/Source/cmDebuggerPipeConnection.h new file mode 100644 index 0000000..0991ff7 --- /dev/null +++ b/Source/cmDebuggerPipeConnection.h @@ -0,0 +1,139 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "cmDebuggerAdapter.h" +#include "cmUVHandlePtr.h" + +namespace cmDebugger { + +class cmDebuggerPipeBase : public dap::ReaderWriter +{ +public: + cmDebuggerPipeBase(std::string name); + + void WaitForConnection(); + + // dap::ReaderWriter implementation + + void close() final; + size_t read(void* buffer, size_t n) final; + bool write(const void* buffer, size_t n) final; + +protected: + virtual void CloseConnection(){}; + template + void StartReading(uv_stream_t* stream) + { + uv_read_start( + stream, + // alloc_cb + [](uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { + (void)handle; + char* rawBuffer = new char[suggested_size]; + *buf = + uv_buf_init(rawBuffer, static_cast(suggested_size)); + }, + // read_cb + [](uv_stream_t* readStream, ssize_t nread, const uv_buf_t* buf) { + auto conn = static_cast(readStream->data); + if (conn) { + if (nread >= 0) { + conn->BufferData(std::string(buf->base, buf->base + nread)); + } else { + conn->close(); + } + } + delete[] (buf->base); + }); + } + void StopLoop(); + + const std::string PipeName; + std::thread LoopThread; + cm::uv_loop_ptr Loop; + cm::uv_pipe_ptr Pipe; + std::mutex Mutex; + std::condition_variable Connected; + bool FailedToOpen = false; + +private: + void BufferData(const std::string& data); + void WriteInternal(); + + cm::uv_async_ptr LoopExit; + cm::uv_async_ptr WriteEvent; + cm::uv_async_ptr PipeClose; + std::string WriteBuffer; + std::string ReadBuffer; + std::condition_variable ReadReady; + std::condition_variable WriteComplete; +}; + +class cmDebuggerPipeConnection + : public cmDebuggerPipeBase + , public cmDebuggerConnection + , public std::enable_shared_from_this +{ +public: + cmDebuggerPipeConnection(std::string name); + ~cmDebuggerPipeConnection() override; + + void WaitForConnection() override + { + cmDebuggerPipeBase::WaitForConnection(); + } + + bool StartListening(std::string& errorMessage) override; + std::shared_ptr GetReader() override; + std::shared_ptr GetWriter() override; + + // dap::ReaderWriter implementation + + bool isOpen() override; + + // Used for unit test synchronization + std::promise StartedListening; + +private: + void CloseConnection() override; + void Connect(uv_stream_t* server); + + cm::uv_pipe_ptr ServerPipe; + cm::uv_async_ptr ServerPipeClose; +}; + +class cmDebuggerPipeClient : public cmDebuggerPipeBase +{ +public: + using cmDebuggerPipeBase::cmDebuggerPipeBase; + ~cmDebuggerPipeClient() override; + + void Start(); + + // dap::ReaderWriter implementation + + bool isOpen() override; + +private: + void CloseConnection() override; + void Connect(); + void FailConnection(); + + bool IsConnected = false; +}; + +} // namespace cmDebugger diff --git a/Source/cmDebuggerProtocol.cxx b/Source/cmDebuggerProtocol.cxx new file mode 100644 index 0000000..505de35 --- /dev/null +++ b/Source/cmDebuggerProtocol.cxx @@ -0,0 +1,80 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ + +#include "cmDebuggerProtocol.h" + +#include + +namespace dap { +DAP_IMPLEMENT_STRUCT_TYPEINFO(CMakeVersion, "", DAP_FIELD(major, "major"), + DAP_FIELD(minor, "minor"), + DAP_FIELD(patch, "patch"), + DAP_FIELD(full, "full")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO( + CMakeInitializeResponse, "", + DAP_FIELD(additionalModuleColumns, "additionalModuleColumns"), + DAP_FIELD(completionTriggerCharacters, "completionTriggerCharacters"), + DAP_FIELD(exceptionBreakpointFilters, "exceptionBreakpointFilters"), + DAP_FIELD(supportSuspendDebuggee, "supportSuspendDebuggee"), + DAP_FIELD(supportTerminateDebuggee, "supportTerminateDebuggee"), + DAP_FIELD(supportedChecksumAlgorithms, "supportedChecksumAlgorithms"), + DAP_FIELD(supportsBreakpointLocationsRequest, + "supportsBreakpointLocationsRequest"), + DAP_FIELD(supportsCancelRequest, "supportsCancelRequest"), + DAP_FIELD(supportsClipboardContext, "supportsClipboardContext"), + DAP_FIELD(supportsCompletionsRequest, "supportsCompletionsRequest"), + DAP_FIELD(supportsConditionalBreakpoints, "supportsConditionalBreakpoints"), + DAP_FIELD(supportsConfigurationDoneRequest, + "supportsConfigurationDoneRequest"), + DAP_FIELD(supportsDataBreakpoints, "supportsDataBreakpoints"), + DAP_FIELD(supportsDelayedStackTraceLoading, + "supportsDelayedStackTraceLoading"), + DAP_FIELD(supportsDisassembleRequest, "supportsDisassembleRequest"), + DAP_FIELD(supportsEvaluateForHovers, "supportsEvaluateForHovers"), + DAP_FIELD(supportsExceptionFilterOptions, "supportsExceptionFilterOptions"), + DAP_FIELD(supportsExceptionInfoRequest, "supportsExceptionInfoRequest"), + DAP_FIELD(supportsExceptionOptions, "supportsExceptionOptions"), + DAP_FIELD(supportsFunctionBreakpoints, "supportsFunctionBreakpoints"), + DAP_FIELD(supportsGotoTargetsRequest, "supportsGotoTargetsRequest"), + DAP_FIELD(supportsHitConditionalBreakpoints, + "supportsHitConditionalBreakpoints"), + DAP_FIELD(supportsInstructionBreakpoints, "supportsInstructionBreakpoints"), + DAP_FIELD(supportsLoadedSourcesRequest, "supportsLoadedSourcesRequest"), + DAP_FIELD(supportsLogPoints, "supportsLogPoints"), + DAP_FIELD(supportsModulesRequest, "supportsModulesRequest"), + DAP_FIELD(supportsReadMemoryRequest, "supportsReadMemoryRequest"), + DAP_FIELD(supportsRestartFrame, "supportsRestartFrame"), + DAP_FIELD(supportsRestartRequest, "supportsRestartRequest"), + DAP_FIELD(supportsSetExpression, "supportsSetExpression"), + DAP_FIELD(supportsSetVariable, "supportsSetVariable"), + DAP_FIELD(supportsSingleThreadExecutionRequests, + "supportsSingleThreadExecutionRequests"), + DAP_FIELD(supportsStepBack, "supportsStepBack"), + DAP_FIELD(supportsStepInTargetsRequest, "supportsStepInTargetsRequest"), + DAP_FIELD(supportsSteppingGranularity, "supportsSteppingGranularity"), + DAP_FIELD(supportsTerminateRequest, "supportsTerminateRequest"), + DAP_FIELD(supportsTerminateThreadsRequest, + "supportsTerminateThreadsRequest"), + DAP_FIELD(supportsValueFormattingOptions, "supportsValueFormattingOptions"), + DAP_FIELD(supportsWriteMemoryRequest, "supportsWriteMemoryRequest"), + DAP_FIELD(cmakeVersion, "cmakeVersion")); + +DAP_IMPLEMENT_STRUCT_TYPEINFO( + CMakeInitializeRequest, "initialize", DAP_FIELD(adapterID, "adapterID"), + DAP_FIELD(clientID, "clientID"), DAP_FIELD(clientName, "clientName"), + DAP_FIELD(columnsStartAt1, "columnsStartAt1"), + DAP_FIELD(linesStartAt1, "linesStartAt1"), DAP_FIELD(locale, "locale"), + DAP_FIELD(pathFormat, "pathFormat"), + DAP_FIELD(supportsArgsCanBeInterpretedByShell, + "supportsArgsCanBeInterpretedByShell"), + DAP_FIELD(supportsInvalidatedEvent, "supportsInvalidatedEvent"), + DAP_FIELD(supportsMemoryEvent, "supportsMemoryEvent"), + DAP_FIELD(supportsMemoryReferences, "supportsMemoryReferences"), + DAP_FIELD(supportsProgressReporting, "supportsProgressReporting"), + DAP_FIELD(supportsRunInTerminalRequest, "supportsRunInTerminalRequest"), + DAP_FIELD(supportsStartDebuggingRequest, "supportsStartDebuggingRequest"), + DAP_FIELD(supportsVariablePaging, "supportsVariablePaging"), + DAP_FIELD(supportsVariableType, "supportsVariableType")); + +} // namespace dap diff --git a/Source/cmDebuggerProtocol.h b/Source/cmDebuggerProtocol.h new file mode 100644 index 0000000..4334aed --- /dev/null +++ b/Source/cmDebuggerProtocol.h @@ -0,0 +1,191 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +#include + +#include + +#include +#include +#include + +namespace dap { + +// Represents the cmake version. +struct CMakeVersion : public InitializeResponse +{ + // The major version number. + integer major; + // The minor version number. + integer minor; + // The patch number. + integer patch; + // The full version string. + string full; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(CMakeVersion); + +// Response to `initialize` request. +struct CMakeInitializeResponse : public Response +{ + // The set of additional module information exposed by the debug adapter. + optional> additionalModuleColumns; + // The set of characters that should trigger completion in a REPL. If not + // specified, the UI should assume the `.` character. + optional> completionTriggerCharacters; + // Available exception filter options for the `setExceptionBreakpoints` + // request. + optional> exceptionBreakpointFilters; + // The debug adapter supports the `suspendDebuggee` attribute on the + // `disconnect` request. + optional supportSuspendDebuggee; + // The debug adapter supports the `terminateDebuggee` attribute on the + // `disconnect` request. + optional supportTerminateDebuggee; + // Checksum algorithms supported by the debug adapter. + optional> supportedChecksumAlgorithms; + // The debug adapter supports the `breakpointLocations` request. + optional supportsBreakpointLocationsRequest; + // The debug adapter supports the `cancel` request. + optional supportsCancelRequest; + // The debug adapter supports the `clipboard` context value in the `evaluate` + // request. + optional supportsClipboardContext; + // The debug adapter supports the `completions` request. + optional supportsCompletionsRequest; + // The debug adapter supports conditional breakpoints. + optional supportsConditionalBreakpoints; + // The debug adapter supports the `configurationDone` request. + optional supportsConfigurationDoneRequest; + // The debug adapter supports data breakpoints. + optional supportsDataBreakpoints; + // The debug adapter supports the delayed loading of parts of the stack, + // which requires that both the `startFrame` and `levels` arguments and the + // `totalFrames` result of the `stackTrace` request are supported. + optional supportsDelayedStackTraceLoading; + // The debug adapter supports the `disassemble` request. + optional supportsDisassembleRequest; + // The debug adapter supports a (side effect free) `evaluate` request for + // data hovers. + optional supportsEvaluateForHovers; + // The debug adapter supports `filterOptions` as an argument on the + // `setExceptionBreakpoints` request. + optional supportsExceptionFilterOptions; + // The debug adapter supports the `exceptionInfo` request. + optional supportsExceptionInfoRequest; + // The debug adapter supports `exceptionOptions` on the + // `setExceptionBreakpoints` request. + optional supportsExceptionOptions; + // The debug adapter supports function breakpoints. + optional supportsFunctionBreakpoints; + // The debug adapter supports the `gotoTargets` request. + optional supportsGotoTargetsRequest; + // The debug adapter supports breakpoints that break execution after a + // specified number of hits. + optional supportsHitConditionalBreakpoints; + // The debug adapter supports adding breakpoints based on instruction + // references. + optional supportsInstructionBreakpoints; + // The debug adapter supports the `loadedSources` request. + optional supportsLoadedSourcesRequest; + // The debug adapter supports log points by interpreting the `logMessage` + // attribute of the `SourceBreakpoint`. + optional supportsLogPoints; + // The debug adapter supports the `modules` request. + optional supportsModulesRequest; + // The debug adapter supports the `readMemory` request. + optional supportsReadMemoryRequest; + // The debug adapter supports restarting a frame. + optional supportsRestartFrame; + // The debug adapter supports the `restart` request. In this case a client + // should not implement `restart` by terminating and relaunching the adapter + // but by calling the `restart` request. + optional supportsRestartRequest; + // The debug adapter supports the `setExpression` request. + optional supportsSetExpression; + // The debug adapter supports setting a variable to a value. + optional supportsSetVariable; + // The debug adapter supports the `singleThread` property on the execution + // requests (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`, + // `stepBack`). + optional supportsSingleThreadExecutionRequests; + // The debug adapter supports stepping back via the `stepBack` and + // `reverseContinue` requests. + optional supportsStepBack; + // The debug adapter supports the `stepInTargets` request. + optional supportsStepInTargetsRequest; + // The debug adapter supports stepping granularities (argument `granularity`) + // for the stepping requests. + optional supportsSteppingGranularity; + // The debug adapter supports the `terminate` request. + optional supportsTerminateRequest; + // The debug adapter supports the `terminateThreads` request. + optional supportsTerminateThreadsRequest; + // The debug adapter supports a `format` attribute on the `stackTrace`, + // `variables`, and `evaluate` requests. + optional supportsValueFormattingOptions; + // The debug adapter supports the `writeMemory` request. + optional supportsWriteMemoryRequest; + // The CMake version. + CMakeVersion cmakeVersion; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(CMakeInitializeResponse); + +// The `initialize` request is sent as the first request from the client to the +// debug adapter in order to configure it with client capabilities and to +// retrieve capabilities from the debug adapter. Until the debug adapter has +// responded with an `initialize` response, the client must not send any +// additional requests or events to the debug adapter. In addition the debug +// adapter is not allowed to send any requests or events to the client until it +// has responded with an `initialize` response. The `initialize` request may +// only be sent once. +struct CMakeInitializeRequest : public Request +{ + using Response = CMakeInitializeResponse; + // The ID of the debug adapter. + string adapterID; + // The ID of the client using this adapter. + optional clientID; + // The human-readable name of the client using this adapter. + optional clientName; + // If true all column numbers are 1-based (default). + optional columnsStartAt1; + // If true all line numbers are 1-based (default). + optional linesStartAt1; + // The ISO-639 locale of the client using this adapter, e.g. en-US or de-CH. + optional locale; + // Determines in what format paths are specified. The default is `path`, + // which is the native format. + // + // May be one of the following enumeration values: + // 'path', 'uri' + optional pathFormat; + // Client supports the `argsCanBeInterpretedByShell` attribute on the + // `runInTerminal` request. + optional supportsArgsCanBeInterpretedByShell; + // Client supports the `invalidated` event. + optional supportsInvalidatedEvent; + // Client supports the `memory` event. + optional supportsMemoryEvent; + // Client supports memory references. + optional supportsMemoryReferences; + // Client supports progress reporting. + optional supportsProgressReporting; + // Client supports the `runInTerminal` request. + optional supportsRunInTerminalRequest; + // Client supports the `startDebugging` request. + optional supportsStartDebuggingRequest; + // Client supports the paging of variables. + optional supportsVariablePaging; + // Client supports the `type` attribute for variables. + optional supportsVariableType; +}; + +DAP_DECLARE_STRUCT_TYPEINFO(CMakeInitializeRequest); + +} // namespace dap diff --git a/Source/cmDebuggerSourceBreakpoint.cxx b/Source/cmDebuggerSourceBreakpoint.cxx new file mode 100644 index 0000000..d4665e6 --- /dev/null +++ b/Source/cmDebuggerSourceBreakpoint.cxx @@ -0,0 +1,14 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#include "cmDebuggerSourceBreakpoint.h" + +namespace cmDebugger { + +cmDebuggerSourceBreakpoint::cmDebuggerSourceBreakpoint(int64_t id, + int64_t line) + : Id(id) + , Line(line) +{ +} + +} // namespace cmDebugger diff --git a/Source/cmDebuggerSourceBreakpoint.h b/Source/cmDebuggerSourceBreakpoint.h new file mode 100644 index 0000000..f6d6cac --- /dev/null +++ b/Source/cmDebuggerSourceBreakpoint.h @@ -0,0 +1,26 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +#include + +namespace cmDebugger { + +class cmDebuggerSourceBreakpoint +{ + int64_t Id; + int64_t Line; + bool IsValid = true; + +public: + cmDebuggerSourceBreakpoint(int64_t id, int64_t line); + int64_t GetId() const noexcept { return this->Id; } + int64_t GetLine() const noexcept { return this->Line; } + void ChangeLine(int64_t line) noexcept { this->Line = line; } + bool GetIsValid() const noexcept { return this->IsValid; } + void Invalid() noexcept { this->IsValid = false; } +}; + +} // namespace cmDebugger diff --git a/Source/cmDebuggerStackFrame.cxx b/Source/cmDebuggerStackFrame.cxx new file mode 100644 index 0000000..789b0a5 --- /dev/null +++ b/Source/cmDebuggerStackFrame.cxx @@ -0,0 +1,28 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#include "cmDebuggerStackFrame.h" + +#include + +#include "cmListFileCache.h" + +namespace cmDebugger { + +std::atomic cmDebuggerStackFrame::NextId(1); + +cmDebuggerStackFrame::cmDebuggerStackFrame(cmMakefile* mf, + std::string sourcePath, + cmListFileFunction const& lff) + : Id(NextId.fetch_add(1)) + , FileName(std::move(sourcePath)) + , Function(lff) + , Makefile(mf) +{ +} + +int64_t cmDebuggerStackFrame::GetLine() const noexcept +{ + return this->Function.Line(); +} + +} // namespace cmDebugger diff --git a/Source/cmDebuggerStackFrame.h b/Source/cmDebuggerStackFrame.h new file mode 100644 index 0000000..dc3b2ab --- /dev/null +++ b/Source/cmDebuggerStackFrame.h @@ -0,0 +1,33 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +#include +#include +#include + +class cmListFileFunction; +class cmMakefile; + +namespace cmDebugger { + +class cmDebuggerStackFrame +{ + static std::atomic NextId; + std::int64_t Id; + std::string FileName; + cmListFileFunction const& Function; + cmMakefile* Makefile; + +public: + cmDebuggerStackFrame(cmMakefile* mf, std::string sourcePath, + cmListFileFunction const& lff); + int64_t GetId() const noexcept { return this->Id; } + std::string const& GetFileName() const noexcept { return this->FileName; } + int64_t GetLine() const noexcept; + cmMakefile* GetMakefile() const noexcept { return this->Makefile; } +}; + +} // namespace cmDebugger diff --git a/Source/cmDebuggerThread.cxx b/Source/cmDebuggerThread.cxx new file mode 100644 index 0000000..fd52f5a --- /dev/null +++ b/Source/cmDebuggerThread.cxx @@ -0,0 +1,150 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ + +#include "cmDebuggerThread.h" + +#include +#include + +#include +#include + +#include "cmDebuggerStackFrame.h" +#include "cmDebuggerVariables.h" +#include "cmDebuggerVariablesHelper.h" +#include "cmDebuggerVariablesManager.h" + +namespace cmDebugger { + +cmDebuggerThread::cmDebuggerThread(int64_t id, std::string name) + : Id(id) + , Name(std::move(name)) + , VariablesManager(std::make_shared()) +{ +} + +void cmDebuggerThread::PushStackFrame(cmMakefile* mf, + std::string const& sourcePath, + cmListFileFunction const& lff) +{ + std::unique_lock lock(Mutex); + Frames.emplace_back( + std::make_shared(mf, sourcePath, lff)); + FrameMap.insert({ Frames.back()->GetId(), Frames.back() }); +} + +void cmDebuggerThread::PopStackFrame() +{ + std::unique_lock lock(Mutex); + FrameMap.erase(Frames.back()->GetId()); + FrameScopes.erase(Frames.back()->GetId()); + FrameVariables.erase(Frames.back()->GetId()); + Frames.pop_back(); +} + +std::shared_ptr cmDebuggerThread::GetTopStackFrame() +{ + std::unique_lock lock(Mutex); + if (!Frames.empty()) { + return Frames.back(); + } + + return {}; +} + +std::shared_ptr cmDebuggerThread::GetStackFrame( + int64_t frameId) +{ + std::unique_lock lock(Mutex); + auto it = FrameMap.find(frameId); + + if (it == FrameMap.end()) { + return {}; + } + + return it->second; +} + +dap::ScopesResponse cmDebuggerThread::GetScopesResponse( + int64_t frameId, bool supportsVariableType) +{ + std::unique_lock lock(Mutex); + auto it = FrameScopes.find(frameId); + + if (it != FrameScopes.end()) { + dap::ScopesResponse response; + response.scopes = it->second; + return response; + } + + auto it2 = FrameMap.find(frameId); + if (it2 == FrameMap.end()) { + return dap::ScopesResponse(); + } + + std::shared_ptr frame = it2->second; + std::shared_ptr localVariables = + cmDebuggerVariablesHelper::Create(VariablesManager, "Locals", + supportsVariableType, frame); + + FrameVariables[frameId].emplace_back(localVariables); + + dap::Scope scope; + scope.name = localVariables->GetName(); + scope.presentationHint = "locals"; + scope.variablesReference = localVariables->GetId(); + + dap::Source source; + source.name = frame->GetFileName(); + source.path = source.name; + scope.source = source; + + FrameScopes[frameId].push_back(scope); + + dap::ScopesResponse response; + response.scopes.push_back(scope); + return response; +} + +dap::VariablesResponse cmDebuggerThread::GetVariablesResponse( + dap::VariablesRequest const& request) +{ + std::unique_lock lock(Mutex); + dap::VariablesResponse response; + response.variables = VariablesManager->HandleVariablesRequest(request); + return response; +} + +dap::StackTraceResponse GetStackTraceResponse( + std::shared_ptr const& thread) +{ + dap::StackTraceResponse response; + std::unique_lock lock(thread->Mutex); + for (int i = static_cast(thread->Frames.size()) - 1; i >= 0; --i) { + dap::Source source; + source.name = thread->Frames[i]->GetFileName(); + source.path = source.name; + +#ifdef __GNUC__ +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Warray-bounds" +#endif + dap::StackFrame stackFrame; +#ifdef __GNUC__ +# pragma GCC diagnostic pop +#endif + stackFrame.line = thread->Frames[i]->GetLine(); + stackFrame.column = 1; + stackFrame.name = thread->Frames[i]->GetFileName() + " Line " + + std::to_string(stackFrame.line); + stackFrame.id = thread->Frames[i]->GetId(); + stackFrame.source = source; + + response.stackFrames.push_back(stackFrame); + } + + response.totalFrames = response.stackFrames.size(); + return response; +} + +} // namespace cmDebugger diff --git a/Source/cmDebuggerThread.h b/Source/cmDebuggerThread.h new file mode 100644 index 0000000..65ee2cf --- /dev/null +++ b/Source/cmDebuggerThread.h @@ -0,0 +1,59 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +#include +#include +#include +#include +#include +#include +#include + +#include + +class cmListFileFunction; +class cmMakefile; + +namespace cmDebugger { +class cmDebuggerStackFrame; +class cmDebuggerVariables; +class cmDebuggerVariablesManager; +} + +namespace cmDebugger { + +class cmDebuggerThread +{ + int64_t Id; + std::string Name; + std::vector> Frames; + std::unordered_map> FrameMap; + std::mutex Mutex; + std::unordered_map> FrameScopes; + std::unordered_map>> + FrameVariables; + std::shared_ptr VariablesManager; + +public: + cmDebuggerThread(int64_t id, std::string name); + int64_t GetId() const { return this->Id; } + const std::string& GetName() const { return this->Name; } + void PushStackFrame(cmMakefile* mf, std::string const& sourcePath, + cmListFileFunction const& lff); + void PopStackFrame(); + std::shared_ptr GetTopStackFrame(); + std::shared_ptr GetStackFrame(int64_t frameId); + size_t GetStackFrameSize() const { return this->Frames.size(); } + dap::ScopesResponse GetScopesResponse(int64_t frameId, + bool supportsVariableType); + dap::VariablesResponse GetVariablesResponse( + dap::VariablesRequest const& request); + friend dap::StackTraceResponse GetStackTraceResponse( + std::shared_ptr const& thread); +}; + +} // namespace cmDebugger diff --git a/Source/cmDebuggerThreadManager.cxx b/Source/cmDebuggerThreadManager.cxx new file mode 100644 index 0000000..0eb443b --- /dev/null +++ b/Source/cmDebuggerThreadManager.cxx @@ -0,0 +1,47 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ + +#include "cmDebuggerThreadManager.h" + +#include + +#include + +#include "cmDebuggerThread.h" + +namespace cmDebugger { + +std::atomic cmDebuggerThreadManager::NextThreadId(1); + +std::shared_ptr cmDebuggerThreadManager::StartThread( + std::string const& name) +{ + std::shared_ptr thread = + std::make_shared( + cmDebuggerThreadManager::NextThreadId.fetch_add(1), name); + Threads.emplace_back(thread); + return thread; +} + +void cmDebuggerThreadManager::EndThread( + std::shared_ptr const& thread) +{ + Threads.remove(thread); +} + +cm::optional +cmDebuggerThreadManager::GetThreadStackTraceResponse(int64_t id) +{ + auto it = find_if(Threads.begin(), Threads.end(), + [&](const std::shared_ptr& t) { + return t->GetId() == id; + }); + + if (it == Threads.end()) { + return {}; + } + + return GetStackTraceResponse(*it); +} + +} // namespace cmDebugger diff --git a/Source/cmDebuggerThreadManager.h b/Source/cmDebuggerThreadManager.h new file mode 100644 index 0000000..934cf85 --- /dev/null +++ b/Source/cmDebuggerThreadManager.h @@ -0,0 +1,38 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +#include +#include +#include +#include +#include + +#include + +namespace cmDebugger { +class cmDebuggerThread; +} + +namespace dap { +struct StackTraceResponse; +} + +namespace cmDebugger { + +class cmDebuggerThreadManager +{ + static std::atomic NextThreadId; + std::list> Threads; + +public: + cmDebuggerThreadManager() = default; + std::shared_ptr StartThread(std::string const& name); + void EndThread(std::shared_ptr const& thread); + cm::optional GetThreadStackTraceResponse( + std::int64_t id); +}; + +} // namespace cmDebugger diff --git a/Source/cmDebuggerVariables.cxx b/Source/cmDebuggerVariables.cxx new file mode 100644 index 0000000..40fe41f --- /dev/null +++ b/Source/cmDebuggerVariables.cxx @@ -0,0 +1,133 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ + +#include "cmDebuggerVariables.h" + +#include +#include + +#include +#include +#include + +#include "cmDebuggerVariablesManager.h" + +namespace cmDebugger { + +namespace { +const dap::VariablePresentationHint PrivatePropertyHint = { {}, + "property", + {}, + "private" }; +const dap::VariablePresentationHint PrivateDataHint = { {}, + "data", + {}, + "private" }; +} + +std::atomic cmDebuggerVariables::NextId(1); + +cmDebuggerVariables::cmDebuggerVariables( + std::shared_ptr variablesManager, + std::string name, bool supportsVariableType) + : Id(NextId.fetch_add(1)) + , Name(std::move(name)) + , SupportsVariableType(supportsVariableType) + , VariablesManager(std::move(variablesManager)) +{ + VariablesManager->RegisterHandler( + Id, [this](dap::VariablesRequest const& request) { + (void)request; + return this->HandleVariablesRequest(); + }); +} + +cmDebuggerVariables::cmDebuggerVariables( + std::shared_ptr variablesManager, + std::string name, bool supportsVariableType, + std::function()> getKeyValuesFunction) + : Id(NextId.fetch_add(1)) + , Name(std::move(name)) + , GetKeyValuesFunction(std::move(getKeyValuesFunction)) + , SupportsVariableType(supportsVariableType) + , VariablesManager(std::move(variablesManager)) +{ + VariablesManager->RegisterHandler( + Id, [this](dap::VariablesRequest const& request) { + (void)request; + return this->HandleVariablesRequest(); + }); +} + +void cmDebuggerVariables::AddSubVariables( + std::shared_ptr const& variables) +{ + if (variables != nullptr) { + SubVariables.emplace_back(variables); + } +} + +dap::array cmDebuggerVariables::HandleVariablesRequest() +{ + dap::array variables; + + if (GetKeyValuesFunction != nullptr) { + auto values = GetKeyValuesFunction(); + for (auto const& entry : values) { + if (IgnoreEmptyStringEntries && entry.Type == "string" && + entry.Value.empty()) { + continue; + } + variables.push_back(dap::Variable{ {}, + {}, + {}, + entry.Name, + {}, + PrivateDataHint, + entry.Type, + entry.Value, + 0 }); + } + } + + EnumerateSubVariablesIfAny(variables); + + if (EnableSorting) { + std::sort(variables.begin(), variables.end(), + [](dap::Variable const& a, dap::Variable const& b) { + return a.name < b.name; + }); + } + return variables; +} + +void cmDebuggerVariables::EnumerateSubVariablesIfAny( + dap::array& toBeReturned) const +{ + dap::array ret; + for (auto const& variables : SubVariables) { + toBeReturned.emplace_back( + dap::Variable{ {}, + {}, + {}, + variables->GetName(), + {}, + PrivatePropertyHint, + SupportsVariableType ? "collection" : nullptr, + variables->GetValue(), + variables->GetId() }); + } +} + +void cmDebuggerVariables::ClearSubVariables() +{ + SubVariables.clear(); +} + +cmDebuggerVariables::~cmDebuggerVariables() +{ + ClearSubVariables(); + VariablesManager->UnregisterHandler(Id); +} + +} // namespace cmDebugger diff --git a/Source/cmDebuggerVariables.h b/Source/cmDebuggerVariables.h new file mode 100644 index 0000000..eaaf2a8 --- /dev/null +++ b/Source/cmDebuggerVariables.h @@ -0,0 +1,124 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +#include +#include +#include +#include +#include +#include +#include + +#include // IWYU pragma: keep + +namespace cmDebugger { +class cmDebuggerVariablesManager; +} + +namespace dap { +struct Variable; +} + +namespace cmDebugger { + +struct cmDebuggerVariableEntry +{ + cmDebuggerVariableEntry() + : cmDebuggerVariableEntry("", "", "") + { + } + cmDebuggerVariableEntry(std::string name, std::string value, + std::string type) + : Name(std::move(name)) + , Value(std::move(value)) + , Type(std::move(type)) + { + } + cmDebuggerVariableEntry(std::string name, std::string value) + : Name(std::move(name)) + , Value(std::move(value)) + , Type("string") + { + } + cmDebuggerVariableEntry(std::string name, const char* value) + : Name(std::move(name)) + , Value(value == nullptr ? "" : value) + , Type("string") + { + } + cmDebuggerVariableEntry(std::string name, bool value) + : Name(std::move(name)) + , Value(value ? "TRUE" : "FALSE") + , Type("bool") + { + } + cmDebuggerVariableEntry(std::string name, int64_t value) + : Name(std::move(name)) + , Value(std::to_string(value)) + , Type("int") + { + } + cmDebuggerVariableEntry(std::string name, int value) + : Name(std::move(name)) + , Value(std::to_string(value)) + , Type("int") + { + } + std::string const Name; + std::string const Value; + std::string const Type; +}; + +class cmDebuggerVariables +{ + static std::atomic NextId; + int64_t Id; + std::string Name; + std::string Value; + + std::function()> GetKeyValuesFunction; + std::vector> SubVariables; + bool IgnoreEmptyStringEntries = false; + bool EnableSorting = true; + + virtual dap::array HandleVariablesRequest(); + friend class cmDebuggerVariablesManager; + +protected: + const bool SupportsVariableType; + std::shared_ptr VariablesManager; + void EnumerateSubVariablesIfAny( + dap::array& toBeReturned) const; + void ClearSubVariables(); + +public: + cmDebuggerVariables( + std::shared_ptr variablesManager, + std::string name, bool supportsVariableType); + cmDebuggerVariables( + std::shared_ptr variablesManager, + std::string name, bool supportsVariableType, + std::function()> getKeyValuesFunc); + inline int64_t GetId() const noexcept { return this->Id; } + inline std::string GetName() const noexcept { return this->Name; } + inline std::string GetValue() const noexcept { return this->Value; } + inline void SetValue(std::string const& value) noexcept + { + this->Value = value; + } + void AddSubVariables(std::shared_ptr const& variables); + inline void SetIgnoreEmptyStringEntries(bool value) noexcept + { + this->IgnoreEmptyStringEntries = value; + } + inline void SetEnableSorting(bool value) noexcept + { + this->EnableSorting = value; + } + virtual ~cmDebuggerVariables(); +}; + +} // namespace cmDebugger diff --git a/Source/cmDebuggerVariablesHelper.cxx b/Source/cmDebuggerVariablesHelper.cxx new file mode 100644 index 0000000..42ce5e7 --- /dev/null +++ b/Source/cmDebuggerVariablesHelper.cxx @@ -0,0 +1,644 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ + +#include "cmDebuggerVariablesHelper.h" + +#include +#include +#include +#include +#include +#include + +#include "cm_codecvt.hxx" + +#include "cmDebuggerStackFrame.h" +#include "cmDebuggerVariables.h" +#include "cmFileSet.h" +#include "cmGlobalGenerator.h" +#include "cmList.h" +#include "cmListFileCache.h" +#include "cmMakefile.h" +#include "cmPropertyMap.h" +#include "cmState.h" +#include "cmStateSnapshot.h" +#include "cmTarget.h" +#include "cmTest.h" +#include "cmValue.h" +#include "cmake.h" + +namespace cmDebugger { + +std::shared_ptr cmDebuggerVariablesHelper::Create( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + cmPolicies::PolicyMap const& policyMap) +{ + static std::map policyStatusString = { + { cmPolicies::PolicyStatus::OLD, "OLD" }, + { cmPolicies::PolicyStatus::WARN, "WARN" }, + { cmPolicies::PolicyStatus::NEW, "NEW" }, + { cmPolicies::PolicyStatus::REQUIRED_IF_USED, "REQUIRED_IF_USED" }, + { cmPolicies::PolicyStatus::REQUIRED_ALWAYS, "REQUIRED_ALWAYS" } + }; + + return std::make_shared( + variablesManager, name, supportsVariableType, [=]() { + std::vector ret; + ret.reserve(cmPolicies::CMPCOUNT); + for (int i = 0; i < cmPolicies::CMPCOUNT; ++i) { + if (policyMap.IsDefined(static_cast(i))) { + auto status = policyMap.Get(static_cast(i)); + std::ostringstream ss; + ss << "CMP" << std::setfill('0') << std::setw(4) << i; + ret.emplace_back(ss.str(), policyStatusString[status]); + } + } + return ret; + }); +} + +std::shared_ptr cmDebuggerVariablesHelper::CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + std::vector> const& list) +{ + if (list.empty()) { + return {}; + } + + auto listVariables = std::make_shared( + variablesManager, name, supportsVariableType, [=]() { + std::vector ret; + ret.reserve(list.size()); + for (auto const& kv : list) { + ret.emplace_back(kv.first, kv.second); + } + return ret; + }); + + listVariables->SetValue(std::to_string(list.size())); + return listVariables; +} + +std::shared_ptr cmDebuggerVariablesHelper::CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + cmBTStringRange const& entries) +{ + if (entries.empty()) { + return {}; + } + + auto sourceEntries = std::make_shared( + variablesManager, name, supportsVariableType); + + for (auto const& entry : entries) { + auto arrayVariables = std::make_shared( + variablesManager, entry.Value, supportsVariableType, [=]() { + cmList items{ entry.Value }; + std::vector ret; + ret.reserve(items.size()); + int i = 0; + for (std::string const& item : items) { + ret.emplace_back("[" + std::to_string(i++) + "]", item); + } + return ret; + }); + arrayVariables->SetEnableSorting(false); + sourceEntries->AddSubVariables(arrayVariables); + } + + sourceEntries->SetValue(std::to_string(entries.size())); + return sourceEntries; +} + +std::shared_ptr cmDebuggerVariablesHelper::CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + std::set const& values) +{ + if (values.empty()) { + return {}; + } + + auto arrayVariables = std::make_shared( + variablesManager, name, supportsVariableType, [=]() { + std::vector ret; + ret.reserve(values.size()); + int i = 0; + for (std::string const& value : values) { + ret.emplace_back("[" + std::to_string(i++) + "]", value); + } + return ret; + }); + arrayVariables->SetValue(std::to_string(values.size())); + arrayVariables->SetEnableSorting(false); + return arrayVariables; +} + +std::shared_ptr cmDebuggerVariablesHelper::CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + std::vector const& values) +{ + if (values.empty()) { + return {}; + } + + auto arrayVariables = std::make_shared( + variablesManager, name, supportsVariableType, [=]() { + std::vector ret; + ret.reserve(values.size()); + int i = 0; + for (std::string const& value : values) { + ret.emplace_back("[" + std::to_string(i++) + "]", value); + } + return ret; + }); + + arrayVariables->SetValue(std::to_string(values.size())); + arrayVariables->SetEnableSorting(false); + return arrayVariables; +} + +std::shared_ptr cmDebuggerVariablesHelper::CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + std::vector> const& list) +{ + if (list.empty()) { + return {}; + } + + auto variables = std::make_shared( + variablesManager, name, supportsVariableType, [=]() { + std::vector ret; + ret.reserve(list.size()); + int i = 0; + for (auto const& item : list) { + ret.emplace_back("[" + std::to_string(i++) + "]", item.Value); + } + + return ret; + }); + + variables->SetValue(std::to_string(list.size())); + variables->SetEnableSorting(false); + return variables; +} + +std::shared_ptr cmDebuggerVariablesHelper::CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, cmFileSet* fileSet) +{ + if (fileSet == nullptr) { + return {}; + } + + static auto visibilityString = [](cmFileSetVisibility visibility) { + switch (visibility) { + case cmFileSetVisibility::Private: + return "Private"; + case cmFileSetVisibility::Public: + return "Public"; + case cmFileSetVisibility::Interface: + return "Interface"; + default: + return "Unknown"; + } + }; + + auto variables = std::make_shared( + variablesManager, name, supportsVariableType, [=]() { + std::vector ret{ + { "Name", fileSet->GetName() }, + { "Type", fileSet->GetType() }, + { "Visibility", visibilityString(fileSet->GetVisibility()) }, + }; + + return ret; + }); + + variables->AddSubVariables(CreateIfAny(variablesManager, "Directories", + supportsVariableType, + fileSet->GetDirectoryEntries())); + variables->AddSubVariables(CreateIfAny(variablesManager, "Files", + supportsVariableType, + fileSet->GetFileEntries())); + return variables; +} + +std::shared_ptr cmDebuggerVariablesHelper::CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + std::vector const& fileSets) +{ + if (fileSets.empty()) { + return {}; + } + + auto fileSetsVariables = std::make_shared( + variablesManager, name, supportsVariableType); + + for (auto const& fileSet : fileSets) { + fileSetsVariables->AddSubVariables(CreateIfAny( + variablesManager, fileSet->GetName(), supportsVariableType, fileSet)); + } + + return fileSetsVariables; +} + +std::shared_ptr cmDebuggerVariablesHelper::CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + std::vector const& targets) +{ + if (targets.empty()) { + return {}; + } + + auto targetsVariables = std::make_shared( + variablesManager, name, supportsVariableType); + + for (auto const& target : targets) { + auto targetVariables = std::make_shared( + variablesManager, target->GetName(), supportsVariableType, [=]() { + std::vector ret = { + { "InstallPath", target->GetInstallPath() }, + { "IsAIX", target->IsAIX() }, + { "IsAndroidGuiExecutable", target->IsAndroidGuiExecutable() }, + { "IsAppBundleOnApple", target->IsAppBundleOnApple() }, + { "IsDLLPlatform", target->IsDLLPlatform() }, + { "IsExecutableWithExports", target->IsExecutableWithExports() }, + { "IsFrameworkOnApple", target->IsFrameworkOnApple() }, + { "IsImported", target->IsImported() }, + { "IsImportedGloballyVisible", target->IsImportedGloballyVisible() }, + { "IsPerConfig", target->IsPerConfig() }, + { "Name", target->GetName() }, + { "RuntimeInstallPath", target->GetRuntimeInstallPath() }, + { "Type", cmState::GetTargetTypeName(target->GetType()) }, + }; + + return ret; + }); + targetVariables->SetValue(cmState::GetTargetTypeName(target->GetType())); + + targetVariables->AddSubVariables(Create(variablesManager, "PolicyMap", + supportsVariableType, + target->GetPolicyMap())); + targetVariables->AddSubVariables( + CreateIfAny(variablesManager, "Properties", supportsVariableType, + target->GetProperties().GetList())); + + targetVariables->AddSubVariables( + CreateIfAny(variablesManager, "IncludeDirectories", supportsVariableType, + target->GetIncludeDirectoriesEntries())); + targetVariables->AddSubVariables(CreateIfAny(variablesManager, "Sources", + supportsVariableType, + target->GetSourceEntries())); + targetVariables->AddSubVariables( + CreateIfAny(variablesManager, "CompileDefinitions", supportsVariableType, + target->GetCompileDefinitionsEntries())); + targetVariables->AddSubVariables( + CreateIfAny(variablesManager, "CompileFeatures", supportsVariableType, + target->GetCompileFeaturesEntries())); + targetVariables->AddSubVariables( + CreateIfAny(variablesManager, "CompileOptions", supportsVariableType, + target->GetCompileOptionsEntries())); + targetVariables->AddSubVariables(CreateIfAny( + variablesManager, "CxxModuleHeaderSets", supportsVariableType, + target->GetCxxModuleHeaderSetsEntries())); + targetVariables->AddSubVariables( + CreateIfAny(variablesManager, "CxxModuleSets", supportsVariableType, + target->GetCxxModuleSetsEntries())); + targetVariables->AddSubVariables( + CreateIfAny(variablesManager, "HeaderSets", supportsVariableType, + target->GetHeaderSetsEntries())); + targetVariables->AddSubVariables(CreateIfAny( + variablesManager, "InterfaceCxxModuleHeaderSets", supportsVariableType, + target->GetInterfaceCxxModuleHeaderSetsEntries())); + targetVariables->AddSubVariables(CreateIfAny( + variablesManager, "InterfaceHeaderSets", supportsVariableType, + target->GetInterfaceHeaderSetsEntries())); + targetVariables->AddSubVariables( + CreateIfAny(variablesManager, "LinkDirectories", supportsVariableType, + target->GetLinkDirectoriesEntries())); + targetVariables->AddSubVariables(CreateIfAny( + variablesManager, "LinkImplementations", supportsVariableType, + target->GetLinkImplementationEntries())); + targetVariables->AddSubVariables(CreateIfAny( + variablesManager, "LinkInterfaceDirects", supportsVariableType, + target->GetLinkInterfaceDirectEntries())); + targetVariables->AddSubVariables(CreateIfAny( + variablesManager, "LinkInterfaceDirectExcludes", supportsVariableType, + target->GetLinkInterfaceDirectExcludeEntries())); + targetVariables->AddSubVariables( + CreateIfAny(variablesManager, "LinkInterfaces", supportsVariableType, + target->GetLinkInterfaceEntries())); + targetVariables->AddSubVariables( + CreateIfAny(variablesManager, "LinkOptions", supportsVariableType, + target->GetLinkOptionsEntries())); + targetVariables->AddSubVariables(CreateIfAny( + variablesManager, "SystemIncludeDirectories", supportsVariableType, + target->GetSystemIncludeDirectories())); + targetVariables->AddSubVariables(CreateIfAny(variablesManager, "Makefile", + supportsVariableType, + target->GetMakefile())); + targetVariables->AddSubVariables( + CreateIfAny(variablesManager, "GlobalGenerator", supportsVariableType, + target->GetGlobalGenerator())); + + std::vector allFileSets; + auto allFileSetNames = target->GetAllFileSetNames(); + allFileSets.reserve(allFileSetNames.size()); + for (auto const& fileSetName : allFileSetNames) { + allFileSets.emplace_back(target->GetFileSet(fileSetName)); + } + targetVariables->AddSubVariables(CreateIfAny( + variablesManager, "AllFileSets", supportsVariableType, allFileSets)); + + std::vector allInterfaceFileSets; + auto allInterfaceFileSetNames = target->GetAllInterfaceFileSets(); + allInterfaceFileSets.reserve(allInterfaceFileSetNames.size()); + for (auto const& interfaceFileSetName : allInterfaceFileSetNames) { + allInterfaceFileSets.emplace_back( + target->GetFileSet(interfaceFileSetName)); + } + targetVariables->AddSubVariables( + CreateIfAny(variablesManager, "AllInterfaceFileSets", + supportsVariableType, allInterfaceFileSets)); + + targetVariables->SetIgnoreEmptyStringEntries(true); + targetsVariables->AddSubVariables(targetVariables); + } + + targetsVariables->SetValue(std::to_string(targets.size())); + return targetsVariables; +} + +std::shared_ptr cmDebuggerVariablesHelper::Create( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + std::shared_ptr const& frame) +{ + auto variables = std::make_shared( + variablesManager, name, supportsVariableType, [=]() { + return std::vector{ { "CurrentLine", + frame->GetLine() } }; + }); + + auto closureKeys = frame->GetMakefile()->GetStateSnapshot().ClosureKeys(); + auto locals = std::make_shared( + variablesManager, "Locals", supportsVariableType, [=]() { + std::vector ret; + ret.reserve(closureKeys.size()); + for (auto const& key : closureKeys) { + ret.emplace_back( + key, frame->GetMakefile()->GetStateSnapshot().GetDefinition(key)); + } + return ret; + }); + locals->SetValue(std::to_string(closureKeys.size())); + variables->AddSubVariables(locals); + + std::function isDirectory = + [](std::string const& key) { + size_t pos1 = key.rfind("_DIR"); + size_t pos2 = key.rfind("_DIRECTORY"); + return !((pos1 == std::string::npos || pos1 != key.size() - 4) && + (pos2 == std::string::npos || pos2 != key.size() - 10)); + }; + auto directorySize = + std::count_if(closureKeys.begin(), closureKeys.end(), isDirectory); + auto directories = std::make_shared( + variablesManager, "Directories", supportsVariableType, [=]() { + std::vector ret; + ret.reserve(directorySize); + for (auto const& key : closureKeys) { + if (isDirectory(key)) { + ret.emplace_back( + key, frame->GetMakefile()->GetStateSnapshot().GetDefinition(key)); + } + } + return ret; + }); + directories->SetValue(std::to_string(directorySize)); + variables->AddSubVariables(directories); + + auto cacheVariables = std::make_shared( + variablesManager, "CacheVariables", supportsVariableType); + auto* state = frame->GetMakefile()->GetCMakeInstance()->GetState(); + auto keys = state->GetCacheEntryKeys(); + for (auto const& key : keys) { + auto entry = std::make_shared( + variablesManager, + key + ":" + + cmState::CacheEntryTypeToString(state->GetCacheEntryType(key)), + supportsVariableType, [=]() { + std::vector ret; + auto properties = state->GetCacheEntryPropertyList(key); + ret.reserve(properties.size() + 2); + for (auto const& propertyName : properties) { + ret.emplace_back(propertyName, + state->GetCacheEntryProperty(key, propertyName)); + } + + ret.emplace_back( + "TYPE", + cmState::CacheEntryTypeToString(state->GetCacheEntryType(key))); + ret.emplace_back("VALUE", state->GetCacheEntryValue(key)); + return ret; + }); + + entry->SetValue(state->GetCacheEntryValue(key)); + cacheVariables->AddSubVariables(entry); + } + + cacheVariables->SetValue(std::to_string(keys.size())); + variables->AddSubVariables(cacheVariables); + + auto targetVariables = + CreateIfAny(variablesManager, "Targets", supportsVariableType, + frame->GetMakefile()->GetOrderedTargets()); + + variables->AddSubVariables(targetVariables); + std::vector tests; + frame->GetMakefile()->GetTests( + frame->GetMakefile()->GetDefaultConfiguration(), tests); + variables->AddSubVariables( + CreateIfAny(variablesManager, "Tests", supportsVariableType, tests)); + + return variables; +} + +std::shared_ptr cmDebuggerVariablesHelper::CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, cmTest* test) +{ + if (test == nullptr) { + return {}; + } + + auto variables = std::make_shared( + variablesManager, name, supportsVariableType, [=]() { + std::vector ret{ + { "CommandExpandLists", test->GetCommandExpandLists() }, + { "Name", test->GetName() }, + { "OldStyle", test->GetOldStyle() }, + }; + + return ret; + }); + + variables->AddSubVariables(CreateIfAny( + variablesManager, "Command", supportsVariableType, test->GetCommand())); + + variables->AddSubVariables(CreateIfAny(variablesManager, "Properties", + supportsVariableType, + test->GetProperties().GetList())); + return variables; +} + +std::shared_ptr cmDebuggerVariablesHelper::CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + std::vector const& tests) +{ + if (tests.empty()) { + return {}; + } + + auto variables = std::make_shared( + variablesManager, name, supportsVariableType); + + for (auto const& test : tests) { + variables->AddSubVariables(CreateIfAny(variablesManager, test->GetName(), + supportsVariableType, test)); + } + variables->SetValue(std::to_string(tests.size())); + return variables; +} + +std::shared_ptr cmDebuggerVariablesHelper::CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, cmMakefile* mf) +{ + if (mf == nullptr) { + return {}; + } + + auto AppleSDKTypeString = [&](cmMakefile::AppleSDK sdk) { + switch (sdk) { + case cmMakefile::AppleSDK::MacOS: + return "MacOS"; + case cmMakefile::AppleSDK::IPhoneOS: + return "IPhoneOS"; + case cmMakefile::AppleSDK::IPhoneSimulator: + return "IPhoneSimulator"; + case cmMakefile::AppleSDK::AppleTVOS: + return "AppleTVOS"; + case cmMakefile::AppleSDK::AppleTVSimulator: + return "AppleTVSimulator"; + default: + return "Unknown"; + } + }; + + auto variables = std::make_shared( + variablesManager, name, supportsVariableType, [=]() { + std::vector ret = { + { "DefineFlags", mf->GetDefineFlags() }, + { "DirectoryId", mf->GetDirectoryId().String }, + { "IsRootMakefile", mf->IsRootMakefile() }, + { "HomeDirectory", mf->GetHomeDirectory() }, + { "HomeOutputDirectory", mf->GetHomeOutputDirectory() }, + { "CurrentSourceDirectory", mf->GetCurrentSourceDirectory() }, + { "CurrentBinaryDirectory", mf->GetCurrentBinaryDirectory() }, + { "PlatformIs32Bit", mf->PlatformIs32Bit() }, + { "PlatformIs64Bit", mf->PlatformIs64Bit() }, + { "PlatformIsx32", mf->PlatformIsx32() }, + { "AppleSDKType", AppleSDKTypeString(mf->GetAppleSDKType()) }, + { "PlatformIsAppleEmbedded", mf->PlatformIsAppleEmbedded() } + }; + + return ret; + }); + + variables->AddSubVariables(CreateIfAny( + variablesManager, "ListFiles", supportsVariableType, mf->GetListFiles())); + variables->AddSubVariables(CreateIfAny(variablesManager, "OutputFiles", + supportsVariableType, + mf->GetOutputFiles())); + + variables->SetIgnoreEmptyStringEntries(true); + variables->SetValue(mf->GetDirectoryId().String); + return variables; +} + +std::shared_ptr cmDebuggerVariablesHelper::CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, cmGlobalGenerator* gen) +{ + if (gen == nullptr) { + return {}; + } + + auto makeFileEncodingString = [](codecvt::Encoding encoding) { + switch (encoding) { + case codecvt::Encoding::None: + return "None"; + case codecvt::Encoding::UTF8: + return "UTF8"; + case codecvt::Encoding::UTF8_WITH_BOM: + return "UTF8_WITH_BOM"; + case codecvt::Encoding::ANSI: + return "ANSI"; + case codecvt::Encoding::ConsoleOutput: + return "ConsoleOutput"; + default: + return "Unknown"; + } + }; + + auto variables = std::make_shared( + variablesManager, name, supportsVariableType, [=]() { + std::vector ret = { + { "AllTargetName", gen->GetAllTargetName() }, + { "CleanTargetName", gen->GetCleanTargetName() }, + { "EditCacheCommand", gen->GetEditCacheCommand() }, + { "EditCacheTargetName", gen->GetEditCacheTargetName() }, + { "ExtraGeneratorName", gen->GetExtraGeneratorName() }, + { "ForceUnixPaths", gen->GetForceUnixPaths() }, + { "InstallLocalTargetName", gen->GetInstallLocalTargetName() }, + { "InstallStripTargetName", gen->GetInstallStripTargetName() }, + { "InstallTargetName", gen->GetInstallTargetName() }, + { "IsMultiConfig", gen->IsMultiConfig() }, + { "Name", gen->GetName() }, + { "MakefileEncoding", + makeFileEncodingString(gen->GetMakefileEncoding()) }, + { "PackageSourceTargetName", gen->GetPackageSourceTargetName() }, + { "PackageTargetName", gen->GetPackageTargetName() }, + { "PreinstallTargetName", gen->GetPreinstallTargetName() }, + { "NeedSymbolicMark", gen->GetNeedSymbolicMark() }, + { "RebuildCacheTargetName", gen->GetRebuildCacheTargetName() }, + { "TestTargetName", gen->GetTestTargetName() }, + { "UseLinkScript", gen->GetUseLinkScript() }, + }; + + return ret; + }); + + if (gen->GetInstallComponents() != nullptr) { + variables->AddSubVariables( + CreateIfAny(variablesManager, "InstallComponents", supportsVariableType, + *gen->GetInstallComponents())); + } + + variables->SetIgnoreEmptyStringEntries(true); + variables->SetValue(gen->GetName()); + + return variables; +} + +} // namespace cmDebugger diff --git a/Source/cmDebuggerVariablesHelper.h b/Source/cmDebuggerVariablesHelper.h new file mode 100644 index 0000000..9b11eaf --- /dev/null +++ b/Source/cmDebuggerVariablesHelper.h @@ -0,0 +1,106 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +#include +#include +#include +#include +#include + +#include "cmAlgorithms.h" +#include "cmPolicies.h" + +class cmFileSet; +class cmGlobalGenerator; +class cmMakefile; +class cmTarget; +class cmTest; + +namespace cmDebugger { +class cmDebuggerStackFrame; +class cmDebuggerVariables; +class cmDebuggerVariablesManager; +} + +template +class BT; + +namespace cmDebugger { + +class cmDebuggerVariablesHelper +{ + cmDebuggerVariablesHelper() = default; + +public: + static std::shared_ptr Create( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + cmPolicies::PolicyMap const& policyMap); + + static std::shared_ptr CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + std::vector> const& list); + + static std::shared_ptr CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + cmBTStringRange const& entries); + + static std::shared_ptr CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + std::set const& values); + + static std::shared_ptr CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + std::vector const& values); + + static std::shared_ptr CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + std::vector> const& list); + + static std::shared_ptr CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, cmFileSet* fileSet); + + static std::shared_ptr CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + std::vector const& fileSets); + + static std::shared_ptr CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, cmTest* test); + + static std::shared_ptr CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + std::vector const& tests); + + static std::shared_ptr CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + std::vector const& targets); + + static std::shared_ptr Create( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + std::shared_ptr const& frame); + + static std::shared_ptr CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, cmMakefile* mf); + + static std::shared_ptr CreateIfAny( + std::shared_ptr const& variablesManager, + std::string const& name, bool supportsVariableType, + cmGlobalGenerator* gen); +}; + +} // namespace cmDebugger diff --git a/Source/cmDebuggerVariablesManager.cxx b/Source/cmDebuggerVariablesManager.cxx new file mode 100644 index 0000000..9b9b476 --- /dev/null +++ b/Source/cmDebuggerVariablesManager.cxx @@ -0,0 +1,38 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ + +#include "cmDebuggerVariablesManager.h" + +#include + +#include +#include + +namespace cmDebugger { + +void cmDebuggerVariablesManager::RegisterHandler( + int64_t id, + std::function(dap::VariablesRequest const&)> + handler) +{ + VariablesHandlers[id] = std::move(handler); +} + +void cmDebuggerVariablesManager::UnregisterHandler(int64_t id) +{ + VariablesHandlers.erase(id); +} + +dap::array cmDebuggerVariablesManager::HandleVariablesRequest( + dap::VariablesRequest const& request) +{ + auto it = VariablesHandlers.find(request.variablesReference); + + if (it != VariablesHandlers.end()) { + return it->second(request); + } + + return dap::array(); +} + +} // namespace cmDebugger diff --git a/Source/cmDebuggerVariablesManager.h b/Source/cmDebuggerVariablesManager.h new file mode 100644 index 0000000..c219164 --- /dev/null +++ b/Source/cmDebuggerVariablesManager.h @@ -0,0 +1,40 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +#include +#include +#include +#include + +#include // IWYU pragma: keep + +namespace dap { +struct Variable; +struct VariablesRequest; +} + +namespace cmDebugger { + +class cmDebuggerVariablesManager +{ + std::unordered_map< + int64_t, + std::function(dap::VariablesRequest const&)>> + VariablesHandlers; + void RegisterHandler( + int64_t id, + std::function(dap::VariablesRequest const&)> + handler); + void UnregisterHandler(int64_t id); + friend class cmDebuggerVariables; + +public: + cmDebuggerVariablesManager() = default; + dap::array HandleVariablesRequest( + dap::VariablesRequest const& request); +}; + +} // namespace cmDebugger diff --git a/Source/cmMakefile.cxx b/Source/cmMakefile.cxx index 585924d..92ba2d4 100644 --- a/Source/cmMakefile.cxx +++ b/Source/cmMakefile.cxx @@ -68,6 +68,10 @@ # include "cmVariableWatch.h" #endif +#ifdef CMake_ENABLE_DEBUGGER +# include "cmDebuggerAdapter.h" +#endif + #ifndef __has_feature # define __has_feature(x) 0 #endif @@ -424,6 +428,13 @@ public: return argsValue; }); #endif +#ifdef CMake_ENABLE_DEBUGGER + if (this->Makefile->GetCMakeInstance()->GetDebugAdapter() != nullptr) { + this->Makefile->GetCMakeInstance() + ->GetDebugAdapter() + ->OnBeginFunctionCall(mf, lfc.FilePath, lff); + } +#endif } ~cmMakefileCall() @@ -434,6 +445,13 @@ public: this->Makefile->ExecutionStatusStack.pop_back(); --this->Makefile->RecursionDepth; this->Makefile->Backtrace = this->Makefile->Backtrace.Pop(); +#ifdef CMake_ENABLE_DEBUGGER + if (this->Makefile->GetCMakeInstance()->GetDebugAdapter() != nullptr) { + this->Makefile->GetCMakeInstance() + ->GetDebugAdapter() + ->OnEndFunctionCall(); + } +#endif } cmMakefileCall(const cmMakefileCall&) = delete; @@ -663,12 +681,33 @@ bool cmMakefile::ReadDependentFile(const std::string& filename, IncludeScope incScope(this, filenametoread, noPolicyScope); +#ifdef CMake_ENABLE_DEBUGGER + if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) { + this->GetCMakeInstance()->GetDebugAdapter()->OnBeginFileParse( + this, filenametoread); + } +#endif + cmListFile listFile; if (!listFile.ParseFile(filenametoread.c_str(), this->GetMessenger(), this->Backtrace)) { +#ifdef CMake_ENABLE_DEBUGGER + if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) { + this->GetCMakeInstance()->GetDebugAdapter()->OnEndFileParse(); + } +#endif + return false; } +#ifdef CMake_ENABLE_DEBUGGER + if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) { + this->GetCMakeInstance()->GetDebugAdapter()->OnEndFileParse(); + this->GetCMakeInstance()->GetDebugAdapter()->OnFileParsedSuccessfully( + filenametoread, listFile.Functions); + } +#endif + this->RunListFile(listFile, filenametoread); if (cmSystemTools::GetFatalErrorOccurred()) { incScope.Quiet(); @@ -764,12 +803,33 @@ bool cmMakefile::ReadListFile(const std::string& filename) ListFileScope scope(this, filenametoread); +#ifdef CMake_ENABLE_DEBUGGER + if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) { + this->GetCMakeInstance()->GetDebugAdapter()->OnBeginFileParse( + this, filenametoread); + } +#endif + cmListFile listFile; if (!listFile.ParseFile(filenametoread.c_str(), this->GetMessenger(), this->Backtrace)) { +#ifdef CMake_ENABLE_DEBUGGER + if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) { + this->GetCMakeInstance()->GetDebugAdapter()->OnEndFileParse(); + } +#endif + return false; } +#ifdef CMake_ENABLE_DEBUGGER + if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) { + this->GetCMakeInstance()->GetDebugAdapter()->OnEndFileParse(); + this->GetCMakeInstance()->GetDebugAdapter()->OnFileParsedSuccessfully( + filenametoread, listFile.Functions); + } +#endif + this->RunListFile(listFile, filenametoread); if (cmSystemTools::GetFatalErrorOccurred()) { scope.Quiet(); @@ -791,6 +851,13 @@ bool cmMakefile::ReadListFileAsString(const std::string& content, return false; } +#ifdef CMake_ENABLE_DEBUGGER + if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) { + this->GetCMakeInstance()->GetDebugAdapter()->OnFileParsedSuccessfully( + filenametoread, listFile.Functions); + } +#endif + this->RunListFile(listFile, filenametoread); if (cmSystemTools::GetFatalErrorOccurred()) { scope.Quiet(); @@ -1658,11 +1725,33 @@ void cmMakefile::Configure() assert(cmSystemTools::FileExists(currentStart, true)); this->AddDefinition("CMAKE_PARENT_LIST_FILE", currentStart); +#ifdef CMake_ENABLE_DEBUGGER + if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) { + this->GetCMakeInstance()->GetDebugAdapter()->OnBeginFileParse( + this, currentStart); + } +#endif + cmListFile listFile; if (!listFile.ParseFile(currentStart.c_str(), this->GetMessenger(), this->Backtrace)) { +#ifdef CMake_ENABLE_DEBUGGER + if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) { + this->GetCMakeInstance()->GetDebugAdapter()->OnEndFileParse(); + } +#endif + return; } + +#ifdef CMake_ENABLE_DEBUGGER + if (this->GetCMakeInstance()->GetDebugAdapter() != nullptr) { + this->GetCMakeInstance()->GetDebugAdapter()->OnEndFileParse(); + this->GetCMakeInstance()->GetDebugAdapter()->OnFileParsedSuccessfully( + currentStart, listFile.Functions); + } +#endif + if (this->IsRootMakefile()) { bool hasVersion = false; // search for the right policy command @@ -3769,6 +3858,12 @@ void cmMakefile::DisplayStatus(const std::string& message, float s) const return; } cm->UpdateProgress(message, s); + +#ifdef CMake_ENABLE_DEBUGGER + if (cm->GetDebugAdapter() != nullptr) { + cm->GetDebugAdapter()->OnMessageOutput(MessageType::MESSAGE, message); + } +#endif } std::string cmMakefile::GetModulesFile(const std::string& filename, diff --git a/Source/cmMessageCommand.cxx b/Source/cmMessageCommand.cxx index baf40f8..68b3a5d 100644 --- a/Source/cmMessageCommand.cxx +++ b/Source/cmMessageCommand.cxx @@ -3,6 +3,7 @@ #include "cmMessageCommand.h" #include +#include #include #include @@ -19,6 +20,10 @@ #include "cmSystemTools.h" #include "cmake.h" +#ifdef CMake_ENABLE_DEBUGGER +# include "cmDebuggerAdapter.h" +#endif + namespace { enum class CheckingType @@ -202,6 +207,12 @@ bool cmMessageCommand(std::vector const& args, case Message::LogLevel::LOG_NOTICE: cmSystemTools::Message(IndentText(message, mf)); +#ifdef CMake_ENABLE_DEBUGGER + if (mf.GetCMakeInstance()->GetDebugAdapter() != nullptr) { + mf.GetCMakeInstance()->GetDebugAdapter()->OnMessageOutput(type, + message); + } +#endif break; case Message::LogLevel::LOG_STATUS: diff --git a/Source/cmMessenger.cxx b/Source/cmMessenger.cxx index 7de8936..4e975d1 100644 --- a/Source/cmMessenger.cxx +++ b/Source/cmMessenger.cxx @@ -16,6 +16,10 @@ #include "cmsys/Terminal.h" +#ifdef CMake_ENABLE_DEBUGGER +# include "cmDebuggerAdapter.h" +#endif + MessageType cmMessenger::ConvertMessageType(MessageType t) const { if (t == MessageType::AUTHOR_WARNING || t == MessageType::AUTHOR_ERROR) { @@ -207,6 +211,12 @@ void cmMessenger::DisplayMessage(MessageType t, const std::string& text, PrintCallStack(msg, backtrace, this->TopSource); displayMessage(t, msg); + +#ifdef CMake_ENABLE_DEBUGGER + if (DebuggerAdapter != nullptr) { + DebuggerAdapter->OnMessageOutput(t, msg.str()); + } +#endif } void cmMessenger::PrintBacktraceTitle(std::ostream& out, diff --git a/Source/cmMessenger.h b/Source/cmMessenger.h index 451add0..bdefb00 100644 --- a/Source/cmMessenger.h +++ b/Source/cmMessenger.h @@ -5,6 +5,7 @@ #include "cmConfigure.h" // IWYU pragma: keep #include +#include #include #include @@ -12,6 +13,12 @@ #include "cmListFileCache.h" #include "cmMessageType.h" +#ifdef CMake_ENABLE_DEBUGGER +namespace cmDebugger { +class cmDebuggerAdapter; +} +#endif + class cmMessenger { public: @@ -55,6 +62,13 @@ public: // Print the top of a backtrace. void PrintBacktraceTitle(std::ostream& out, cmListFileBacktrace const& bt) const; +#ifdef CMake_ENABLE_DEBUGGER + void SetDebuggerAdapter( + std::shared_ptr const& debuggerAdapter) + { + DebuggerAdapter = debuggerAdapter; + } +#endif private: bool IsMessageTypeVisible(MessageType t) const; @@ -66,4 +80,7 @@ private: bool SuppressDeprecatedWarnings = false; bool DevWarningsAsErrors = false; bool DeprecatedWarningsAsErrors = false; +#ifdef CMake_ENABLE_DEBUGGER + std::shared_ptr DebuggerAdapter; +#endif }; diff --git a/Source/cmake.cxx b/Source/cmake.cxx index c5b467d..0a1e7ab 100644 --- a/Source/cmake.cxx +++ b/Source/cmake.cxx @@ -38,6 +38,10 @@ #include "cmCMakePresetsGraph.h" #include "cmCommandLineArgument.h" #include "cmCommands.h" +#ifdef CMake_ENABLE_DEBUGGER +# include "cmDebuggerAdapter.h" +# include "cmDebuggerPipeConnection.h" +#endif #include "cmDocumentation.h" #include "cmDocumentationEntry.h" #include "cmDuration.h" @@ -411,6 +415,11 @@ Json::Value cmake::ReportCapabilitiesJson() const obj["fileApi"] = cmFileAPI::ReportCapabilities(); obj["serverMode"] = false; obj["tls"] = static_cast(curlVersion->features & CURL_VERSION_SSL); +# ifdef CMake_ENABLE_DEBUGGER + obj["debugger"] = true; +# else + obj["debugger"] = false; +# endif return obj; } @@ -617,6 +626,13 @@ bool cmake::SetCacheArgs(const std::vector& args) }; auto ScriptLambda = [&](std::string const& path, cmake* state) -> bool { +#ifdef CMake_ENABLE_DEBUGGER + // Script mode doesn't hit the usual code path in cmake::Run() that starts + // the debugger, so start it manually here instead. + if (!this->StartDebuggerIfEnabled()) { + return false; + } +#endif // Register fake project commands that hint misuse in script mode. GetProjectCommandsInScriptMode(state->GetState()); // Documented behavior of CMAKE{,_CURRENT}_{SOURCE,BINARY}_DIR is to be @@ -1233,7 +1249,52 @@ void cmake::SetArgs(const std::vector& args) "CMAKE_COMPILE_WARNING_AS_ERROR variable.\n"; state->SetIgnoreWarningAsError(true); return true; - } } + } }, + CommandArgument{ "--debugger", CommandArgument::Values::Zero, + [](std::string const&, cmake* state) -> bool { +#ifdef CMake_ENABLE_DEBUGGER + std::cout << "Running with debugger on.\n"; + state->SetDebuggerOn(true); + return true; +#else + static_cast(state); + cmSystemTools::Error( + "CMake was not built with support for --debugger"); + return false; +#endif + } }, + CommandArgument{ "--debugger-pipe", + "No path specified for --debugger-pipe", + CommandArgument::Values::One, + [](std::string const& value, cmake* state) -> bool { +#ifdef CMake_ENABLE_DEBUGGER + state->DebuggerPipe = value; + return true; +#else + static_cast(value); + static_cast(state); + cmSystemTools::Error("CMake was not built with support " + "for --debugger-pipe"); + return false; +#endif + } }, + CommandArgument{ + "--debugger-dap-log", "No file specified for --debugger-dap-log", + CommandArgument::Values::One, + [](std::string const& value, cmake* state) -> bool { +#ifdef CMake_ENABLE_DEBUGGER + std::string path = cmSystemTools::CollapseFullPath(value); + cmSystemTools::ConvertToUnixSlashes(path); + state->DebuggerDapLogFile = path; + return true; +#else + static_cast(value); + static_cast(state); + cmSystemTools::Error( + "CMake was not built with support for --debugger-dap-log"); + return false; +#endif + } }, }; #if defined(CMAKE_HAVE_VS_GENERATORS) @@ -2618,6 +2679,52 @@ void cmake::PreLoadCMakeFiles() } } +#ifdef CMake_ENABLE_DEBUGGER + +bool cmake::StartDebuggerIfEnabled() +{ + if (!this->GetDebuggerOn()) { + return true; + } + + if (DebugAdapter == nullptr) { + if (this->GetDebuggerPipe().empty()) { + std::cerr + << "Error: --debugger-pipe must be set when debugging is enabled.\n"; + return false; + } + + try { + DebugAdapter = std::make_shared( + std::make_shared( + this->GetDebuggerPipe()), + this->GetDebuggerDapLogFile()); + } catch (const std::runtime_error& error) { + std::cerr << "Error: Failed to create debugger adapter.\n"; + std::cerr << error.what() << "\n"; + return false; + } + Messenger->SetDebuggerAdapter(DebugAdapter); + } + + return true; +} + +void cmake::StopDebuggerIfNeeded(int exitCode) +{ + if (!this->GetDebuggerOn()) { + return; + } + + // The debug adapter may have failed to start (e.g. invalid pipe path). + if (DebugAdapter != nullptr) { + DebugAdapter->ReportExitCode(exitCode); + DebugAdapter.reset(); + } +} + +#endif + // handle a command line invocation int cmake::Run(const std::vector& args, bool noconfigure) { @@ -2707,6 +2814,12 @@ int cmake::Run(const std::vector& args, bool noconfigure) return 0; } +#ifdef CMake_ENABLE_DEBUGGER + if (!this->StartDebuggerIfEnabled()) { + return -1; + } +#endif + int ret = this->Configure(); if (ret) { #if defined(CMAKE_HAVE_VS_GENERATORS) diff --git a/Source/cmake.h b/Source/cmake.h index 955ec4f..d394a3e 100644 --- a/Source/cmake.h +++ b/Source/cmake.h @@ -37,6 +37,13 @@ #endif class cmConfigureLog; + +#ifdef CMake_ENABLE_DEBUGGER +namespace cmDebugger { +class cmDebuggerAdapter; +} +#endif + class cmExternalMakefileProjectGeneratorFactory; class cmFileAPI; class cmFileTimeCache; @@ -662,6 +669,23 @@ public: } #endif +#ifdef CMake_ENABLE_DEBUGGER + bool GetDebuggerOn() const { return this->DebuggerOn; } + std::string GetDebuggerPipe() const { return this->DebuggerPipe; } + std::string GetDebuggerDapLogFile() const + { + return this->DebuggerDapLogFile; + } + void SetDebuggerOn(bool b) { this->DebuggerOn = b; } + bool StartDebuggerIfEnabled(); + void StopDebuggerIfNeeded(int exitCode); + std::shared_ptr GetDebugAdapter() + const noexcept + { + return this->DebugAdapter; + } +#endif + protected: void RunCheckForUnusedVariables(); int HandleDeleteCacheVariables(const std::string& var); @@ -802,6 +826,13 @@ private: std::unique_ptr ProfilingOutput; #endif +#ifdef CMake_ENABLE_DEBUGGER + std::shared_ptr DebugAdapter; + bool DebuggerOn = false; + std::string DebuggerPipe; + std::string DebuggerDapLogFile; +#endif + public: static cmDocumentationEntry CMAKE_STANDARD_OPTIONS_TABLE[18]; }; diff --git a/Source/cmakemain.cxx b/Source/cmakemain.cxx index ad27443..ced83dc 100644 --- a/Source/cmakemain.cxx +++ b/Source/cmakemain.cxx @@ -392,8 +392,14 @@ int do_cmake(int ac, char const* const* av) // Always return a non-negative value. Windows tools do not always // interpret negative return values as errors. if (res != 0) { +#ifdef CMake_ENABLE_DEBUGGER + cm.StopDebuggerIfNeeded(1); +#endif return 1; } +#ifdef CMake_ENABLE_DEBUGGER + cm.StopDebuggerIfNeeded(0); +#endif return 0; } diff --git a/Tests/CMakeLib/CMakeLists.txt b/Tests/CMakeLib/CMakeLists.txt index 944b328..5c14de2 100644 --- a/Tests/CMakeLib/CMakeLists.txt +++ b/Tests/CMakeLib/CMakeLists.txt @@ -32,6 +32,16 @@ set(CMakeLib_TESTS testCMExtEnumSet.cxx testList.cxx ) +if(CMake_ENABLE_DEBUGGER) + list(APPEND CMakeLib_TESTS + testDebuggerAdapter.cxx + testDebuggerAdapterPipe.cxx + testDebuggerBreakpointManager.cxx + testDebuggerVariables.cxx + testDebuggerVariablesHelper.cxx + testDebuggerVariablesManager.cxx + ) +endif() if (CMake_TEST_FILESYSTEM_PATH OR NOT CMake_HAVE_CXX_FILESYSTEM) list(APPEND CMakeLib_TESTS testCMFilesystemPath.cxx) endif() @@ -78,3 +88,18 @@ add_subdirectory(PseudoMemcheck) add_executable(testAffinity testAffinity.cxx) target_link_libraries(testAffinity CMakeLib) + +if(CMake_ENABLE_DEBUGGER) + add_executable(testDebuggerNamedPipe testDebuggerNamedPipe.cxx) + target_link_libraries(testDebuggerNamedPipe PRIVATE CMakeLib) + set(testDebuggerNamedPipe_Project_ARGS + "$" ${CMAKE_CURRENT_SOURCE_DIR}/DebuggerSample ${CMAKE_CURRENT_BINARY_DIR}/DebuggerSample + ) + set(testDebuggerNamedPipe_Script_ARGS + "$" ${CMAKE_CURRENT_SOURCE_DIR}/DebuggerSample/script.cmake + ) + foreach(case Project Script) + add_test(NAME CMakeLib.testDebuggerNamedPipe-${case} COMMAND testDebuggerNamedPipe ${testDebuggerNamedPipe_${case}_ARGS}) + set_property(TEST CMakeLib.testDebuggerNamedPipe-${case} PROPERTY TIMEOUT 300) + endforeach() +endif() diff --git a/Tests/CMakeLib/DebuggerSample/CMakeLists.txt b/Tests/CMakeLib/DebuggerSample/CMakeLists.txt new file mode 100644 index 0000000..8f8603a --- /dev/null +++ b/Tests/CMakeLib/DebuggerSample/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.26) +project(DebuggerSample NONE) +message("Hello CMake Debugger") + +# There are concerns that because the debugger uses libuv for pipe +# communication, libuv may register a SIGCHILD handler that interferes with +# the existing handler used by kwsys process management. Test this case with a +# simple external process. +execute_process(COMMAND "${CMAKE_COMMAND}" -E echo test) diff --git a/Tests/CMakeLib/DebuggerSample/script.cmake b/Tests/CMakeLib/DebuggerSample/script.cmake new file mode 100644 index 0000000..4c0c00a --- /dev/null +++ b/Tests/CMakeLib/DebuggerSample/script.cmake @@ -0,0 +1 @@ +message(STATUS "This is an example script") diff --git a/Tests/CMakeLib/testCommon.h b/Tests/CMakeLib/testCommon.h new file mode 100644 index 0000000..bd2d54e --- /dev/null +++ b/Tests/CMakeLib/testCommon.h @@ -0,0 +1,30 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include +#include +#include + +#define ASSERT_TRUE(x) \ + do { \ + if (!(x)) { \ + std::cout << "ASSERT_TRUE(" #x ") failed on line " << __LINE__ << "\n"; \ + return false; \ + } \ + } while (false) + +inline int runTests(std::vector> const& tests) +{ + for (auto const& test : tests) { + if (!test()) { + return 1; + } + std::cout << "."; + } + + std::cout << " Passed" << std::endl; + return 0; +} + +#define BOOL_STRING(b) ((b) ? "TRUE" : "FALSE") diff --git a/Tests/CMakeLib/testDebugger.h b/Tests/CMakeLib/testDebugger.h new file mode 100644 index 0000000..8ba21f6 --- /dev/null +++ b/Tests/CMakeLib/testDebugger.h @@ -0,0 +1,99 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include +#include + +#include "cmDebuggerAdapter.h" +#include "cmDebuggerProtocol.h" +#include "cmListFileCache.h" +#include "cmMessenger.h" +#include +#include +#include + +#include "testCommon.h" + +#define ASSERT_VARIABLE(x, expectedName, expectedValue, expectedType) \ + do { \ + ASSERT_TRUE(x.name == expectedName); \ + ASSERT_TRUE(x.value == expectedValue); \ + ASSERT_TRUE(x.type.value() == expectedType); \ + ASSERT_TRUE(x.evaluateName.has_value() == false); \ + if (std::string(expectedType) == "collection") { \ + ASSERT_TRUE(x.variablesReference != 0); \ + } \ + } while (false) + +#define ASSERT_VARIABLE_REFERENCE(x, expectedName, expectedValue, \ + expectedType, expectedReference) \ + do { \ + ASSERT_VARIABLE(x, expectedName, expectedValue, expectedType); \ + ASSERT_TRUE(x.variablesReference == (expectedReference)); \ + } while (false) + +#define ASSERT_VARIABLE_REFERENCE_NOT_ZERO(x, expectedName, expectedValue, \ + expectedType) \ + do { \ + ASSERT_VARIABLE(x, expectedName, expectedValue, expectedType); \ + ASSERT_TRUE(x.variablesReference != 0); \ + } while (false) + +#define ASSERT_BREAKPOINT(x, expectedId, expectedLine, sourcePath, \ + isVerified) \ + do { \ + ASSERT_TRUE(x.id.has_value()); \ + ASSERT_TRUE(x.id.value() == expectedId); \ + ASSERT_TRUE(x.line.has_value()); \ + ASSERT_TRUE(x.line.value() == expectedLine); \ + ASSERT_TRUE(x.source.has_value()); \ + ASSERT_TRUE(x.source.value().path.has_value()); \ + ASSERT_TRUE(x.source.value().path.value() == sourcePath); \ + ASSERT_TRUE(x.verified == isVerified); \ + } while (false) + +class DebuggerTestHelper +{ + std::shared_ptr Client2Debugger = dap::pipe(); + std::shared_ptr Debugger2Client = dap::pipe(); + +public: + std::unique_ptr Client = dap::Session::create(); + std::unique_ptr Debugger = dap::Session::create(); + void bind() + { + auto client2server = dap::pipe(); + auto server2client = dap::pipe(); + Client->bind(server2client, client2server); + Debugger->bind(client2server, server2client); + } + std::vector CreateListFileFunctions(const char* str, + const char* filename) + { + cmMessenger messenger; + cmListFileBacktrace backtrace; + cmListFile listfile; + listfile.ParseString(str, filename, &messenger, backtrace); + return listfile.Functions; + } +}; + +class ScopedThread +{ +public: + template + explicit ScopedThread(Args&&... args) + : Thread(std::forward(args)...) + { + } + + ~ScopedThread() + { + if (Thread.joinable()) + Thread.join(); + } + +private: + std::thread Thread; +}; diff --git a/Tests/CMakeLib/testDebuggerAdapter.cxx b/Tests/CMakeLib/testDebuggerAdapter.cxx new file mode 100644 index 0000000..394986b --- /dev/null +++ b/Tests/CMakeLib/testDebuggerAdapter.cxx @@ -0,0 +1,173 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "cmDebuggerAdapter.h" +#include "cmDebuggerProtocol.h" +#include "cmVersionConfig.h" + +#include "testCommon.h" +#include "testDebugger.h" + +class DebuggerLocalConnection : public cmDebugger::cmDebuggerConnection +{ +public: + DebuggerLocalConnection() + : ClientToDebugger(dap::pipe()) + , DebuggerToClient(dap::pipe()) + { + } + + bool StartListening(std::string& errorMessage) override + { + errorMessage = ""; + return true; + } + void WaitForConnection() override {} + + std::shared_ptr GetReader() override + { + return ClientToDebugger; + }; + + std::shared_ptr GetWriter() override + { + return DebuggerToClient; + } + + std::shared_ptr ClientToDebugger; + std::shared_ptr DebuggerToClient; +}; + +bool testBasicProtocol() +{ + std::promise debuggerAdapterInitializedPromise; + std::future debuggerAdapterInitializedFuture = + debuggerAdapterInitializedPromise.get_future(); + + std::promise initializedEventReceivedPromise; + std::future initializedEventReceivedFuture = + initializedEventReceivedPromise.get_future(); + + std::promise exitedEventReceivedPromise; + std::future exitedEventReceivedFuture = + exitedEventReceivedPromise.get_future(); + + std::promise terminatedEventReceivedPromise; + std::future terminatedEventReceivedFuture = + terminatedEventReceivedPromise.get_future(); + + std::promise threadStartedPromise; + std::future threadStartedFuture = threadStartedPromise.get_future(); + + std::promise threadExitedPromise; + std::future threadExitedFuture = threadExitedPromise.get_future(); + + std::promise disconnectResponseReceivedPromise; + std::future disconnectResponseReceivedFuture = + disconnectResponseReceivedPromise.get_future(); + + auto futureTimeout = std::chrono::seconds(60); + + auto connection = std::make_shared(); + std::unique_ptr client = dap::Session::create(); + client->registerHandler([&](const dap::InitializedEvent& e) { + (void)e; + initializedEventReceivedPromise.set_value(true); + }); + client->registerHandler([&](const dap::ExitedEvent& e) { + (void)e; + exitedEventReceivedPromise.set_value(true); + }); + client->registerHandler([&](const dap::TerminatedEvent& e) { + (void)e; + terminatedEventReceivedPromise.set_value(true); + }); + client->registerHandler([&](const dap::ThreadEvent& e) { + if (e.reason == "started") { + threadStartedPromise.set_value(true); + } else if (e.reason == "exited") { + threadExitedPromise.set_value(true); + } + }); + + client->bind(connection->DebuggerToClient, connection->ClientToDebugger); + + ScopedThread debuggerThread([&]() -> int { + std::shared_ptr debuggerAdapter = + std::make_shared( + 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; + }); + + dap::CMakeInitializeRequest initializeRequest; + auto initializeResponse = client->send(initializeRequest).get(); + 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(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); + ASSERT_TRUE(terminatedEventReceivedFuture.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; +} + +int testDebuggerAdapter(int, char*[]) +{ + return runTests(std::vector>{ + testBasicProtocol, + }); +} diff --git a/Tests/CMakeLib/testDebuggerAdapterPipe.cxx b/Tests/CMakeLib/testDebuggerAdapterPipe.cxx new file mode 100644 index 0000000..643661d --- /dev/null +++ b/Tests/CMakeLib/testDebuggerAdapterPipe.cxx @@ -0,0 +1,184 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "cmDebuggerAdapter.h" +#include "cmDebuggerPipeConnection.h" +#include "cmDebuggerProtocol.h" +#include "cmVersionConfig.h" + +#ifdef _WIN32 +# include "cmCryptoHash.h" +# include "cmSystemTools.h" +#endif + +#include "testCommon.h" +#include "testDebugger.h" + +bool testProtocolWithPipes() +{ + std::promise debuggerConnectionCreatedPromise; + std::future debuggerConnectionCreatedFuture = + debuggerConnectionCreatedPromise.get_future(); + + std::future startedListeningFuture; + + std::promise debuggerAdapterInitializedPromise; + std::future debuggerAdapterInitializedFuture = + debuggerAdapterInitializedPromise.get_future(); + + std::promise initializedEventReceivedPromise; + std::future initializedEventReceivedFuture = + initializedEventReceivedPromise.get_future(); + + std::promise exitedEventReceivedPromise; + std::future exitedEventReceivedFuture = + exitedEventReceivedPromise.get_future(); + + std::promise terminatedEventReceivedPromise; + std::future terminatedEventReceivedFuture = + terminatedEventReceivedPromise.get_future(); + + std::promise threadStartedPromise; + std::future threadStartedFuture = threadStartedPromise.get_future(); + + std::promise threadExitedPromise; + std::future threadExitedFuture = threadExitedPromise.get_future(); + + std::promise disconnectResponseReceivedPromise; + std::future disconnectResponseReceivedFuture = + disconnectResponseReceivedPromise.get_future(); + + auto futureTimeout = std::chrono::seconds(60); + +#ifdef _WIN32 + std::string namedPipe = R"(\\.\pipe\LOCAL\CMakeDebuggerPipe2_)" + + cmCryptoHash(cmCryptoHash::AlgoSHA256) + .HashString(cmSystemTools::GetCurrentWorkingDirectory()); +#else + std::string namedPipe = "CMakeDebuggerPipe2"; +#endif + + std::unique_ptr client = dap::Session::create(); + client->registerHandler([&](const dap::InitializedEvent& e) { + (void)e; + initializedEventReceivedPromise.set_value(true); + }); + client->registerHandler([&](const dap::ExitedEvent& e) { + (void)e; + exitedEventReceivedPromise.set_value(true); + }); + client->registerHandler([&](const dap::TerminatedEvent& e) { + (void)e; + terminatedEventReceivedPromise.set_value(true); + }); + client->registerHandler([&](const dap::ThreadEvent& 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(namedPipe); + startedListeningFuture = connection->StartedListening.get_future(); + debuggerConnectionCreatedPromise.set_value(); + std::shared_ptr debuggerAdapter = + std::make_shared( + 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 (const std::runtime_error& 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(namedPipe); + client2Debugger->Start(); + 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; +} + +int testDebuggerAdapterPipe(int, char*[]) +{ + return runTests(std::vector>{ + testProtocolWithPipes, + }); +} diff --git a/Tests/CMakeLib/testDebuggerBreakpointManager.cxx b/Tests/CMakeLib/testDebuggerBreakpointManager.cxx new file mode 100644 index 0000000..83734ea --- /dev/null +++ b/Tests/CMakeLib/testDebuggerBreakpointManager.cxx @@ -0,0 +1,172 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "cmDebuggerBreakpointManager.h" +#include "cmDebuggerSourceBreakpoint.h" // IWYU pragma: keep +#include "cmListFileCache.h" + +#include "testCommon.h" +#include "testDebugger.h" + +static bool testHandleBreakpointRequestBeforeFileIsLoaded() +{ + // Arrange + DebuggerTestHelper helper; + cmDebugger::cmDebuggerBreakpointManager breakpointManager( + helper.Debugger.get()); + helper.bind(); + dap::SetBreakpointsRequest setBreakpointRequest; + std::string sourcePath = "C:/CMakeLists.txt"; + setBreakpointRequest.source.path = sourcePath; + dap::array sourceBreakpoints(3); + sourceBreakpoints[0].line = 1; + sourceBreakpoints[1].line = 2; + sourceBreakpoints[2].line = 3; + setBreakpointRequest.breakpoints = sourceBreakpoints; + + // Act + auto got = helper.Client->send(setBreakpointRequest).get(); + + // Assert + auto& response = got.response; + ASSERT_TRUE(!got.error); + ASSERT_TRUE(response.breakpoints.size() == sourceBreakpoints.size()); + ASSERT_BREAKPOINT(response.breakpoints[0], 0, sourceBreakpoints[0].line, + sourcePath, false); + ASSERT_BREAKPOINT(response.breakpoints[1], 1, sourceBreakpoints[1].line, + sourcePath, false); + ASSERT_BREAKPOINT(response.breakpoints[2], 2, sourceBreakpoints[2].line, + sourcePath, false); + return true; +} + +static bool testHandleBreakpointRequestAfterFileIsLoaded() +{ + // Arrange + DebuggerTestHelper helper; + std::atomic notExpectBreakpointEvents(true); + helper.Client->registerHandler([&](const dap::BreakpointEvent&) { + notExpectBreakpointEvents.store(false); + }); + + cmDebugger::cmDebuggerBreakpointManager breakpointManager( + helper.Debugger.get()); + helper.bind(); + std::string sourcePath = "C:/CMakeLists.txt"; + std::vector functions = helper.CreateListFileFunctions( + "# Comment1\nset(var1 foo)\n# Comment2\nset(var2\nbar)\n", + sourcePath.c_str()); + + breakpointManager.SourceFileLoaded(sourcePath, functions); + dap::SetBreakpointsRequest setBreakpointRequest; + setBreakpointRequest.source.path = sourcePath; + dap::array sourceBreakpoints(5); + sourceBreakpoints[0].line = 1; + sourceBreakpoints[1].line = 2; + sourceBreakpoints[2].line = 3; + sourceBreakpoints[3].line = 4; + sourceBreakpoints[4].line = 5; + setBreakpointRequest.breakpoints = sourceBreakpoints; + + // Act + auto got = helper.Client->send(setBreakpointRequest).get(); + + // Assert + auto& response = got.response; + ASSERT_TRUE(!got.error); + ASSERT_TRUE(response.breakpoints.size() == sourceBreakpoints.size()); + // Line 1 is a comment. Move it to next valid function, which is line 2. + ASSERT_BREAKPOINT(response.breakpoints[0], 0, 2, sourcePath, true); + ASSERT_BREAKPOINT(response.breakpoints[1], 1, sourceBreakpoints[1].line, + sourcePath, true); + // Line 3 is a comment. Move it to next valid function, which is line 4. + ASSERT_BREAKPOINT(response.breakpoints[2], 2, 4, sourcePath, true); + ASSERT_BREAKPOINT(response.breakpoints[3], 3, sourceBreakpoints[3].line, + sourcePath, true); + // Line 5 is the 2nd part of line 4 function. No valid function after line 5, + // show the breakpoint at line 4. + ASSERT_BREAKPOINT(response.breakpoints[4], 4, sourceBreakpoints[3].line, + sourcePath, true); + + ASSERT_TRUE(notExpectBreakpointEvents.load()); + + return true; +} + +static bool testSourceFileLoadedAfterHandleBreakpointRequest() +{ + // Arrange + DebuggerTestHelper helper; + std::vector breakpointEvents; + std::atomic remainingBreakpointEvents(5); + std::promise allBreakpointEventsReceivedPromise; + std::future allBreakpointEventsReceivedFuture = + allBreakpointEventsReceivedPromise.get_future(); + helper.Client->registerHandler([&](const dap::BreakpointEvent& event) { + breakpointEvents.emplace_back(event); + if (--remainingBreakpointEvents == 0) { + allBreakpointEventsReceivedPromise.set_value(); + } + }); + cmDebugger::cmDebuggerBreakpointManager breakpointManager( + helper.Debugger.get()); + helper.bind(); + dap::SetBreakpointsRequest setBreakpointRequest; + std::string sourcePath = "C:/CMakeLists.txt"; + setBreakpointRequest.source.path = sourcePath; + dap::array sourceBreakpoints(5); + sourceBreakpoints[0].line = 1; + sourceBreakpoints[1].line = 2; + sourceBreakpoints[2].line = 3; + sourceBreakpoints[3].line = 4; + sourceBreakpoints[4].line = 5; + setBreakpointRequest.breakpoints = sourceBreakpoints; + std::vector functions = helper.CreateListFileFunctions( + "# Comment1\nset(var1 foo)\n# Comment2\nset(var2\nbar)\n", + sourcePath.c_str()); + auto got = helper.Client->send(setBreakpointRequest).get(); + + // Act + breakpointManager.SourceFileLoaded(sourcePath, functions); + ASSERT_TRUE(allBreakpointEventsReceivedFuture.wait_for( + std::chrono::seconds(10)) == std::future_status::ready); + + // Assert + ASSERT_TRUE(breakpointEvents.size() > 0); + // Line 1 is a comment. Move it to next valid function, which is line 2. + ASSERT_BREAKPOINT(breakpointEvents[0].breakpoint, 0, 2, sourcePath, true); + ASSERT_BREAKPOINT(breakpointEvents[1].breakpoint, 1, + sourceBreakpoints[1].line, sourcePath, true); + // Line 3 is a comment. Move it to next valid function, which is line 4. + ASSERT_BREAKPOINT(breakpointEvents[2].breakpoint, 2, 4, sourcePath, true); + ASSERT_BREAKPOINT(breakpointEvents[3].breakpoint, 3, + sourceBreakpoints[3].line, sourcePath, true); + // Line 5 is the 2nd part of line 4 function. No valid function after line 5, + // show the breakpoint at line 4. + ASSERT_BREAKPOINT(breakpointEvents[4].breakpoint, 4, + sourceBreakpoints[3].line, sourcePath, true); + return true; +} + +int testDebuggerBreakpointManager(int, char*[]) +{ + return runTests(std::vector>{ + testHandleBreakpointRequestBeforeFileIsLoaded, + testHandleBreakpointRequestAfterFileIsLoaded, + testSourceFileLoadedAfterHandleBreakpointRequest, + }); +} diff --git a/Tests/CMakeLib/testDebuggerNamedPipe.cxx b/Tests/CMakeLib/testDebuggerNamedPipe.cxx new file mode 100644 index 0000000..d2b0728 --- /dev/null +++ b/Tests/CMakeLib/testDebuggerNamedPipe.cxx @@ -0,0 +1,218 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "cmsys/RegularExpression.hxx" + +#include "cmDebuggerPipeConnection.h" +#include "cmSystemTools.h" + +#ifdef _WIN32 +# include "cmCryptoHash.h" +#endif + +static void sendCommands(std::shared_ptr const& debugger, + int delayMs, + std::vector const& initCommands) +{ + for (const auto& command : initCommands) { + std::string contentLength = "Content-Length:"; + contentLength += std::to_string(command.size()) + "\r\n\r\n"; + debugger->write(contentLength.c_str(), contentLength.size()); + if (!debugger->write(command.c_str(), command.size())) { + std::cout << "debugger write error" << std::endl; + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(delayMs)); + } +} + +/** \brief Test CMake debugger named pipe. + * + * Test CMake debugger named pipe by + * 1. Create a named pipe for DAP traffic between the client and the debugger. + * 2. Create a client thread to wait for the debugger connection. + * - Once the debugger is connected, send the minimum required commands to + * get debugger going. + * - Wait for the CMake to complete the cache generation + * - Send the disconnect command. + * - Read and store the debugger's responses for validation. + * 3. Run the CMake command with debugger on and wait for it to complete. + * 4. Validate the response to ensure we are getting the expected responses. + * + */ +int runTest(int argc, char* argv[]) +{ + if (argc < 3) { + std::cout << "Usage:\n"; + std::cout << "\t(project mode) TestDebuggerNamedPipe " + " \n"; + std::cout << "\t(script mode) TestDebuggerNamedPipe " + "\n"; + return 1; + } + + bool scriptMode = argc == 3; + +#ifdef _WIN32 + std::string namedPipe = R"(\\.\pipe\LOCAL\CMakeDebuggerPipe_)" + + cmCryptoHash(cmCryptoHash::AlgoSHA256) + .HashString(scriptMode ? argv[2] : argv[3]); +#else + std::string namedPipe = + std::string("CMakeDebuggerPipe") + (scriptMode ? "Script" : "Project"); +#endif + + std::vector cmakeCommand; + cmakeCommand.emplace_back(argv[1]); + cmakeCommand.emplace_back("--debugger"); + cmakeCommand.emplace_back("--debugger-pipe"); + cmakeCommand.emplace_back(namedPipe); + + if (scriptMode) { + cmakeCommand.emplace_back("-P"); + cmakeCommand.emplace_back(argv[2]); + } else { + cmakeCommand.emplace_back("-S"); + cmakeCommand.emplace_back(argv[2]); + cmakeCommand.emplace_back("-B"); + cmakeCommand.emplace_back(argv[3]); + } + + // Capture debugger response stream. + std::stringstream debuggerResponseStream; + + // Start the debugger client process. + std::thread clientThread([&]() { + // Poll until the pipe server is running. Clients can also look for a magic + // string in the CMake output, but this is easier for the test case. + std::shared_ptr client; + int attempt = 0; + do { + attempt++; + try { + client = std::make_shared(namedPipe); + client->Start(); + client->WaitForConnection(); + std::cout << "cmDebuggerPipeClient connected.\n"; + break; + } catch (std::runtime_error&) { + std::cout << "Failed attempt " << attempt + << " to connect to pipe server. Retrying.\n"; + client.reset(); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + } + } while (attempt < 50); // 10 seconds + + if (attempt >= 50) { + return -1; + } + + // Send init commands to get debugger going. + sendCommands( + client, 400, + { "{\"arguments\":{\"adapterID\":\"\"},\"command\":\"initialize\"," + "\"seq\":" + "1,\"type\":\"request\"}", + "{\"arguments\":{},\"command\":\"launch\",\"seq\":2,\"type\":" + "\"request\"}", + "{\"arguments\":{},\"command\":\"configurationDone\",\"seq\":3," + "\"type\":" + "\"request\"}" }); + + // Look for "exitCode" as a sign that configuration has completed and + // it's now safe to disconnect. + for (;;) { + char buffer[1]; + size_t result = client->read(buffer, 1); + if (result != 1) { + std::cout << "debugger read error: " << result << std::endl; + break; + } + debuggerResponseStream << buffer[0]; + if (debuggerResponseStream.str().find("exitCode") != std::string::npos) { + break; + } + } + + // Send disconnect command. + sendCommands( + client, 200, + { "{\"arguments\":{},\"command\":\"disconnect\",\"seq\":4,\"type\":" + "\"request\"}" }); + + // Read any remaining debugger responses. + for (;;) { + char buffer[1]; + size_t result = client->read(buffer, 1); + if (result != 1) { + std::cout << "debugger read error: " << result << std::endl; + break; + } + debuggerResponseStream << buffer[0]; + } + + client->close(); + + return 0; + }); + + if (!cmSystemTools::RunSingleCommand(cmakeCommand, nullptr, nullptr, nullptr, + nullptr, cmSystemTools::OUTPUT_MERGE)) { + std::cout << "Error running command" << std::endl; + return -1; + } + + clientThread.join(); + + auto debuggerResponse = debuggerResponseStream.str(); + + std::vector expectedResponses = { + R"("event" : "initialized".*"type" : "event")", + R"("command" : "launch".*"success" : true.*"type" : "response")", + R"("command" : "configurationDone".*"success" : true.*"type" : "response")", + R"("reason" : "started".*"threadId" : 1.*"event" : "thread".*"type" : "event")", + R"("reason" : "exited".*"threadId" : 1.*"event" : "thread".*"type" : "event")", + R"("exitCode" : 0.*"event" : "exited".*"type" : "event")", + R"("command" : "disconnect".*"success" : true.*"type" : "response")" + }; + + for (auto& regexString : expectedResponses) { + cmsys::RegularExpression regex(regexString); + if (!regex.find(debuggerResponse)) { + std::cout << "Expected response not found: " << regexString << std::endl; + std::cout << debuggerResponse << std::endl; + return -1; + } + } + + return 0; +} + +int main(int argc, char* argv[]) +{ + try { + return runTest(argc, argv); + } catch (const std::exception& ex) { + std::cout << "An exception occurred: " << ex.what() << std::endl; + return -1; + } catch (const std::string& ex) { + std::cout << "An exception occurred: " << ex << std::endl; + return -1; + } catch (...) { + std::cout << "An unknown exception occurred" << std::endl; + return -1; + } +} diff --git a/Tests/CMakeLib/testDebuggerVariables.cxx b/Tests/CMakeLib/testDebuggerVariables.cxx new file mode 100644 index 0000000..6c19baa --- /dev/null +++ b/Tests/CMakeLib/testDebuggerVariables.cxx @@ -0,0 +1,185 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "cmDebuggerVariables.h" +#include "cmDebuggerVariablesManager.h" + +#include "testCommon.h" +#include "testDebugger.h" + +static dap::VariablesRequest CreateVariablesRequest(int64_t reference) +{ + dap::VariablesRequest variableRequest; + variableRequest.variablesReference = reference; + return variableRequest; +} + +static bool testUniqueIds() +{ + auto variablesManager = + std::make_shared(); + + std::unordered_set variableIds; + bool noDuplicateIds = true; + for (int i = 0; i < 10000 && noDuplicateIds; ++i) { + auto variable = + cmDebugger::cmDebuggerVariables(variablesManager, "Locals", true, []() { + return std::vector(); + }); + + if (variableIds.find(variable.GetId()) != variableIds.end()) { + noDuplicateIds = false; + } + variableIds.insert(variable.GetId()); + } + + ASSERT_TRUE(noDuplicateIds); + + return true; +} + +static bool testConstructors() +{ + auto variablesManager = + std::make_shared(); + + auto parent = std::make_shared( + variablesManager, "Parent", true, [=]() { + return std::vector{ + { "ParentKey", "ParentValue", "ParentType" } + }; + }); + + auto children1 = std::make_shared( + variablesManager, "Children1", true, [=]() { + return std::vector{ + { "ChildKey1", "ChildValue1", "ChildType1" }, + { "ChildKey2", "ChildValue2", "ChildType2" } + }; + }); + + parent->AddSubVariables(children1); + + auto children2 = std::make_shared( + variablesManager, "Children2", true); + + auto grandChildren21 = std::make_shared( + variablesManager, "GrandChildren21", true); + grandChildren21->SetValue("GrandChildren21 Value"); + children2->AddSubVariables(grandChildren21); + parent->AddSubVariables(children2); + + dap::array variables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(parent->GetId())); + ASSERT_TRUE(variables.size() == 3); + ASSERT_VARIABLE_REFERENCE(variables[0], "Children1", "", "collection", + children1->GetId()); + ASSERT_VARIABLE_REFERENCE(variables[1], "Children2", "", "collection", + children2->GetId()); + ASSERT_VARIABLE(variables[2], "ParentKey", "ParentValue", "ParentType"); + + variables = variablesManager->HandleVariablesRequest( + CreateVariablesRequest(children1->GetId())); + ASSERT_TRUE(variables.size() == 2); + ASSERT_VARIABLE(variables[0], "ChildKey1", "ChildValue1", "ChildType1"); + ASSERT_VARIABLE(variables[1], "ChildKey2", "ChildValue2", "ChildType2"); + + variables = variablesManager->HandleVariablesRequest( + CreateVariablesRequest(children2->GetId())); + ASSERT_TRUE(variables.size() == 1); + ASSERT_VARIABLE_REFERENCE(variables[0], "GrandChildren21", + "GrandChildren21 Value", "collection", + grandChildren21->GetId()); + + return true; +} + +static bool testIgnoreEmptyStringEntries() +{ + auto variablesManager = + std::make_shared(); + + auto vars = std::make_shared( + variablesManager, "Variables", true, []() { + return std::vector{ + { "IntValue1", 5 }, { "StringValue1", "" }, + { "StringValue2", "foo" }, { "StringValue3", "" }, + { "StringValue4", "bar" }, { "StringValue5", "" }, + { "IntValue2", int64_t(99) }, { "BooleanTrue", true }, + { "BooleanFalse", false }, + }; + }); + + vars->SetIgnoreEmptyStringEntries(true); + vars->SetEnableSorting(false); + + dap::array variables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(vars->GetId())); + ASSERT_TRUE(variables.size() == 6); + ASSERT_VARIABLE(variables[0], "IntValue1", "5", "int"); + ASSERT_VARIABLE(variables[1], "StringValue2", "foo", "string"); + ASSERT_VARIABLE(variables[2], "StringValue4", "bar", "string"); + ASSERT_VARIABLE(variables[3], "IntValue2", "99", "int"); + ASSERT_VARIABLE(variables[4], "BooleanTrue", "TRUE", "bool"); + ASSERT_VARIABLE(variables[5], "BooleanFalse", "FALSE", "bool"); + + return true; +} + +static bool testSortTheResult() +{ + auto variablesManager = + std::make_shared(); + + auto vars = std::make_shared( + variablesManager, "Variables", true, []() { + return std::vector{ + { "4", "4" }, { "2", "2" }, { "1", "1" }, { "3", "3" }, { "5", "5" }, + }; + }); + + dap::array variables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(vars->GetId())); + ASSERT_TRUE(variables.size() == 5); + ASSERT_VARIABLE(variables[0], "1", "1", "string"); + ASSERT_VARIABLE(variables[1], "2", "2", "string"); + ASSERT_VARIABLE(variables[2], "3", "3", "string"); + ASSERT_VARIABLE(variables[3], "4", "4", "string"); + ASSERT_VARIABLE(variables[4], "5", "5", "string"); + + vars->SetEnableSorting(false); + + variables = variablesManager->HandleVariablesRequest( + CreateVariablesRequest(vars->GetId())); + ASSERT_TRUE(variables.size() == 5); + ASSERT_VARIABLE(variables[0], "4", "4", "string"); + ASSERT_VARIABLE(variables[1], "2", "2", "string"); + ASSERT_VARIABLE(variables[2], "1", "1", "string"); + ASSERT_VARIABLE(variables[3], "3", "3", "string"); + ASSERT_VARIABLE(variables[4], "5", "5", "string"); + + return true; +} + +int testDebuggerVariables(int, char*[]) +{ + return runTests(std::vector>{ + testUniqueIds, + testConstructors, + testIgnoreEmptyStringEntries, + testSortTheResult, + }); +} diff --git a/Tests/CMakeLib/testDebuggerVariablesHelper.cxx b/Tests/CMakeLib/testDebuggerVariablesHelper.cxx new file mode 100644 index 0000000..e0bbdf0 --- /dev/null +++ b/Tests/CMakeLib/testDebuggerVariablesHelper.cxx @@ -0,0 +1,587 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "cmDebuggerStackFrame.h" +#include "cmDebuggerVariables.h" +#include "cmDebuggerVariablesHelper.h" +#include "cmDebuggerVariablesManager.h" +#include "cmFileSet.h" +#include "cmGlobalGenerator.h" +#include "cmListFileCache.h" +#include "cmMakefile.h" +#include "cmPolicies.h" +#include "cmPropertyMap.h" +#include "cmState.h" +#include "cmStateDirectory.h" +#include "cmStateSnapshot.h" +#include "cmStateTypes.h" +#include "cmTarget.h" +#include "cmTest.h" +#include "cmake.h" + +#include "testCommon.h" +#include "testDebugger.h" + +static dap::VariablesRequest CreateVariablesRequest(int64_t reference) +{ + dap::VariablesRequest variableRequest; + variableRequest.variablesReference = reference; + return variableRequest; +} + +struct Dummies +{ + std::shared_ptr CMake; + std::shared_ptr Makefile; + std::shared_ptr GlobalGenerator; +}; + +static Dummies CreateDummies( + std::string targetName, + std::string currentSourceDirectory = "c:/CurrentSourceDirectory", + std::string currentBinaryDirectory = "c:/CurrentBinaryDirectory") +{ + Dummies dummies; + dummies.CMake = + std::make_shared(cmake::RoleProject, cmState::Project); + cmState* state = dummies.CMake->GetState(); + dummies.GlobalGenerator = + std::make_shared(dummies.CMake.get()); + cmStateSnapshot snapshot = state->CreateBaseSnapshot(); + snapshot.GetDirectory().SetCurrentSource(currentSourceDirectory); + snapshot.GetDirectory().SetCurrentBinary(currentBinaryDirectory); + dummies.Makefile = + std::make_shared(dummies.GlobalGenerator.get(), snapshot); + dummies.Makefile->CreateNewTarget(targetName, cmStateEnums::EXECUTABLE); + return dummies; +} + +static bool testCreateFromPolicyMap() +{ + auto variablesManager = + std::make_shared(); + + cmPolicies::PolicyMap policyMap; + policyMap.Set(cmPolicies::CMP0000, cmPolicies::NEW); + policyMap.Set(cmPolicies::CMP0003, cmPolicies::WARN); + policyMap.Set(cmPolicies::CMP0005, cmPolicies::OLD); + auto vars = cmDebugger::cmDebuggerVariablesHelper::Create( + variablesManager, "Locals", true, policyMap); + + dap::array variables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(vars->GetId())); + ASSERT_TRUE(variables.size() == 3); + ASSERT_VARIABLE(variables[0], "CMP0000", "NEW", "string"); + ASSERT_VARIABLE(variables[1], "CMP0003", "WARN", "string"); + ASSERT_VARIABLE(variables[2], "CMP0005", "OLD", "string"); + + return true; +} + +static bool testCreateFromPairVector() +{ + auto variablesManager = + std::make_shared(); + + std::vector> pairs = { + { "Foo1", "Bar1" }, { "Foo2", "Bar2" } + }; + + auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, pairs); + + dap::array variables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(vars->GetId())); + + ASSERT_TRUE(vars->GetValue() == std::to_string(pairs.size())); + ASSERT_TRUE(variables.size() == 2); + ASSERT_VARIABLE(variables[0], "Foo1", "Bar1", "string"); + ASSERT_VARIABLE(variables[1], "Foo2", "Bar2", "string"); + + auto none = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, + std::vector>()); + + ASSERT_TRUE(none == nullptr); + + return true; +} + +static bool testCreateFromSet() +{ + auto variablesManager = + std::make_shared(); + + std::set set = { "Foo", "Bar" }; + + auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, set); + + dap::array variables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(vars->GetId())); + + ASSERT_TRUE(vars->GetValue() == std::to_string(set.size())); + ASSERT_TRUE(variables.size() == 2); + ASSERT_VARIABLE(variables[0], "[0]", "Bar", "string"); + ASSERT_VARIABLE(variables[1], "[1]", "Foo", "string"); + + auto none = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, std::set()); + + ASSERT_TRUE(none == nullptr); + + return true; +} + +static bool testCreateFromStringVector() +{ + auto variablesManager = + std::make_shared(); + + std::vector list = { "Foo", "Bar" }; + + auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, list); + + dap::array variables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(vars->GetId())); + + ASSERT_TRUE(vars->GetValue() == std::to_string(list.size())); + ASSERT_TRUE(variables.size() == 2); + ASSERT_VARIABLE(variables[0], "[0]", "Foo", "string"); + ASSERT_VARIABLE(variables[1], "[1]", "Bar", "string"); + + auto none = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, std::vector()); + + ASSERT_TRUE(none == nullptr); + + return true; +} + +static bool testCreateFromTarget() +{ + auto variablesManager = + std::make_shared(); + + auto dummies = CreateDummies("Foo"); + + auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, dummies.Makefile->GetOrderedTargets()); + + dap::array variables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(vars->GetId())); + + ASSERT_TRUE(variables.size() == 1); + ASSERT_VARIABLE(variables[0], "Foo", "EXECUTABLE", "collection"); + + variables = variablesManager->HandleVariablesRequest( + CreateVariablesRequest(variables[0].variablesReference)); + + ASSERT_TRUE(variables.size() == 15); + ASSERT_VARIABLE(variables[0], "GlobalGenerator", "Generic", "collection"); + ASSERT_VARIABLE(variables[1], "IsAIX", "FALSE", "bool"); + ASSERT_VARIABLE(variables[2], "IsAndroidGuiExecutable", "FALSE", "bool"); + ASSERT_VARIABLE(variables[3], "IsAppBundleOnApple", "FALSE", "bool"); + ASSERT_VARIABLE(variables[4], "IsDLLPlatform", "FALSE", "bool"); + ASSERT_VARIABLE(variables[5], "IsExecutableWithExports", "FALSE", "bool"); + ASSERT_VARIABLE(variables[6], "IsFrameworkOnApple", "FALSE", "bool"); + ASSERT_VARIABLE(variables[7], "IsImported", "FALSE", "bool"); + ASSERT_VARIABLE(variables[8], "IsImportedGloballyVisible", "FALSE", "bool"); + ASSERT_VARIABLE(variables[9], "IsPerConfig", "TRUE", "bool"); + ASSERT_VARIABLE(variables[10], "Makefile", + dummies.Makefile->GetDirectoryId().String, "collection"); + ASSERT_VARIABLE(variables[11], "Name", "Foo", "string"); + ASSERT_VARIABLE(variables[12], "PolicyMap", "", "collection"); + ASSERT_VARIABLE(variables[13], "Properties", + std::to_string(dummies.Makefile->GetOrderedTargets()[0] + ->GetProperties() + .GetList() + .size()), + "collection"); + ASSERT_VARIABLE(variables[14], "Type", "EXECUTABLE", "string"); + + auto none = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, std::vector()); + + ASSERT_TRUE(none == nullptr); + + return true; +} + +static bool testCreateFromGlobalGenerator() +{ + auto variablesManager = + std::make_shared(); + + auto dummies = CreateDummies("Foo"); + + auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, dummies.GlobalGenerator.get()); + + dap::array variables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(vars->GetId())); + + ASSERT_TRUE(variables.size() == 10); + ASSERT_VARIABLE(variables[0], "AllTargetName", "ALL_BUILD", "string"); + ASSERT_VARIABLE(variables[1], "ForceUnixPaths", "FALSE", "bool"); + ASSERT_VARIABLE(variables[2], "InstallTargetName", "INSTALL", "string"); + ASSERT_VARIABLE(variables[3], "IsMultiConfig", "FALSE", "bool"); + ASSERT_VARIABLE(variables[4], "MakefileEncoding", "None", "string"); + ASSERT_VARIABLE(variables[5], "Name", "Generic", "string"); + ASSERT_VARIABLE(variables[6], "NeedSymbolicMark", "FALSE", "bool"); + ASSERT_VARIABLE(variables[7], "PackageTargetName", "PACKAGE", "string"); + ASSERT_VARIABLE(variables[8], "TestTargetName", "RUN_TESTS", "string"); + ASSERT_VARIABLE(variables[9], "UseLinkScript", "FALSE", "bool"); + + auto none = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, + static_cast(nullptr)); + + ASSERT_TRUE(none == nullptr); + + return true; +} + +static bool testCreateFromTests() +{ + auto variablesManager = + std::make_shared(); + + auto dummies = CreateDummies("Foo"); + cmTest test1 = cmTest(dummies.Makefile.get()); + test1.SetName("Test1"); + test1.SetOldStyle(false); + test1.SetCommandExpandLists(true); + test1.SetCommand(std::vector{ "Foo1", "arg1" }); + test1.SetProperty("Prop1", "Prop1"); + cmTest test2 = cmTest(dummies.Makefile.get()); + test2.SetName("Test2"); + test2.SetOldStyle(false); + test2.SetCommandExpandLists(false); + test2.SetCommand(std::vector{ "Bar1", "arg1", "arg2" }); + test2.SetProperty("Prop2", "Prop2"); + test2.SetProperty("Prop3", "Prop3"); + + std::vector tests = { &test1, &test2 }; + + auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, tests); + + dap::array variables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(vars->GetId())); + + ASSERT_TRUE(vars->GetValue() == std::to_string(tests.size())); + ASSERT_TRUE(variables.size() == 2); + ASSERT_VARIABLE_REFERENCE_NOT_ZERO(variables[0], test1.GetName(), "", + "collection"); + ASSERT_VARIABLE_REFERENCE_NOT_ZERO(variables[1], test2.GetName(), "", + "collection"); + + dap::array testVariables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(variables[0].variablesReference)); + ASSERT_TRUE(testVariables.size() == 5); + ASSERT_VARIABLE_REFERENCE_NOT_ZERO(testVariables[0], "Command", + std::to_string(test1.GetCommand().size()), + "collection"); + ASSERT_VARIABLE(testVariables[1], "CommandExpandLists", + BOOL_STRING(test1.GetCommandExpandLists()), "bool"); + ASSERT_VARIABLE(testVariables[2], "Name", test1.GetName(), "string"); + ASSERT_VARIABLE(testVariables[3], "OldStyle", + BOOL_STRING(test1.GetOldStyle()), "bool"); + ASSERT_VARIABLE_REFERENCE_NOT_ZERO(testVariables[4], "Properties", "1", + "collection"); + + dap::array commandVariables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(testVariables[0].variablesReference)); + ASSERT_TRUE(commandVariables.size() == test1.GetCommand().size()); + for (size_t i = 0; i < commandVariables.size(); ++i) { + ASSERT_VARIABLE(commandVariables[i], "[" + std::to_string(i) + "]", + test1.GetCommand()[i], "string"); + } + + dap::array propertiesVariables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(testVariables[4].variablesReference)); + ASSERT_TRUE(propertiesVariables.size() == 1); + ASSERT_VARIABLE(propertiesVariables[0], "Prop1", "Prop1", "string"); + + testVariables = variablesManager->HandleVariablesRequest( + CreateVariablesRequest(variables[1].variablesReference)); + ASSERT_TRUE(testVariables.size() == 5); + ASSERT_VARIABLE_REFERENCE_NOT_ZERO(testVariables[0], "Command", + std::to_string(test2.GetCommand().size()), + "collection"); + ASSERT_VARIABLE(testVariables[1], "CommandExpandLists", + BOOL_STRING(test2.GetCommandExpandLists()), "bool"); + ASSERT_VARIABLE(testVariables[2], "Name", test2.GetName(), "string"); + ASSERT_VARIABLE(testVariables[3], "OldStyle", + BOOL_STRING(test2.GetOldStyle()), "bool"); + ASSERT_VARIABLE_REFERENCE_NOT_ZERO(testVariables[4], "Properties", "2", + "collection"); + + commandVariables = variablesManager->HandleVariablesRequest( + CreateVariablesRequest(testVariables[0].variablesReference)); + ASSERT_TRUE(commandVariables.size() == test2.GetCommand().size()); + for (size_t i = 0; i < commandVariables.size(); ++i) { + ASSERT_VARIABLE(commandVariables[i], "[" + std::to_string(i) + "]", + test2.GetCommand()[i], "string"); + } + + propertiesVariables = variablesManager->HandleVariablesRequest( + CreateVariablesRequest(testVariables[4].variablesReference)); + ASSERT_TRUE(propertiesVariables.size() == 2); + ASSERT_VARIABLE(propertiesVariables[0], "Prop2", "Prop2", "string"); + ASSERT_VARIABLE(propertiesVariables[1], "Prop3", "Prop3", "string"); + + auto none = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, std::vector()); + + ASSERT_TRUE(none == nullptr); + + return true; +} + +static bool testCreateFromMakefile() +{ + auto variablesManager = + std::make_shared(); + + auto dummies = CreateDummies("Foo"); + auto snapshot = dummies.Makefile->GetStateSnapshot(); + auto state = dummies.Makefile->GetState(); + state->SetSourceDirectory("c:/HomeDirectory"); + state->SetBinaryDirectory("c:/HomeOutputDirectory"); + auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, dummies.Makefile.get()); + + dap::array variables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(vars->GetId())); + + ASSERT_TRUE(variables.size() == 12); + ASSERT_VARIABLE(variables[0], "AppleSDKType", "MacOS", "string"); + ASSERT_VARIABLE(variables[1], "CurrentBinaryDirectory", + snapshot.GetDirectory().GetCurrentBinary(), "string"); + ASSERT_VARIABLE(variables[2], "CurrentSourceDirectory", + snapshot.GetDirectory().GetCurrentSource(), "string"); + ASSERT_VARIABLE(variables[3], "DefineFlags", " ", "string"); + ASSERT_VARIABLE(variables[4], "DirectoryId", + dummies.Makefile->GetDirectoryId().String, "string"); + ASSERT_VARIABLE(variables[5], "HomeDirectory", state->GetSourceDirectory(), + "string"); + ASSERT_VARIABLE(variables[6], "HomeOutputDirectory", + state->GetBinaryDirectory(), "string"); + ASSERT_VARIABLE(variables[7], "IsRootMakefile", "TRUE", "bool"); + ASSERT_VARIABLE(variables[8], "PlatformIs32Bit", "FALSE", "bool"); + ASSERT_VARIABLE(variables[9], "PlatformIs64Bit", "FALSE", "bool"); + ASSERT_VARIABLE(variables[10], "PlatformIsAppleEmbedded", "FALSE", "bool"); + ASSERT_VARIABLE(variables[11], "PlatformIsx32", "FALSE", "bool"); + + auto none = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, static_cast(nullptr)); + + ASSERT_TRUE(none == nullptr); + + return true; +} + +static bool testCreateFromStackFrame() +{ + auto variablesManager = + std::make_shared(); + auto dummies = CreateDummies("Foo"); + + cmListFileFunction lff = cmListFileFunction("set", 99, 99, {}); + auto frame = std::make_shared( + dummies.Makefile.get(), "c:/CMakeLists.txt", lff); + + dummies.CMake->AddCacheEntry("CMAKE_BUILD_TYPE", "Debug", "Build Type", + cmStateEnums::CacheEntryType::STRING); + + auto locals = cmDebugger::cmDebuggerVariablesHelper::Create( + variablesManager, "Locals", true, frame); + + dap::array variables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(locals->GetId())); + + ASSERT_TRUE(variables.size() == 5); + ASSERT_VARIABLE(variables[0], "CacheVariables", "1", "collection"); + ASSERT_VARIABLE(variables[1], "CurrentLine", std::to_string(lff.Line()), + "int"); + ASSERT_VARIABLE(variables[2], "Directories", "2", "collection"); + ASSERT_VARIABLE(variables[3], "Locals", "2", "collection"); + ASSERT_VARIABLE(variables[4], "Targets", "1", "collection"); + + dap::array cacheVariables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(variables[0].variablesReference)); + ASSERT_TRUE(cacheVariables.size() == 1); + ASSERT_VARIABLE(cacheVariables[0], "CMAKE_BUILD_TYPE:STRING", "Debug", + "collection"); + + dap::array directoriesVariables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(variables[2].variablesReference)); + ASSERT_TRUE(directoriesVariables.size() == 2); + ASSERT_VARIABLE( + directoriesVariables[0], "CMAKE_CURRENT_BINARY_DIR", + dummies.Makefile->GetStateSnapshot().GetDirectory().GetCurrentBinary(), + "string"); + ASSERT_VARIABLE( + directoriesVariables[1], "CMAKE_CURRENT_SOURCE_DIR", + dummies.Makefile->GetStateSnapshot().GetDirectory().GetCurrentSource(), + "string"); + + dap::array propertiesVariables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(cacheVariables[0].variablesReference)); + ASSERT_TRUE(propertiesVariables.size() == 3); + ASSERT_VARIABLE(propertiesVariables[0], "HELPSTRING", "Build Type", + "string"); + ASSERT_VARIABLE(propertiesVariables[1], "TYPE", "STRING", "string"); + ASSERT_VARIABLE(propertiesVariables[2], "VALUE", "Debug", "string"); + + return true; +} + +static bool testCreateFromBTStringVector() +{ + auto variablesManager = + std::make_shared(); + + std::vector> list(2); + list[0].Value = "Foo"; + list[1].Value = "Bar"; + + auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, list); + + dap::array variables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(vars->GetId())); + + ASSERT_TRUE(vars->GetValue() == std::to_string(list.size())); + ASSERT_TRUE(variables.size() == 2); + ASSERT_VARIABLE(variables[0], "[0]", "Foo", "string"); + ASSERT_VARIABLE(variables[1], "[1]", "Bar", "string"); + + auto none = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, std::vector()); + + ASSERT_TRUE(none == nullptr); + + return true; +} + +static bool testCreateFromFileSet() +{ + auto variablesManager = + std::make_shared(); + + cmake cm(cmake::RoleScript, cmState::Unknown); + cmFileSet fileSet(cm, "Foo", "HEADERS", cmFileSetVisibility::Public); + BT directory; + directory.Value = "c:/"; + fileSet.AddDirectoryEntry(directory); + BT file; + file.Value = "c:/foo.cxx"; + fileSet.AddFileEntry(file); + + auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, &fileSet); + + dap::array variables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(vars->GetId())); + + ASSERT_TRUE(variables.size() == 5); + ASSERT_VARIABLE_REFERENCE_NOT_ZERO(variables[0], "Directories", "1", + "collection"); + ASSERT_VARIABLE_REFERENCE_NOT_ZERO(variables[1], "Files", "1", "collection"); + ASSERT_VARIABLE(variables[2], "Name", "Foo", "string"); + ASSERT_VARIABLE(variables[3], "Type", "HEADERS", "string"); + ASSERT_VARIABLE(variables[4], "Visibility", "Public", "string"); + + dap::array directoriesVariables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(variables[0].variablesReference)); + ASSERT_TRUE(directoriesVariables.size() == 1); + ASSERT_VARIABLE(directoriesVariables[0], "[0]", directory.Value, "string"); + + dap::array filesVariables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(variables[1].variablesReference)); + ASSERT_TRUE(filesVariables.size() == 1); + ASSERT_VARIABLE(filesVariables[0], "[0]", file.Value, "string"); + + return true; +} + +static bool testCreateFromFileSets() +{ + auto variablesManager = + std::make_shared(); + + cmake cm(cmake::RoleScript, cmState::Unknown); + cmFileSet fileSet(cm, "Foo", "HEADERS", cmFileSetVisibility::Public); + BT directory; + directory.Value = "c:/"; + fileSet.AddDirectoryEntry(directory); + BT file; + file.Value = "c:/foo.cxx"; + fileSet.AddFileEntry(file); + + auto fileSets = std::vector{ &fileSet }; + auto vars = cmDebugger::cmDebuggerVariablesHelper::CreateIfAny( + variablesManager, "Locals", true, fileSets); + + dap::array variables = + variablesManager->HandleVariablesRequest( + CreateVariablesRequest(vars->GetId())); + + ASSERT_TRUE(variables.size() == 1); + ASSERT_VARIABLE_REFERENCE_NOT_ZERO(variables[0], "Foo", "", "collection"); + + return true; +} + +int testDebuggerVariablesHelper(int, char*[]) +{ + return runTests(std::vector>{ + testCreateFromPolicyMap, + testCreateFromPairVector, + testCreateFromSet, + testCreateFromStringVector, + testCreateFromTarget, + testCreateFromGlobalGenerator, + testCreateFromMakefile, + testCreateFromStackFrame, + testCreateFromTests, + testCreateFromBTStringVector, + testCreateFromFileSet, + testCreateFromFileSets, + }); +} diff --git a/Tests/CMakeLib/testDebuggerVariablesManager.cxx b/Tests/CMakeLib/testDebuggerVariablesManager.cxx new file mode 100644 index 0000000..3013b9f --- /dev/null +++ b/Tests/CMakeLib/testDebuggerVariablesManager.cxx @@ -0,0 +1,50 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ + +#include +#include +#include + +#include +#include +#include + +#include "cmDebuggerVariables.h" +#include "cmDebuggerVariablesManager.h" + +#include "testCommon.h" + +static bool testVariablesRegistration() +{ + auto variablesManager = + std::make_shared(); + + int64_t line = 5; + auto local = std::make_shared( + variablesManager, "Local", true, [=]() { + return std::vector{ { "CurrentLine", + line } }; + }); + + dap::VariablesRequest variableRequest; + variableRequest.variablesReference = local->GetId(); + + dap::array variables = + variablesManager->HandleVariablesRequest(variableRequest); + + ASSERT_TRUE(variables.size() == 1); + + local.reset(); + + variables = variablesManager->HandleVariablesRequest(variableRequest); + ASSERT_TRUE(variables.size() == 0); + + return true; +} + +int testDebuggerVariablesManager(int, char*[]) +{ + return runTests(std::vector>{ + testVariablesRegistration, + }); +} diff --git a/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog-result.txt b/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog-stderr.txt b/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog-stderr.txt new file mode 100644 index 0000000..6269c19 --- /dev/null +++ b/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog-stderr.txt @@ -0,0 +1,2 @@ +^CMake Error: No file specified for --debugger-dap-log +CMake Error: Run 'cmake --help' for all supported options\.$ diff --git a/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog.cmake b/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog.cmake new file mode 100644 index 0000000..6ddce8b --- /dev/null +++ b/Tests/RunCMake/CommandLine/DebuggerArgMissingDapLog.cmake @@ -0,0 +1 @@ +message(FATAL_ERROR "This should not be reached.") diff --git a/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe-result.txt b/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe-stderr.txt b/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe-stderr.txt new file mode 100644 index 0000000..947cb00 --- /dev/null +++ b/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe-stderr.txt @@ -0,0 +1,2 @@ +^CMake Error: No path specified for --debugger-pipe +CMake Error: Run 'cmake --help' for all supported options\.$ diff --git a/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe.cmake b/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe.cmake new file mode 100644 index 0000000..6ddce8b --- /dev/null +++ b/Tests/RunCMake/CommandLine/DebuggerArgMissingPipe.cmake @@ -0,0 +1 @@ +message(FATAL_ERROR "This should not be reached.") diff --git a/Tests/RunCMake/CommandLine/DebuggerCapabilityInspect-check.cmake b/Tests/RunCMake/CommandLine/DebuggerCapabilityInspect-check.cmake new file mode 100644 index 0000000..75769f2 --- /dev/null +++ b/Tests/RunCMake/CommandLine/DebuggerCapabilityInspect-check.cmake @@ -0,0 +1,5 @@ +if(actual_stdout MATCHES [["debugger" *: *true]]) + set_property(DIRECTORY PROPERTY CMake_ENABLE_DEBUGGER 1) +else() + set_property(DIRECTORY PROPERTY CMake_ENABLE_DEBUGGER 0) +endif() diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupported-result.txt b/Tests/RunCMake/CommandLine/DebuggerNotSupported-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/CommandLine/DebuggerNotSupported-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupported-stderr.txt b/Tests/RunCMake/CommandLine/DebuggerNotSupported-stderr.txt new file mode 100644 index 0000000..5845bb3 --- /dev/null +++ b/Tests/RunCMake/CommandLine/DebuggerNotSupported-stderr.txt @@ -0,0 +1,2 @@ +^CMake Error: CMake was not built with support for --debugger +CMake Error: Run 'cmake --help' for all supported options\.$ diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupported.cmake b/Tests/RunCMake/CommandLine/DebuggerNotSupported.cmake new file mode 100644 index 0000000..6ddce8b --- /dev/null +++ b/Tests/RunCMake/CommandLine/DebuggerNotSupported.cmake @@ -0,0 +1 @@ +message(FATAL_ERROR "This should not be reached.") diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog-result.txt b/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog-stderr.txt b/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog-stderr.txt new file mode 100644 index 0000000..84c2200 --- /dev/null +++ b/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog-stderr.txt @@ -0,0 +1,2 @@ +^CMake Error: CMake was not built with support for --debugger-dap-log +CMake Error: Run 'cmake --help' for all supported options\.$ diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog.cmake b/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog.cmake new file mode 100644 index 0000000..6ddce8b --- /dev/null +++ b/Tests/RunCMake/CommandLine/DebuggerNotSupportedDapLog.cmake @@ -0,0 +1 @@ +message(FATAL_ERROR "This should not be reached.") diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe-result.txt b/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe-stderr.txt b/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe-stderr.txt new file mode 100644 index 0000000..5684f4c --- /dev/null +++ b/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe-stderr.txt @@ -0,0 +1,2 @@ +^CMake Error: CMake was not built with support for --debugger-pipe +CMake Error: Run 'cmake --help' for all supported options\.$ diff --git a/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe.cmake b/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe.cmake new file mode 100644 index 0000000..6ddce8b --- /dev/null +++ b/Tests/RunCMake/CommandLine/DebuggerNotSupportedPipe.cmake @@ -0,0 +1 @@ +message(FATAL_ERROR "This should not be reached.") diff --git a/Tests/RunCMake/CommandLine/E_capabilities-stdout.txt b/Tests/RunCMake/CommandLine/E_capabilities-stdout.txt index e2f63cd..c01f414 100644 --- a/Tests/RunCMake/CommandLine/E_capabilities-stdout.txt +++ b/Tests/RunCMake/CommandLine/E_capabilities-stdout.txt @@ -1 +1 @@ -^{"fileApi":{"requests":\[{"kind":"codemodel","version":\[{"major":2,"minor":6}]},{"kind":"configureLog","version":\[{"major":1,"minor":0}]},{"kind":"cache","version":\[{"major":2,"minor":0}]},{"kind":"cmakeFiles","version":\[{"major":1,"minor":0}]},{"kind":"toolchains","version":\[{"major":1,"minor":0}]}]},"generators":\[.*\],"serverMode":false,"tls":(true|false),"version":{.*}}$ +^{"debugger":(true|false),"fileApi":{"requests":\[{"kind":"codemodel","version":\[{"major":2,"minor":6}]},{"kind":"configureLog","version":\[{"major":1,"minor":0}]},{"kind":"cache","version":\[{"major":2,"minor":0}]},{"kind":"cmakeFiles","version":\[{"major":1,"minor":0}]},{"kind":"toolchains","version":\[{"major":1,"minor":0}]}]},"generators":\[.*\],"serverMode":false,"tls":(true|false),"version":{.*}}$ diff --git a/Tests/RunCMake/CommandLine/RunCMakeTest.cmake b/Tests/RunCMake/CommandLine/RunCMakeTest.cmake index 205949b..45b4c0e 100644 --- a/Tests/RunCMake/CommandLine/RunCMakeTest.cmake +++ b/Tests/RunCMake/CommandLine/RunCMakeTest.cmake @@ -125,6 +125,17 @@ run_cmake_command(cache-bad-entry run_cmake_command(cache-empty-entry ${CMAKE_COMMAND} --build ${RunCMake_SOURCE_DIR}/cache-empty-entry/) +run_cmake_command(DebuggerCapabilityInspect ${CMAKE_COMMAND} -E capabilities) +get_property(CMake_ENABLE_DEBUGGER DIRECTORY PROPERTY CMake_ENABLE_DEBUGGER) +if(CMake_ENABLE_DEBUGGER) + run_cmake_with_options(DebuggerArgMissingPipe --debugger-pipe) + run_cmake_with_options(DebuggerArgMissingDapLog --debugger-dap-log) +else() + run_cmake_with_options(DebuggerNotSupported --debugger) + run_cmake_with_options(DebuggerNotSupportedPipe --debugger-pipe pipe) + run_cmake_with_options(DebuggerNotSupportedDapLog --debugger-dap-log dap-log) +endif() + function(run_ExplicitDirs) set(RunCMake_TEST_NO_CLEAN 1) set(RunCMake_TEST_NO_SOURCE_DIR 1) diff --git a/Utilities/IWYU/mapping.imp b/Utilities/IWYU/mapping.imp index 366c517..6c12ada 100644 --- a/Utilities/IWYU/mapping.imp +++ b/Utilities/IWYU/mapping.imp @@ -22,6 +22,7 @@ # HACK: check whether this can be removed with next iwyu release. { include: [ "", private, "", public ] }, + { include: [ "", private, "", public ] }, { include: [ "", private, "", public ] }, { include: [ "", private, "", public ] }, { include: [ "", private, "", public ] }, @@ -101,6 +102,7 @@ { symbol: [ "__gnu_cxx::__enable_if::__type", private, "\"cmConfigure.h\"", public ] }, { symbol: [ "std::remove_reference, std::allocator > &>::type", private, "\"cmConfigure.h\"", public ] }, { symbol: [ "std::remove_reference::type", private, "\"cmConfigure.h\"", public ] }, + { symbol: [ "std::remove_reference::type", private, "\"cmConfigure.h\"", public ] }, # Wrappers for 3rd-party libraries { include: [ "@<.*curl/curlver.h>", private, "", public ] }, diff --git a/bootstrap b/bootstrap index a056edf..109e450 100755 --- a/bootstrap +++ b/bootstrap @@ -80,6 +80,7 @@ cmake_init_file="" cmake_bootstrap_system_libs="" cmake_bootstrap_qt_gui="" cmake_bootstrap_qt_qmake="" +cmake_bootstrap_debugger="" cmake_sphinx_info="" cmake_sphinx_man="" cmake_sphinx_html="" @@ -697,6 +698,9 @@ Configuration: --no-qt-gui do not build the Qt-based GUI (default) --qt-qmake= use as the qmake executable to find Qt + --debugger enable debugger support (default if supported) + --no-debugger disable debugger support + --sphinx-info build Info manual with Sphinx --sphinx-man build man pages with Sphinx --sphinx-html build html help with Sphinx @@ -962,6 +966,8 @@ while test $# != 0; do --qt-gui) cmake_bootstrap_qt_gui="1" ;; --no-qt-gui) cmake_bootstrap_qt_gui="0" ;; --qt-qmake=*) cmake_bootstrap_qt_qmake=`cmake_arg "$1"` ;; + --debugger) cmake_bootstrap_debugger="1" ;; + --no-debugger) cmake_bootstrap_debugger="0" ;; --sphinx-info) cmake_sphinx_info="1" ;; --sphinx-man) cmake_sphinx_man="1" ;; --sphinx-html) cmake_sphinx_html="1" ;; @@ -1987,6 +1993,11 @@ if test "x${cmake_bootstrap_qt_qmake}" != "x"; then set (QT_QMAKE_EXECUTABLE "'"${cmake_bootstrap_qt_qmake}"'" CACHE FILEPATH "Location of Qt qmake" FORCE) ' >> "${cmake_bootstrap_dir}/InitialCacheFlags.cmake" fi +if test "x${cmake_bootstrap_debugger}" != "x"; then + echo ' +set (CMake_ENABLE_DEBUGGER '"${cmake_bootstrap_debugger}"' CACHE BOOL "Enable CMake debugger support" FORCE) +' >> "${cmake_bootstrap_dir}/InitialCacheFlags.cmake" +fi if test "x${cmake_sphinx_info}" != "x"; then echo ' set (SPHINX_INFO "'"${cmake_sphinx_info}"'" CACHE BOOL "Build Info manual with Sphinx" FORCE) -- cgit v0.12