//===-- EventHelper.h -----------------------------------------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #include "EventHelper.h" #include "Breakpoint.h" #include "BreakpointBase.h" #include "DAP.h" #include "DAPError.h" #include "DAPLog.h" #include "DAPSessionManager.h" #include "Handler/ResponseHandler.h" #include "JSONUtils.h" #include "LLDBUtils.h" #include "Protocol/ProtocolEvents.h" #include "Protocol/ProtocolRequests.h" #include "Protocol/ProtocolTypes.h" #include "ProtocolUtils.h" #include "lldb/API/SBEvent.h" #include "lldb/API/SBFileSpec.h" #include "lldb/API/SBListener.h" #include "lldb/API/SBPlatform.h" #include "lldb/API/SBStream.h" #include "llvm/Support/Error.h" #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/Threading.h" #include #include #if defined(_WIN32) #define NOMINMAX #include #ifndef PATH_MAX #define PATH_MAX MAX_PATH #endif #endif using namespace llvm; namespace lldb_dap { static void SendThreadExitedEvent(DAP &dap, lldb::tid_t tid) { llvm::json::Object event(CreateEventObject("thread")); llvm::json::Object body; body.try_emplace("reason", "exited"); body.try_emplace("threadId", (int64_t)tid); event.try_emplace("body", std::move(body)); dap.SendJSON(llvm::json::Value(std::move(event))); } /// Get capabilities based on the configured target. static llvm::DenseSet GetTargetBasedCapabilities(DAP &dap) { llvm::DenseSet capabilities; if (!dap.target.IsValid()) return capabilities; const llvm::StringRef target_triple = dap.target.GetTriple(); if (target_triple.starts_with("x86")) capabilities.insert(protocol::eAdapterFeatureStepInTargetsRequest); // We only support restarting launch requests not attach requests. if (dap.last_launch_request) capabilities.insert(protocol::eAdapterFeatureRestartRequest); return capabilities; } void SendExtraCapabilities(DAP &dap) { protocol::Capabilities capabilities = dap.GetCustomCapabilities(); llvm::DenseSet target_capabilities = GetTargetBasedCapabilities(dap); capabilities.supportedFeatures.insert(target_capabilities.begin(), target_capabilities.end()); protocol::CapabilitiesEventBody body; body.capabilities = std::move(capabilities); // Only notify the client if supportedFeatures changed. if (!body.capabilities.supportedFeatures.empty()) dap.Send(protocol::Event{"capabilities", std::move(body)}); } // "ProcessEvent": { // "allOf": [ // { "$ref": "#/definitions/Event" }, // { // "type": "object", // "description": "The event indicates that the debugger has begun // debugging a new process. Either one that it has launched, or one that // it has attached to.", "properties": { // "event": { // "type": "string", // "enum": [ "process" ] // }, // "body": { // "type": "object", // "properties": { // "name": { // "type": "string", // "description": "The logical name of the process. This is // usually the full path to process's executable file. Example: // /home/example/myproj/program.js." // }, // "systemProcessId": { // "type": "integer", // "description": "The process ID of the debugged process, as // assigned by the operating system. This property should be // omitted for logical processes that do not map to operating // system processes on the machine." // }, // "isLocalProcess": { // "type": "boolean", // "description": "If true, the process is running on the same // computer as the debug adapter." // }, // "startMethod": { // "type": "string", // "enum": [ "launch", "attach", "attachForSuspendedLaunch" ], // "description": "Describes how the debug engine started // debugging this process.", "enumDescriptions": [ // "Process was launched under the debugger.", // "Debugger attached to an existing process.", // "A project launcher component has launched a new process in a // suspended state and then asked the debugger to attach." // ] // }, // "pointerSize": { // "type": "integer", // "description": "The size of a pointer or address for this // process, in bits. This value may be used by clients when // formatting addresses for display." // } // }, // "required": [ "name" ] // } // }, // "required": [ "event", "body" ] // } // ] // }, void SendProcessEvent(DAP &dap, LaunchMethod launch_method) { lldb::SBFileSpec exe_fspec = dap.target.GetExecutable(); char exe_path[PATH_MAX]; exe_fspec.GetPath(exe_path, sizeof(exe_path)); llvm::json::Object event(CreateEventObject("process")); llvm::json::Object body; EmplaceSafeString(body, "name", exe_path); const auto pid = dap.target.GetProcess().GetProcessID(); body.try_emplace("systemProcessId", (int64_t)pid); body.try_emplace("isLocalProcess", dap.target.GetPlatform().IsHost()); body.try_emplace("pointerSize", dap.target.GetAddressByteSize() * 8); const char *startMethod = nullptr; switch (launch_method) { case Launch: startMethod = "launch"; break; case Attach: startMethod = "attach"; break; case AttachForSuspendedLaunch: startMethod = "attachForSuspendedLaunch"; break; } body.try_emplace("startMethod", startMethod); event.try_emplace("body", std::move(body)); dap.SendJSON(llvm::json::Value(std::move(event))); } // Send a thread stopped event for all threads as long as the process // is stopped. llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry) { lldb::SBMutex lock = dap.GetAPIMutex(); std::lock_guard guard(lock); lldb::SBProcess process = dap.target.GetProcess(); if (!process.IsValid()) return make_error("invalid process"); lldb::StateType state = process.GetState(); if (!lldb::SBDebugger::StateIsStoppedState(state)) return make_error(); llvm::DenseSet old_thread_ids; old_thread_ids.swap(dap.thread_ids); uint32_t stop_id = on_entry ? 0 : process.GetStopID(); const uint32_t num_threads = process.GetNumThreads(); // First make a pass through the threads to see if the focused thread // has a stop reason. In case the focus thread doesn't have a stop // reason, remember the first thread that has a stop reason so we can // set it as the focus thread if below if needed. lldb::tid_t first_tid_with_reason = LLDB_INVALID_THREAD_ID; uint32_t num_threads_with_reason = 0; bool focus_thread_exists = false; for (uint32_t thread_idx = 0; thread_idx < num_threads; ++thread_idx) { lldb::SBThread thread = process.GetThreadAtIndex(thread_idx); const lldb::tid_t tid = thread.GetThreadID(); const bool has_reason = ThreadHasStopReason(thread); // If the focus thread doesn't have a stop reason, clear the thread ID if (tid == dap.focus_tid) { focus_thread_exists = true; if (!has_reason) dap.focus_tid = LLDB_INVALID_THREAD_ID; } if (has_reason) { ++num_threads_with_reason; if (first_tid_with_reason == LLDB_INVALID_THREAD_ID) first_tid_with_reason = tid; } } // We will have cleared dap.focus_tid if the focus thread doesn't have // a stop reason, so if it was cleared, or wasn't set, or doesn't exist, // then set the focus thread to the first thread with a stop reason. if (!focus_thread_exists || dap.focus_tid == LLDB_INVALID_THREAD_ID) dap.focus_tid = first_tid_with_reason; // If no threads stopped with a reason, then report the first one so // we at least let the UI know we stopped. if (num_threads_with_reason == 0) { lldb::SBThread thread = process.GetThreadAtIndex(0); dap.focus_tid = thread.GetThreadID(); dap.SendJSON(CreateThreadStopped(dap, thread, stop_id)); } else { for (uint32_t thread_idx = 0; thread_idx < num_threads; ++thread_idx) { lldb::SBThread thread = process.GetThreadAtIndex(thread_idx); dap.thread_ids.insert(thread.GetThreadID()); if (ThreadHasStopReason(thread)) { dap.SendJSON(CreateThreadStopped(dap, thread, stop_id)); } } } for (const auto &tid : old_thread_ids) { auto end = dap.thread_ids.end(); auto pos = dap.thread_ids.find(tid); if (pos == end) SendThreadExitedEvent(dap, tid); } dap.RunStopCommands(); return Error::success(); } // Send a "terminated" event to indicate the process is done being // debugged. void SendTerminatedEvent(DAP &dap) { dap.SendTerminatedEvent(); } // Grab any STDOUT and STDERR from the process and send it up to VS Code // via an "output" event to the "stdout" and "stderr" categories. void SendStdOutStdErr(DAP &dap, lldb::SBProcess &process) { char buffer[OutputBufferSize]; size_t count; while ((count = process.GetSTDOUT(buffer, sizeof(buffer))) > 0) dap.SendOutput(OutputType::Stdout, llvm::StringRef(buffer, count)); while ((count = process.GetSTDERR(buffer, sizeof(buffer))) > 0) dap.SendOutput(OutputType::Stderr, llvm::StringRef(buffer, count)); } // Send a "continued" event to indicate the process is in the running state. void SendContinuedEvent(DAP &dap) { lldb::SBProcess process = dap.target.GetProcess(); if (!process.IsValid()) { return; } // If the focus thread is not set then we haven't reported any thread status // to the client, so nothing to report. if (!dap.configuration_done || dap.focus_tid == LLDB_INVALID_THREAD_ID) { return; } llvm::json::Object event(CreateEventObject("continued")); llvm::json::Object body; body.try_emplace("threadId", (int64_t)dap.focus_tid); body.try_emplace("allThreadsContinued", true); event.try_emplace("body", std::move(body)); dap.SendJSON(llvm::json::Value(std::move(event))); } // Send a "exited" event to indicate the process has exited. void SendProcessExitedEvent(DAP &dap, lldb::SBProcess &process) { llvm::json::Object event(CreateEventObject("exited")); llvm::json::Object body; body.try_emplace("exitCode", (int64_t)process.GetExitStatus()); event.try_emplace("body", std::move(body)); dap.SendJSON(llvm::json::Value(std::move(event))); } void SendInvalidatedEvent( DAP &dap, llvm::ArrayRef areas, lldb::tid_t tid) { if (!dap.clientFeatures.contains(protocol::eClientFeatureInvalidatedEvent)) return; protocol::InvalidatedEventBody body; body.areas = areas; if (tid != LLDB_INVALID_THREAD_ID) body.threadId = tid; dap.Send(protocol::Event{"invalidated", std::move(body)}); } void SendMemoryEvent(DAP &dap, lldb::SBValue variable) { if (!dap.clientFeatures.contains(protocol::eClientFeatureMemoryEvent)) return; protocol::MemoryEventBody body; body.memoryReference = variable.GetLoadAddress(); body.count = variable.GetByteSize(); if (body.memoryReference == LLDB_INVALID_ADDRESS) return; dap.Send(protocol::Event{"memory", std::move(body)}); } // Event handler functions that are called by EventThread. // These handlers extract the necessary objects from events and find the // appropriate DAP instance to handle them, maintaining compatibility with // the original DAP::Handle*Event pattern while supporting multi-session // debugging. void HandleProcessEvent(const lldb::SBEvent &event, bool &process_exited, Log *log) { lldb::SBProcess process = lldb::SBProcess::GetProcessFromEvent(event); // Find the DAP instance that owns this process's target. DAP *dap = DAPSessionManager::FindDAP(process.GetTarget()); if (!dap) { DAP_LOG(log, "Unable to find DAP instance for process {0}", process.GetProcessID()); return; } const uint32_t event_mask = event.GetType(); if (event_mask & lldb::SBProcess::eBroadcastBitStateChanged) { auto state = lldb::SBProcess::GetStateFromEvent(event); switch (state) { case lldb::eStateConnected: case lldb::eStateDetached: case lldb::eStateInvalid: case lldb::eStateUnloaded: break; case lldb::eStateAttaching: case lldb::eStateCrashed: case lldb::eStateLaunching: case lldb::eStateStopped: case lldb::eStateSuspended: // Only report a stopped event if the process was not // automatically restarted. if (!lldb::SBProcess::GetRestartedFromEvent(event)) { SendStdOutStdErr(*dap, process); if (llvm::Error err = SendThreadStoppedEvent(*dap)) DAP_LOG_ERROR(dap->log, std::move(err), "({1}) reporting thread stopped: {0}", dap->GetClientName()); } break; case lldb::eStateRunning: case lldb::eStateStepping: dap->WillContinue(); SendContinuedEvent(*dap); break; case lldb::eStateExited: lldb::SBStream stream; process.GetStatus(stream); dap->SendOutput(OutputType::Console, stream.GetData()); // When restarting, we can get an "exited" event for the process we // just killed with the old PID, or even with no PID. In that case // we don't have to terminate the session. if (process.GetProcessID() == LLDB_INVALID_PROCESS_ID || process.GetProcessID() == dap->restarting_process_id) { dap->restarting_process_id = LLDB_INVALID_PROCESS_ID; } else { // Run any exit LLDB commands the user specified in the // launch.json dap->RunExitCommands(); SendProcessExitedEvent(*dap, process); dap->SendTerminatedEvent(); process_exited = true; } break; } } else if ((event_mask & lldb::SBProcess::eBroadcastBitSTDOUT) || (event_mask & lldb::SBProcess::eBroadcastBitSTDERR)) { SendStdOutStdErr(*dap, process); } } void HandleTargetEvent(const lldb::SBEvent &event, Log *log) { lldb::SBTarget target = lldb::SBTarget::GetTargetFromEvent(event); // Find the DAP instance that owns this target. DAP *dap = DAPSessionManager::FindDAP(target); if (!dap) { DAP_LOG(log, "Unable to find DAP instance for target"); return; } const uint32_t event_mask = event.GetType(); if (event_mask & lldb::SBTarget::eBroadcastBitModulesLoaded || event_mask & lldb::SBTarget::eBroadcastBitModulesUnloaded || event_mask & lldb::SBTarget::eBroadcastBitSymbolsLoaded || event_mask & lldb::SBTarget::eBroadcastBitSymbolsChanged) { const uint32_t num_modules = lldb::SBTarget::GetNumModulesFromEvent(event); const bool remove_module = event_mask & lldb::SBTarget::eBroadcastBitModulesUnloaded; // NOTE: Both mutexes must be acquired to prevent deadlock when // handling `modules_request`, which also requires both locks. lldb::SBMutex api_mutex = dap->GetAPIMutex(); const std::scoped_lock guard(api_mutex, dap->modules_mutex); for (uint32_t i = 0; i < num_modules; ++i) { lldb::SBModule module = lldb::SBTarget::GetModuleAtIndexFromEvent(i, event); std::optional p_module = CreateModule(dap->target, module, remove_module); if (!p_module) continue; llvm::StringRef module_id = p_module->id; const bool module_exists = dap->modules.contains(module_id); if (remove_module && module_exists) { dap->modules.erase(module_id); dap->Send(protocol::Event{ "module", protocol::ModuleEventBody{ std::move(p_module).value(), protocol::ModuleEventBody::eReasonRemoved}}); } else if (module_exists) { dap->Send(protocol::Event{ "module", protocol::ModuleEventBody{ std::move(p_module).value(), protocol::ModuleEventBody::eReasonChanged}}); } else if (!remove_module) { dap->modules.insert(module_id); dap->Send(protocol::Event{ "module", protocol::ModuleEventBody{std::move(p_module).value(), protocol::ModuleEventBody::eReasonNew}}); } } } else if (event_mask & lldb::SBTarget::eBroadcastBitNewTargetCreated) { // For NewTargetCreated events, GetTargetFromEvent returns the parent // target, and GetCreatedTargetFromEvent returns the newly created target. lldb::SBTarget created_target = lldb::SBTarget::GetCreatedTargetFromEvent(event); if (!target.IsValid() || !created_target.IsValid()) { DAP_LOG(log, "Received NewTargetCreated event but parent or " "created target is invalid"); return; } // Send a startDebugging reverse request with the debugger and target // IDs. The new DAP instance will use these IDs to find the existing // debugger and target via FindDebuggerWithID and // FindTargetByGloballyUniqueID. llvm::json::Object configuration; configuration.try_emplace("type", "lldb"); configuration.try_emplace("debuggerId", created_target.GetDebugger().GetID()); configuration.try_emplace("targetId", created_target.GetGloballyUniqueID()); configuration.try_emplace("name", created_target.GetTargetSessionName()); llvm::json::Object request; request.try_emplace("request", "attach"); request.try_emplace("configuration", std::move(configuration)); dap->SendReverseRequest("startDebugging", std::move(request)); } } void HandleBreakpointEvent(const lldb::SBEvent &event, Log *log) { const uint32_t event_mask = event.GetType(); if (!(event_mask & lldb::SBTarget::eBroadcastBitBreakpointChanged)) return; lldb::SBBreakpoint bp = lldb::SBBreakpoint::GetBreakpointFromEvent(event); if (!bp.IsValid()) return; // Find the DAP instance that owns this breakpoint's target. DAP *dap = DAPSessionManager::FindDAP(bp.GetTarget()); if (!dap) { DAP_LOG(log, "Unable to find DAP instance for breakpoint"); return; } auto event_type = lldb::SBBreakpoint::GetBreakpointEventTypeFromEvent(event); auto breakpoint = Breakpoint(*dap, bp); // If the breakpoint was set through DAP, it will have the // BreakpointBase::kDAPBreakpointLabel. Regardless of whether // locations were added, removed, or resolved, the breakpoint isn't // going away and the reason is always "changed". if ((event_type & lldb::eBreakpointEventTypeLocationsAdded || event_type & lldb::eBreakpointEventTypeLocationsRemoved || event_type & lldb::eBreakpointEventTypeLocationsResolved) && breakpoint.MatchesName(BreakpointBase::kDAPBreakpointLabel)) { // As the DAP client already knows the path of this breakpoint, we // don't need to send it back as part of the "changed" event. This // avoids sending paths that should be source mapped. Note that // CreateBreakpoint doesn't apply source mapping and certain // implementation ignore the source part of this event anyway. protocol::Breakpoint protocol_bp = breakpoint.ToProtocolBreakpoint(); // "source" is not needed here, unless we add adapter data to be // saved by the client. if (protocol_bp.source && !protocol_bp.source->adapterData) protocol_bp.source = std::nullopt; llvm::json::Object body; body.try_emplace("breakpoint", protocol_bp); body.try_emplace("reason", "changed"); llvm::json::Object bp_event = CreateEventObject("breakpoint"); bp_event.try_emplace("body", std::move(body)); dap->SendJSON(llvm::json::Value(std::move(bp_event))); } } void HandleThreadEvent(const lldb::SBEvent &event, Log *log) { uint32_t event_type = event.GetType(); if (!(event_type & lldb::SBThread::eBroadcastBitStackChanged)) return; lldb::SBThread thread = lldb::SBThread::GetThreadFromEvent(event); if (!thread.IsValid()) return; // Find the DAP instance that owns this thread's process/target. DAP *dap = DAPSessionManager::FindDAP(thread.GetProcess().GetTarget()); if (!dap) { DAP_LOG(log, "Unable to find DAP instance for thread"); return; } SendInvalidatedEvent(*dap, {protocol::InvalidatedEventBody::eAreaStacks}, thread.GetThreadID()); } void HandleDiagnosticEvent(const lldb::SBEvent &event, Log *log) { // Global debugger events - send to all DAP instances. std::vector active_instances = DAPSessionManager::GetInstance().GetActiveSessions(); for (DAP *dap_instance : active_instances) { if (!dap_instance) continue; lldb::SBStructuredData data = lldb::SBDebugger::GetDiagnosticFromEvent(event); if (!data.IsValid()) continue; std::string type = GetStringValue(data.GetValueForKey("type")); std::string message = GetStringValue(data.GetValueForKey("message")); dap_instance->SendOutput(OutputType::Important, llvm::formatv("{0}: {1}", type, message).str()); } } // Note: EventThread() is architecturally different from the other functions in // this file. While the functions above are event helpers that operate on a // single DAP instance (taking `DAP &dap` as a parameter), EventThread() is a // shared event processing loop that: // 1. Listens to events from a shared debugger instance // 2. Dispatches events to the appropriate handler, which internally finds the // DAP instance using DAPSessionManager::FindDAP() // 3. Handles events for multiple different DAP sessions // This allows multiple DAP sessions to share a single debugger and event // thread, which is essential for the target handoff mechanism where child // processes/targets are debugged in separate DAP sessions. // // All events from the debugger, target, process, thread and frames are // received in this function that runs in its own thread. We are using a // "FILE *" to output packets back to VS Code and they have mutexes in them // them prevent multiple threads from writing simultaneously so no locking // is required. void EventThread(lldb::SBDebugger debugger, lldb::SBBroadcaster broadcaster, llvm::StringRef client_name, Log *log) { llvm::set_thread_name("lldb.DAP.client." + client_name + ".event_handler"); lldb::SBListener listener = debugger.GetListener(); broadcaster.AddListener(listener, eBroadcastBitStopEventThread); debugger.GetBroadcaster().AddListener( listener, lldb::eBroadcastBitError | lldb::eBroadcastBitWarning); // listen for thread events. listener.StartListeningForEventClass( debugger, lldb::SBThread::GetBroadcasterClassName(), lldb::SBThread::eBroadcastBitStackChanged); lldb::SBEvent event; bool done = false; while (!done) { if (!listener.WaitForEvent(UINT32_MAX, event)) continue; const uint32_t event_mask = event.GetType(); if (lldb::SBProcess::EventIsProcessEvent(event)) { HandleProcessEvent(event, /*&process_exited=*/done, log); } else if (lldb::SBTarget::EventIsTargetEvent(event)) { HandleTargetEvent(event, log); } else if (lldb::SBBreakpoint::EventIsBreakpointEvent(event)) { HandleBreakpointEvent(event, log); } else if (lldb::SBThread::EventIsThreadEvent(event)) { HandleThreadEvent(event, log); } else if (event_mask & lldb::eBroadcastBitError || event_mask & lldb::eBroadcastBitWarning) { HandleDiagnosticEvent(event, log); } else if (event.BroadcasterMatchesRef(broadcaster)) { if (event_mask & eBroadcastBitStopEventThread) { done = true; } } } } } // namespace lldb_dap