| /* 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 <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 const& req) { |
| (void)req; |
| 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 const& 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 const& req) { |
| (void)req; |
| 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 const& 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<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 const& 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 |