| /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying | |
| file LICENSE.rst or https://cmake.org/licensing for details. */ | |
| #include "cmConfigure.h" // IWYU pragma: keep | |
| #include "cmDebuggerAdapter.h" | |
| #include <algorithm> | |
| #include <climits> | |
| #include <condition_variable> | |
| #include <cstdint> | |
| #include <functional> | |
| #include <iostream> | |
| #include <stdexcept> | |
| #include <utility> | |
| #include <cm/memory> | |
| #include <cm/optional> | |
| #include <cm3p/cppdap/io.h> // IWYU pragma: keep | |
| #include <cm3p/cppdap/protocol.h> | |
| #include <cm3p/cppdap/session.h> | |
| #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 <cmcppdap/include/dap/optional.h> | |
| #include <cmcppdap/include/dap/types.h> | |
| 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<std::mutex> lock(Mutex); | |
| Cv.wait(lock, [&] { return Fired; }); | |
| } | |
| // Fire() sets signals the event, and unblocks any calls to Wait(). | |
| void Fire() | |
| { | |
| std::unique_lock<std::mutex> 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_) | |
| { | |
| } | |
| void Notify() | |
| { | |
| std::unique_lock<std::mutex> lock(Mutex); | |
| Count++; | |
| // notify the waiting thread | |
| Cv.notify_one(); | |
| } | |
| void Wait() | |
| { | |
| std::unique_lock<std::mutex> 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<cmDebuggerConnection> connection, | |
| std::string const& dapLogPath) | |
| : cmDebuggerAdapter(std::move(connection), | |
| dapLogPath.empty() | |
| ? cm::nullopt | |
| : cm::optional<std::shared_ptr<dap::Writer>>( | |
| dap::file(dapLogPath.c_str()))) | |
| { | |
| } | |
| cmDebuggerAdapter::cmDebuggerAdapter( | |
| std::shared_ptr<cmDebuggerConnection> connection, | |
| cm::optional<std::shared_ptr<dap::Writer>> logger) | |
| : Connection(std::move(connection)) | |
| , SessionActive(true) | |
| , DisconnectEvent(cm::make_unique<SyncEvent>()) | |
| , ConfigurationDoneEvent(cm::make_unique<SyncEvent>()) | |
| , ContinueSem(cm::make_unique<Semaphore>()) | |
| , ThreadManager(cm::make_unique<cmDebuggerThreadManager>()) | |
| { | |
| if (logger.has_value()) { | |
| SessionLog = std::move(logger.value()); | |
| } | |
| ClearStepRequests(); | |
| Session = dap::Session::create(); | |
| BreakpointManager = | |
| cm::make_unique<cmDebuggerBreakpointManager>(Session.get()); | |
| ExceptionManager = | |
| cm::make_unique<cmDebuggerExceptionManager>(Session.get()); | |
| // Handle errors reported by the Session. These errors include protocol | |
| // parsing errors and receiving messages with no handler. | |
| Session->onError([this](char const* 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](dap::CMakeInitializeRequest const& req) { | |
| SupportsVariableType = req.supportsVariableType.value(false); | |
| dap::CMakeInitializeResponse response; | |
| response.supportsConfigurationDoneRequest = true; | |
| response.supportsValueFormattingOptions = 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( | |
| [&](dap::ResponseOrError<dap::CMakeInitializeResponse> const&) { | |
| Session->send(dap::InitializedEvent()); | |
| }); | |
| // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Threads | |
| Session->registerHandler([this](dap::ThreadsRequest /*unused*/) { | |
| std::unique_lock<std::mutex> lock(Mutex); | |
| dap::ThreadsResponse response; | |
| // If a client requests threads during shutdown (like after receiving the | |
| // thread exited event), DefaultThread won't be set. | |
| if (DefaultThread) { | |
| 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](dap::StackTraceRequest const& request) | |
| -> dap::ResponseOrError<dap::StackTraceResponse> { | |
| std::unique_lock<std::mutex> lock(Mutex); | |
| cm::optional<dap::StackTraceResponse> response = | |
| ThreadManager->GetThreadStackTraceResponse(request); | |
| 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](dap::ScopesRequest request) | |
| -> dap::ResponseOrError<dap::ScopesResponse> { | |
| std::unique_lock<std::mutex> lock(Mutex); | |
| return DefaultThread->GetScopesResponse(request.frameId, | |
| SupportsVariableType); | |
| }); | |
| // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Variables | |
| Session->registerHandler([this](dap::VariablesRequest const& request) | |
| -> dap::ResponseOrError<dap::VariablesResponse> { | |
| return DefaultThread->GetVariablesResponse(request); | |
| }); | |
| // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Pause | |
| Session->registerHandler([this](dap::PauseRequest /*unused*/) { | |
| PauseRequest.store(true); | |
| return dap::PauseResponse(); | |
| }); | |
| // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Continue | |
| Session->registerHandler([this](dap::ContinueRequest const& req) { | |
| (void)req; | |
| ContinueSem->Notify(); | |
| return dap::ContinueResponse(); | |
| }); | |
| // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Next | |
| Session->registerHandler([this](dap::NextRequest const& 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](dap::StepInRequest const& 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](dap::StepOutRequest const& 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([](dap::LaunchRequest const& req) { | |
| (void)req; | |
| return dap::LaunchResponse(); | |
| }); | |
| // Handler for disconnect requests | |
| Session->registerHandler([this](dap::DisconnectRequest const& request) { | |
| (void)request; | |
| BreakpointManager->ClearAll(); | |
| ExceptionManager->ClearAll(); | |
| ClearStepRequests(); | |
| ContinueSem->Notify(); | |
| DisconnectEvent->Fire(); | |
| SessionActive.store(false); | |
| return dap::DisconnectResponse(); | |
| }); | |
| Session->registerHandler([this](dap::EvaluateRequest const& request) { | |
| dap::EvaluateResponse response; | |
| if (request.frameId.has_value()) { | |
| std::shared_ptr<cmDebuggerStackFrame> 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](dap::ConfigurationDoneRequest /*unused*/) { | |
| 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(); | |
| } else { | |
| // Connection closed or unrecoverable error. | |
| BreakpointManager->ClearAll(); | |
| ExceptionManager->ClearAll(); | |
| ClearStepRequests(); | |
| ContinueSem->Notify(); | |
| DisconnectEvent->Fire(); | |
| SessionActive.store(false); | |
| } | |
| } | |
| }); | |
| 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<cmListFileFunction> const& functions) | |
| { | |
| BreakpointManager->SourceFileLoaded(sourcePath, functions); | |
| } | |
| void cmDebuggerAdapter::OnBeginFunctionCall(cmMakefile* mf, | |
| std::string const& sourcePath, | |
| cmListFileFunction const& lff) | |
| { | |
| std::unique_lock<std::mutex> 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<dap::integer> hitBreakpoints; | |
| hitBreakpoints.resize(hits.size()); | |
| std::transform(hits.begin(), hits.end(), hitBreakpoints.begin(), | |
| [&](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<cmListFileFunction> listFileFunction; | |
| void cmDebuggerAdapter::OnBeginFileParse(cmMakefile* mf, | |
| std::string const& sourcePath) | |
| { | |
| std::unique_lock<std::mutex> lock(Mutex); | |
| listFileFunction = std::make_shared<cmListFileFunction>( | |
| sourcePath, 0, 0, std::vector<cmListFileArgument>()); | |
| DefaultThread->PushStackFrame(mf, sourcePath, *listFileFunction); | |
| } | |
| void cmDebuggerAdapter::OnEndFileParse() | |
| { | |
| DefaultThread->PopStackFrame(); | |
| listFileFunction = nullptr; | |
| } | |
| void cmDebuggerAdapter::OnMessageOutput(MessageType t, std::string const& text) | |
| { | |
| cm::optional<dap::StoppedEvent> 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 |