blob: 01d4547e2d228ef99fef8fd7da2455cb95e4cc9e [file] [log] [blame] [edit]
//===-- 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 <mutex>
#include <utility>
#if defined(_WIN32)
#define NOMINMAX
#include <windows.h>
#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<AdapterFeature> GetTargetBasedCapabilities(DAP &dap) {
llvm::DenseSet<AdapterFeature> 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<AdapterFeature> 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<lldb::SBMutex> guard(lock);
lldb::SBProcess process = dap.target.GetProcess();
if (!process.IsValid())
return make_error<DAPError>("invalid process");
lldb::StateType state = process.GetState();
if (!lldb::SBDebugger::StateIsStoppedState(state))
return make_error<NotStoppedError>();
llvm::DenseSet<lldb::tid_t> 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<protocol::InvalidatedEventBody::Area> 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.
static 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);
}
}
static 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<lldb::SBMutex, std::mutex> 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<protocol::Module> 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<LogFailureResponseHandler>("startDebugging",
std::move(request));
}
}
static 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)));
}
}
static 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());
}
static void HandleDiagnosticEvent(const lldb::SBEvent &event, Log &log) {
// Global debugger events - send to all DAP instances.
std::vector<DAP *> 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