//===-- DAP.cpp -------------------------------------------------*- C++ -*-===// // // 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 "DAP.h" #include "DAPLog.h" #include "EventHelper.h" #include "ExceptionBreakpoint.h" #include "Handler/RequestHandler.h" #include "Handler/ResponseHandler.h" #include "JSONUtils.h" #include "LLDBUtils.h" #include "OutputRedirector.h" #include "Protocol/ProtocolBase.h" #include "Protocol/ProtocolEvents.h" #include "Protocol/ProtocolRequests.h" #include "Protocol/ProtocolTypes.h" #include "ProtocolUtils.h" #include "Transport.h" #include "lldb/API/SBBreakpoint.h" #include "lldb/API/SBCommandInterpreter.h" #include "lldb/API/SBCommandReturnObject.h" #include "lldb/API/SBEvent.h" #include "lldb/API/SBLanguageRuntime.h" #include "lldb/API/SBListener.h" #include "lldb/API/SBProcess.h" #include "lldb/API/SBStream.h" #include "lldb/Utility/IOObject.h" #include "lldb/Utility/Status.h" #include "lldb/lldb-defines.h" #include "lldb/lldb-enumerations.h" #include "lldb/lldb-types.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/Twine.h" #include "llvm/Support/Chrono.h" #include "llvm/Support/Error.h" #include "llvm/Support/ErrorHandling.h" #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/raw_ostream.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) #define NOMINMAX #include #include #include #else #include #endif using namespace lldb_dap; using namespace lldb_dap::protocol; using namespace lldb_private; namespace { #ifdef _WIN32 const char DEV_NULL[] = "nul"; #else const char DEV_NULL[] = "/dev/null"; #endif } // namespace namespace lldb_dap { static std::string GetStringFromStructuredData(lldb::SBStructuredData &data, const char *key) { lldb::SBStructuredData keyValue = data.GetValueForKey(key); if (!keyValue) return std::string(); const size_t length = keyValue.GetStringValue(nullptr, 0); if (length == 0) return std::string(); std::string str(length + 1, 0); keyValue.GetStringValue(&str[0], length + 1); return str; } static uint64_t GetUintFromStructuredData(lldb::SBStructuredData &data, const char *key) { lldb::SBStructuredData keyValue = data.GetValueForKey(key); if (!keyValue.IsValid()) return 0; return keyValue.GetUnsignedIntegerValue(); } /// Return string with first character capitalized. static std::string capitalize(llvm::StringRef str) { if (str.empty()) return ""; return ((llvm::Twine)llvm::toUpper(str[0]) + str.drop_front()).str(); } llvm::StringRef DAP::debug_adapter_path = ""; DAP::DAP(Log *log, const ReplMode default_repl_mode, std::vector pre_init_commands, Transport &transport) : log(log), transport(transport), broadcaster("lldb-dap"), progress_event_reporter( [&](const ProgressEvent &event) { SendJSON(event.ToJSON()); }), repl_mode(default_repl_mode) { configuration.preInitCommands = std::move(pre_init_commands); RegisterRequests(); } DAP::~DAP() = default; void DAP::PopulateExceptionBreakpoints() { if (lldb::SBDebugger::SupportsLanguage(lldb::eLanguageTypeC_plus_plus)) { exception_breakpoints.emplace_back(*this, "cpp_catch", "C++ Catch", lldb::eLanguageTypeC_plus_plus, eExceptionKindCatch); exception_breakpoints.emplace_back(*this, "cpp_throw", "C++ Throw", lldb::eLanguageTypeC_plus_plus, eExceptionKindThrow); } if (lldb::SBDebugger::SupportsLanguage(lldb::eLanguageTypeObjC)) { exception_breakpoints.emplace_back(*this, "objc_catch", "Objective-C Catch", lldb::eLanguageTypeObjC, eExceptionKindCatch); exception_breakpoints.emplace_back(*this, "objc_throw", "Objective-C Throw", lldb::eLanguageTypeObjC, eExceptionKindThrow); } if (lldb::SBDebugger::SupportsLanguage(lldb::eLanguageTypeSwift)) { exception_breakpoints.emplace_back(*this, "swift_catch", "Swift Catch", lldb::eLanguageTypeSwift, eExceptionKindCatch); exception_breakpoints.emplace_back(*this, "swift_throw", "Swift Throw", lldb::eLanguageTypeSwift, eExceptionKindThrow); } // Besides handling the hardcoded list of languages from above, we try to find // any other languages that support exception breakpoints using the SB API. for (int raw_lang = lldb::eLanguageTypeUnknown; raw_lang < lldb::eNumLanguageTypes; ++raw_lang) { lldb::LanguageType lang = static_cast(raw_lang); // We first discard any languages already handled above. if (lldb::SBLanguageRuntime::LanguageIsCFamily(lang) || lang == lldb::eLanguageTypeSwift) continue; if (!lldb::SBDebugger::SupportsLanguage(lang)) continue; const char *name = lldb::SBLanguageRuntime::GetNameForLanguageType(lang); if (!name) continue; std::string raw_lang_name = name; std::string capitalized_lang_name = capitalize(name); if (lldb::SBLanguageRuntime::SupportsExceptionBreakpointsOnThrow(lang)) { const char *raw_throw_keyword = lldb::SBLanguageRuntime::GetThrowKeywordForLanguage(lang); std::string throw_keyword = raw_throw_keyword ? raw_throw_keyword : "throw"; exception_breakpoints.emplace_back( *this, raw_lang_name + "_" + throw_keyword, capitalized_lang_name + " " + capitalize(throw_keyword), lang, eExceptionKindThrow); } if (lldb::SBLanguageRuntime::SupportsExceptionBreakpointsOnCatch(lang)) { const char *raw_catch_keyword = lldb::SBLanguageRuntime::GetCatchKeywordForLanguage(lang); std::string catch_keyword = raw_catch_keyword ? raw_catch_keyword : "catch"; exception_breakpoints.emplace_back( *this, raw_lang_name + "_" + catch_keyword, capitalized_lang_name + " " + capitalize(catch_keyword), lang, eExceptionKindCatch); } } } ExceptionBreakpoint *DAP::GetExceptionBreakpoint(llvm::StringRef filter) { for (auto &bp : exception_breakpoints) { if (bp.GetFilter() == filter) return &bp; } return nullptr; } ExceptionBreakpoint *DAP::GetExceptionBreakpoint(const lldb::break_id_t bp_id) { for (auto &bp : exception_breakpoints) { if (bp.GetID() == bp_id) return &bp; } return nullptr; } llvm::Error DAP::ConfigureIO(std::FILE *overrideOut, std::FILE *overrideErr) { in = lldb::SBFile(std::fopen(DEV_NULL, "r"), /*transfer_ownership=*/true); if (auto Error = out.RedirectTo(overrideOut, [this](llvm::StringRef output) { SendOutput(OutputType::Console, output); })) return Error; if (auto Error = err.RedirectTo(overrideErr, [this](llvm::StringRef output) { SendOutput(OutputType::Console, output); })) return Error; return llvm::Error::success(); } void DAP::StopEventHandlers() { if (event_thread.joinable()) { broadcaster.BroadcastEventByType(eBroadcastBitStopEventThread); event_thread.join(); } if (progress_event_thread.joinable()) { broadcaster.BroadcastEventByType(eBroadcastBitStopProgressThread); progress_event_thread.join(); } } // Serialize the JSON value into a string and send the JSON packet to // the "out" stream. void DAP::SendJSON(const llvm::json::Value &json) { // FIXME: Instead of parsing the output message from JSON, pass the `Message` // as parameter to `SendJSON`. Message message; llvm::json::Path::Root root; if (!fromJSON(json, message, root)) { DAP_LOG_ERROR(log, root.getError(), "({1}) encoding failed: {0}", transport.GetClientName()); return; } Send(message); } void DAP::Send(const Message &message) { // FIXME: After all the requests have migrated from LegacyRequestHandler > // RequestHandler<> this should be handled in RequestHandler<>::operator(). if (auto *resp = std::get_if(&message); resp && debugger.InterruptRequested()) { // Clear the interrupt request. debugger.CancelInterruptRequest(); // If the debugger was interrupted, convert this response into a 'cancelled' // response because we might have a partial result. Response cancelled{/*request_seq=*/resp->request_seq, /*command=*/resp->command, /*success=*/false, /*message=*/eResponseMessageCancelled, /*body=*/std::nullopt}; if (llvm::Error err = transport.Write(cancelled)) DAP_LOG_ERROR(log, std::move(err), "({1}) write failed: {0}", transport.GetClientName()); return; } if (llvm::Error err = transport.Write(message)) DAP_LOG_ERROR(log, std::move(err), "({1}) write failed: {0}", transport.GetClientName()); } // "OutputEvent": { // "allOf": [ { "$ref": "#/definitions/Event" }, { // "type": "object", // "description": "Event message for 'output' event type. The event // indicates that the target has produced some output.", // "properties": { // "event": { // "type": "string", // "enum": [ "output" ] // }, // "body": { // "type": "object", // "properties": { // "category": { // "type": "string", // "description": "The output category. If not specified, // 'console' is assumed.", // "_enum": [ "console", "stdout", "stderr", "telemetry" ] // }, // "output": { // "type": "string", // "description": "The output to report." // }, // "variablesReference": { // "type": "number", // "description": "If an attribute 'variablesReference' exists // and its value is > 0, the output contains // objects which can be retrieved by passing // variablesReference to the VariablesRequest." // }, // "source": { // "$ref": "#/definitions/Source", // "description": "An optional source location where the output // was produced." // }, // "line": { // "type": "integer", // "description": "An optional source location line where the // output was produced." // }, // "column": { // "type": "integer", // "description": "An optional source location column where the // output was produced." // }, // "data": { // "type":["array","boolean","integer","null","number","object", // "string"], // "description": "Optional data to report. For the 'telemetry' // category the data will be sent to telemetry, for // the other categories the data is shown in JSON // format." // } // }, // "required": ["output"] // } // }, // "required": [ "event", "body" ] // }] // } void DAP::SendOutput(OutputType o, const llvm::StringRef output) { if (output.empty()) return; const char *category = nullptr; switch (o) { case OutputType::Console: category = "console"; break; case OutputType::Important: category = "important"; break; case OutputType::Stdout: category = "stdout"; break; case OutputType::Stderr: category = "stderr"; break; case OutputType::Telemetry: category = "telemetry"; break; } // Send each line of output as an individual event, including the newline if // present. ::size_t idx = 0; do { ::size_t end = output.find('\n', idx); if (end == llvm::StringRef::npos) end = output.size() - 1; llvm::json::Object event(CreateEventObject("output")); llvm::json::Object body; body.try_emplace("category", category); EmplaceSafeString(body, "output", output.slice(idx, end + 1).str()); event.try_emplace("body", std::move(body)); SendJSON(llvm::json::Value(std::move(event))); idx = end + 1; } while (idx < output.size()); } // interface ProgressStartEvent extends Event { // event: 'progressStart'; // // body: { // /** // * An ID that must be used in subsequent 'progressUpdate' and // 'progressEnd' // * events to make them refer to the same progress reporting. // * IDs must be unique within a debug session. // */ // progressId: string; // // /** // * Mandatory (short) title of the progress reporting. Shown in the UI to // * describe the long running operation. // */ // title: string; // // /** // * The request ID that this progress report is related to. If specified a // * debug adapter is expected to emit // * progress events for the long running request until the request has // been // * either completed or cancelled. // * If the request ID is omitted, the progress report is assumed to be // * related to some general activity of the debug adapter. // */ // requestId?: number; // // /** // * If true, the request that reports progress may be canceled with a // * 'cancel' request. // * So this property basically controls whether the client should use UX // that // * supports cancellation. // * Clients that don't support cancellation are allowed to ignore the // * setting. // */ // cancellable?: boolean; // // /** // * Optional, more detailed progress message. // */ // message?: string; // // /** // * Optional progress percentage to display (value range: 0 to 100). If // * omitted no percentage will be shown. // */ // percentage?: number; // }; // } // // interface ProgressUpdateEvent extends Event { // event: 'progressUpdate'; // // body: { // /** // * The ID that was introduced in the initial 'progressStart' event. // */ // progressId: string; // // /** // * Optional, more detailed progress message. If omitted, the previous // * message (if any) is used. // */ // message?: string; // // /** // * Optional progress percentage to display (value range: 0 to 100). If // * omitted no percentage will be shown. // */ // percentage?: number; // }; // } // // interface ProgressEndEvent extends Event { // event: 'progressEnd'; // // body: { // /** // * The ID that was introduced in the initial 'ProgressStartEvent'. // */ // progressId: string; // // /** // * Optional, more detailed progress message. If omitted, the previous // * message (if any) is used. // */ // message?: string; // }; // } void DAP::SendProgressEvent(uint64_t progress_id, const char *message, uint64_t completed, uint64_t total) { progress_event_reporter.Push(progress_id, message, completed, total); } void __attribute__((format(printf, 3, 4))) DAP::SendFormattedOutput(OutputType o, const char *format, ...) { char buffer[1024]; va_list args; va_start(args, format); int actual_length = vsnprintf(buffer, sizeof(buffer), format, args); va_end(args); SendOutput( o, llvm::StringRef(buffer, std::min(actual_length, sizeof(buffer)))); } int32_t DAP::CreateSourceReference(lldb::addr_t address) { std::lock_guard guard(m_source_references_mutex); auto iter = llvm::find(m_source_references, address); if (iter != m_source_references.end()) return std::distance(m_source_references.begin(), iter) + 1; m_source_references.emplace_back(address); return static_cast(m_source_references.size()); } std::optional DAP::GetSourceReferenceAddress(int32_t reference) { std::lock_guard guard(m_source_references_mutex); if (reference <= LLDB_DAP_INVALID_SRC_REF) return std::nullopt; if (static_cast(reference) > m_source_references.size()) return std::nullopt; return m_source_references[reference - 1]; } ExceptionBreakpoint *DAP::GetExceptionBPFromStopReason(lldb::SBThread &thread) { const auto num = thread.GetStopReasonDataCount(); // Check to see if have hit an exception breakpoint and change the // reason to "exception", but only do so if all breakpoints that were // hit are exception breakpoints. ExceptionBreakpoint *exc_bp = nullptr; for (size_t i = 0; i < num; i += 2) { // thread.GetStopReasonDataAtIndex(i) will return the bp ID and // thread.GetStopReasonDataAtIndex(i+1) will return the location // within that breakpoint. We only care about the bp ID so we can // see if this is an exception breakpoint that is getting hit. lldb::break_id_t bp_id = thread.GetStopReasonDataAtIndex(i); exc_bp = GetExceptionBreakpoint(bp_id); // If any breakpoint is not an exception breakpoint, then stop and // report this as a normal breakpoint if (exc_bp == nullptr) return nullptr; } return exc_bp; } lldb::SBThread DAP::GetLLDBThread(lldb::tid_t tid) { return target.GetProcess().GetThreadByID(tid); } lldb::SBThread DAP::GetLLDBThread(const llvm::json::Object &arguments) { auto tid = GetInteger(arguments, "threadId") .value_or(LLDB_INVALID_THREAD_ID); return target.GetProcess().GetThreadByID(tid); } lldb::SBFrame DAP::GetLLDBFrame(uint64_t frame_id) { lldb::SBProcess process = target.GetProcess(); // Upper 32 bits is the thread index ID lldb::SBThread thread = process.GetThreadByIndexID(GetLLDBThreadIndexID(frame_id)); // Lower 32 bits is the frame index return thread.GetFrameAtIndex(GetLLDBFrameID(frame_id)); } lldb::SBFrame DAP::GetLLDBFrame(const llvm::json::Object &arguments) { const auto frame_id = GetInteger(arguments, "frameId").value_or(UINT64_MAX); return GetLLDBFrame(frame_id); } ReplMode DAP::DetectReplMode(lldb::SBFrame frame, std::string &expression, bool partial_expression) { // Check for the escape hatch prefix. if (!expression.empty() && llvm::StringRef(expression) .starts_with(configuration.commandEscapePrefix)) { expression = expression.substr(configuration.commandEscapePrefix.size()); return ReplMode::Command; } switch (repl_mode) { case ReplMode::Variable: return ReplMode::Variable; case ReplMode::Command: return ReplMode::Command; case ReplMode::Auto: // To determine if the expression is a command or not, check if the first // term is a variable or command. If it's a variable in scope we will prefer // that behavior and give a warning to the user if they meant to invoke the // operation as a command. // // Example use case: // int p and expression "p + 1" > variable // int i and expression "i" > variable // int var and expression "va" > command std::pair token = llvm::getToken(expression); // If the first token is not fully finished yet, we can't // determine whether this will be a variable or a lldb command. if (partial_expression && token.second.empty()) return ReplMode::Auto; std::string term = token.first.str(); lldb::SBCommandInterpreter interpreter = debugger.GetCommandInterpreter(); bool term_is_command = interpreter.CommandExists(term.c_str()) || interpreter.UserCommandExists(term.c_str()) || interpreter.AliasExists(term.c_str()); bool term_is_variable = frame.FindVariable(term.c_str()).IsValid(); // If we have both a variable and command, warn the user about the conflict. if (term_is_command && term_is_variable) { llvm::errs() << "Warning: Expression '" << term << "' is both an LLDB command and variable. It will be evaluated as " "a variable. To evaluate the expression as an LLDB command, use '" << configuration.commandEscapePrefix << "' as a prefix.\n"; } // Variables take preference to commands in auto, since commands can always // be called using the command_escape_prefix return term_is_variable ? ReplMode::Variable : term_is_command ? ReplMode::Command : ReplMode::Variable; } llvm_unreachable("enum cases exhausted."); } std::optional DAP::ResolveSource(const lldb::SBFrame &frame) { if (!frame.IsValid()) return std::nullopt; const lldb::SBAddress frame_pc = frame.GetPCAddress(); if (DisplayAssemblySource(debugger, frame_pc)) return ResolveAssemblySource(frame_pc); return CreateSource(frame.GetLineEntry().GetFileSpec()); } std::optional DAP::ResolveSource(lldb::SBAddress address) { if (DisplayAssemblySource(debugger, address)) return ResolveAssemblySource(address); lldb::SBLineEntry line_entry = GetLineEntryForAddress(target, address); if (!line_entry.IsValid()) return std::nullopt; return CreateSource(line_entry.GetFileSpec()); } std::optional DAP::ResolveAssemblySource(lldb::SBAddress address) { lldb::SBSymbol symbol = address.GetSymbol(); lldb::addr_t load_addr = LLDB_INVALID_ADDRESS; std::string name; if (symbol.IsValid()) { load_addr = symbol.GetStartAddress().GetLoadAddress(target); name = symbol.GetName(); } else { load_addr = address.GetLoadAddress(target); name = GetLoadAddressString(load_addr); } if (load_addr == LLDB_INVALID_ADDRESS) return std::nullopt; protocol::Source source; source.sourceReference = CreateSourceReference(load_addr); lldb::SBModule module = address.GetModule(); if (module.IsValid()) { lldb::SBFileSpec file_spec = module.GetFileSpec(); if (file_spec.IsValid()) { std::string path = GetSBFileSpecPath(file_spec); if (!path.empty()) source.path = path + '`' + name; } } source.name = std::move(name); // Mark the source as deemphasized since users will only be able to view // assembly for these frames. source.presentationHint = protocol::Source::eSourcePresentationHintDeemphasize; return source; } bool DAP::RunLLDBCommands(llvm::StringRef prefix, llvm::ArrayRef commands) { bool required_command_failed = false; std::string output = ::RunLLDBCommands( debugger, prefix, commands, required_command_failed, /*parse_command_directives*/ true, /*echo_commands*/ true); SendOutput(OutputType::Console, output); return !required_command_failed; } static llvm::Error createRunLLDBCommandsErrorMessage(llvm::StringRef category) { return llvm::createStringError( llvm::inconvertibleErrorCode(), llvm::formatv( "Failed to run {0} commands. See the Debug Console for more details.", category) .str() .c_str()); } llvm::Error DAP::RunAttachCommands(llvm::ArrayRef attach_commands) { if (!RunLLDBCommands("Running attachCommands:", attach_commands)) return createRunLLDBCommandsErrorMessage("attach"); return llvm::Error::success(); } llvm::Error DAP::RunLaunchCommands(llvm::ArrayRef launch_commands) { if (!RunLLDBCommands("Running launchCommands:", launch_commands)) return createRunLLDBCommandsErrorMessage("launch"); return llvm::Error::success(); } llvm::Error DAP::RunInitCommands() { if (!RunLLDBCommands("Running initCommands:", configuration.initCommands)) return createRunLLDBCommandsErrorMessage("initCommands"); return llvm::Error::success(); } llvm::Error DAP::RunPreInitCommands() { if (!RunLLDBCommands("Running preInitCommands:", configuration.preInitCommands)) return createRunLLDBCommandsErrorMessage("preInitCommands"); return llvm::Error::success(); } llvm::Error DAP::RunPreRunCommands() { if (!RunLLDBCommands("Running preRunCommands:", configuration.preRunCommands)) return createRunLLDBCommandsErrorMessage("preRunCommands"); return llvm::Error::success(); } void DAP::RunPostRunCommands() { RunLLDBCommands("Running postRunCommands:", configuration.postRunCommands); } void DAP::RunStopCommands() { RunLLDBCommands("Running stopCommands:", configuration.stopCommands); } void DAP::RunExitCommands() { RunLLDBCommands("Running exitCommands:", configuration.exitCommands); } void DAP::RunTerminateCommands() { RunLLDBCommands("Running terminateCommands:", configuration.terminateCommands); } lldb::SBTarget DAP::CreateTarget(lldb::SBError &error) { // Grab the name of the program we need to debug and create a target using // the given program as an argument. Executable file can be a source of target // architecture and platform, if they differ from the host. Setting exe path // in launch info is useless because Target.Launch() will not change // architecture and platform, therefore they should be known at the target // creation. We also use target triple and platform from the launch // configuration, if given, since in some cases ELF file doesn't contain // enough information to determine correct arch and platform (or ELF can be // omitted at all), so it is good to leave the user an opportunity to specify // those. Any of those three can be left empty. auto target = this->debugger.CreateTarget( /*filename=*/configuration.program.data(), /*target_triple=*/configuration.targetTriple.data(), /*platform_name=*/configuration.platformName.data(), /*add_dependent_modules=*/true, // Add dependent modules. error); return target; } void DAP::SetTarget(const lldb::SBTarget target) { this->target = target; if (target.IsValid()) { // Configure breakpoint event listeners for the target. lldb::SBListener listener = this->debugger.GetListener(); listener.StartListeningForEvents( this->target.GetBroadcaster(), lldb::SBTarget::eBroadcastBitBreakpointChanged | lldb::SBTarget::eBroadcastBitModulesLoaded | lldb::SBTarget::eBroadcastBitModulesUnloaded | lldb::SBTarget::eBroadcastBitSymbolsLoaded | lldb::SBTarget::eBroadcastBitSymbolsChanged); listener.StartListeningForEvents(this->broadcaster, eBroadcastBitStopEventThread); } } bool DAP::HandleObject(const Message &M) { TelemetryDispatcher dispatcher(&debugger); dispatcher.Set("client_name", transport.GetClientName().str()); if (const auto *req = std::get_if(&M)) { { std::lock_guard guard(m_active_request_mutex); m_active_request = req; // Clear the interrupt request prior to invoking a handler. if (debugger.InterruptRequested()) debugger.CancelInterruptRequest(); } auto cleanup = llvm::make_scope_exit([&]() { std::scoped_lock active_request_lock(m_active_request_mutex); m_active_request = nullptr; }); auto handler_pos = request_handlers.find(req->command); dispatcher.Set("client_data", llvm::Twine("request_command:", req->command).str()); if (handler_pos != request_handlers.end()) { handler_pos->second->Run(*req); return true; // Success } dispatcher.Set("error", llvm::Twine("unhandled-command:" + req->command).str()); DAP_LOG(log, "({0}) error: unhandled command '{1}'", transport.GetClientName(), req->command); return false; // Fail } if (const auto *resp = std::get_if(&M)) { std::unique_ptr response_handler; { std::lock_guard guard(call_mutex); auto inflight = inflight_reverse_requests.find(resp->request_seq); if (inflight != inflight_reverse_requests.end()) { response_handler = std::move(inflight->second); inflight_reverse_requests.erase(inflight); } } if (!response_handler) response_handler = std::make_unique("", resp->request_seq); // Result should be given, use null if not. if (resp->success) { (*response_handler)(resp->body); dispatcher.Set("client_data", llvm::Twine("response_command:", resp->command).str()); } else { llvm::StringRef message = "Unknown error, response failed"; if (resp->message) { message = std::visit(llvm::makeVisitor( [](const std::string &message) -> llvm::StringRef { return message; }, [](const protocol::ResponseMessage &message) -> llvm::StringRef { switch (message) { case protocol::eResponseMessageCancelled: return "cancelled"; case protocol::eResponseMessageNotStopped: return "notStopped"; } llvm_unreachable("unknown response message kind."); }), *resp->message); } dispatcher.Set("error", message.str()); (*response_handler)(llvm::createStringError( std::error_code(-1, std::generic_category()), message)); } return true; } dispatcher.Set("error", "Unsupported protocol message"); DAP_LOG(log, "Unsupported protocol message"); return false; } void DAP::SendTerminatedEvent() { // Prevent races if the process exits while we're being asked to disconnect. llvm::call_once(terminated_event_flag, [&] { RunTerminateCommands(); // Send a "terminated" event llvm::json::Object event(CreateTerminatedEventObject(target)); SendJSON(llvm::json::Value(std::move(event))); }); } llvm::Error DAP::Disconnect() { return Disconnect(!is_attach); } llvm::Error DAP::Disconnect(bool terminateDebuggee) { lldb::SBError error; lldb::SBProcess process = target.GetProcess(); auto state = process.GetState(); switch (state) { case lldb::eStateInvalid: case lldb::eStateUnloaded: case lldb::eStateDetached: case lldb::eStateExited: break; case lldb::eStateConnected: case lldb::eStateAttaching: case lldb::eStateLaunching: case lldb::eStateStepping: case lldb::eStateCrashed: case lldb::eStateSuspended: case lldb::eStateStopped: case lldb::eStateRunning: { ScopeSyncMode scope_sync_mode(debugger); error = terminateDebuggee ? process.Kill() : process.Detach(); break; } } SendTerminatedEvent(); disconnecting = true; return ToError(error); } bool DAP::IsCancelled(const protocol::Request &req) { std::lock_guard guard(m_cancelled_requests_mutex); return m_cancelled_requests.contains(req.seq); } void DAP::ClearCancelRequest(const CancelArguments &args) { std::lock_guard guard(m_cancelled_requests_mutex); if (args.requestId) m_cancelled_requests.erase(*args.requestId); } template static std::optional getArgumentsIfRequest(const Message &pm, llvm::StringLiteral command) { auto *const req = std::get_if(&pm); if (!req || req->command != command) return std::nullopt; T args; llvm::json::Path::Root root; if (!fromJSON(req->arguments, args, root)) return std::nullopt; return args; } llvm::Error DAP::Loop() { // Can't use \a std::future because it doesn't compile on // Windows. std::future queue_reader = std::async(std::launch::async, [&]() -> lldb::SBError { llvm::set_thread_name(transport.GetClientName() + ".transport_handler"); auto cleanup = llvm::make_scope_exit([&]() { // Ensure we're marked as disconnecting when the reader exits. disconnecting = true; m_queue_cv.notify_all(); }); while (!disconnecting) { llvm::Expected next = transport.Read(std::chrono::seconds(1)); if (next.errorIsA()) { consumeError(next.takeError()); break; } // If the read timed out, continue to check if we should disconnect. if (next.errorIsA()) { consumeError(next.takeError()); continue; } if (llvm::Error err = next.takeError()) { lldb::SBError errWrapper; errWrapper.SetErrorString(llvm::toString(std::move(err)).c_str()); return errWrapper; } if (const protocol::Request *req = std::get_if(&*next); req && req->command == "disconnect") disconnecting = true; const std::optional cancel_args = getArgumentsIfRequest(*next, "cancel"); if (cancel_args) { { std::lock_guard guard(m_cancelled_requests_mutex); if (cancel_args->requestId) m_cancelled_requests.insert(*cancel_args->requestId); } // If a cancel is requested for the active request, make a best // effort attempt to interrupt. std::lock_guard guard(m_active_request_mutex); if (m_active_request && cancel_args->requestId == m_active_request->seq) { DAP_LOG( log, "({0}) interrupting inflight request (command={1} seq={2})", transport.GetClientName(), m_active_request->command, m_active_request->seq); debugger.RequestInterrupt(); } } { std::lock_guard guard(m_queue_mutex); m_queue.push_back(std::move(*next)); } m_queue_cv.notify_one(); } return lldb::SBError(); }); auto cleanup = llvm::make_scope_exit([&]() { out.Stop(); err.Stop(); StopEventHandlers(); }); while (true) { std::unique_lock lock(m_queue_mutex); m_queue_cv.wait(lock, [&] { return disconnecting || !m_queue.empty(); }); if (disconnecting && m_queue.empty()) break; Message next = m_queue.front(); m_queue.pop_front(); // Unlock while we're processing the event. lock.unlock(); if (!HandleObject(next)) return llvm::createStringError(llvm::inconvertibleErrorCode(), "unhandled packet"); } return ToError(queue_reader.get()); } lldb::SBError DAP::WaitForProcessToStop(std::chrono::seconds seconds) { lldb::SBError error; lldb::SBProcess process = target.GetProcess(); if (!process.IsValid()) { error.SetErrorString("invalid process"); return error; } auto timeout_time = std::chrono::steady_clock::now() + std::chrono::seconds(seconds); while (std::chrono::steady_clock::now() < timeout_time) { const auto state = process.GetState(); switch (state) { case lldb::eStateUnloaded: case lldb::eStateAttaching: case lldb::eStateConnected: case lldb::eStateInvalid: case lldb::eStateLaunching: case lldb::eStateRunning: case lldb::eStateStepping: case lldb::eStateSuspended: break; case lldb::eStateDetached: error.SetErrorString("process detached during launch or attach"); return error; case lldb::eStateExited: error.SetErrorString("process exited during launch or attach"); return error; case lldb::eStateCrashed: case lldb::eStateStopped: return lldb::SBError(); // Success! } std::this_thread::sleep_for(std::chrono::microseconds(250)); } error.SetErrorString( llvm::formatv("process failed to stop within {0}", seconds) .str() .c_str()); return error; } void DAP::ConfigureSourceMaps() { if (configuration.sourceMap.empty() && configuration.sourcePath.empty()) return; std::string sourceMapCommand; llvm::raw_string_ostream strm(sourceMapCommand); strm << "settings set target.source-map "; if (!configuration.sourceMap.empty()) { for (const auto &kv : configuration.sourceMap) { strm << "\"" << kv.first << "\" \"" << kv.second << "\" "; } } else if (!configuration.sourcePath.empty()) { strm << "\".\" \"" << configuration.sourcePath << "\""; } RunLLDBCommands("Setting source map:", {sourceMapCommand}); } void DAP::SetConfiguration(const protocol::Configuration &config, bool is_attach) { configuration = config; stop_at_entry = config.stopOnEntry; this->is_attach = is_attach; if (configuration.customFrameFormat) SetFrameFormat(*configuration.customFrameFormat); if (configuration.customThreadFormat) SetThreadFormat(*configuration.customThreadFormat); } void DAP::SetFrameFormat(llvm::StringRef format) { lldb::SBError error; frame_format = lldb::SBFormat(format.str().c_str(), error); if (error.Fail()) { SendOutput(OutputType::Console, llvm::formatv( "The provided frame format '{0}' couldn't be parsed: {1}\n", format, error.GetCString()) .str()); } } void DAP::SetThreadFormat(llvm::StringRef format) { lldb::SBError error; thread_format = lldb::SBFormat(format.str().c_str(), error); if (error.Fail()) { SendOutput(OutputType::Console, llvm::formatv( "The provided thread format '{0}' couldn't be parsed: {1}\n", format, error.GetCString()) .str()); } } InstructionBreakpoint * DAP::GetInstructionBreakpoint(const lldb::break_id_t bp_id) { for (auto &bp : instruction_breakpoints) { if (bp.second.GetID() == bp_id) return &bp.second; } return nullptr; } InstructionBreakpoint * DAP::GetInstructionBPFromStopReason(lldb::SBThread &thread) { const auto num = thread.GetStopReasonDataCount(); InstructionBreakpoint *inst_bp = nullptr; for (size_t i = 0; i < num; i += 2) { // thread.GetStopReasonDataAtIndex(i) will return the bp ID and // thread.GetStopReasonDataAtIndex(i+1) will return the location // within that breakpoint. We only care about the bp ID so we can // see if this is an instruction breakpoint that is getting hit. lldb::break_id_t bp_id = thread.GetStopReasonDataAtIndex(i); inst_bp = GetInstructionBreakpoint(bp_id); // If any breakpoint is not an instruction breakpoint, then stop and // report this as a normal breakpoint if (inst_bp == nullptr) return nullptr; } return inst_bp; } protocol::Capabilities DAP::GetCapabilities() { protocol::Capabilities capabilities; // Supported capabilities that are not specific to a single request. capabilities.supportedFeatures = { protocol::eAdapterFeatureLogPoints, protocol::eAdapterFeatureSteppingGranularity, protocol::eAdapterFeatureValueFormattingOptions, }; // Capabilities associated with specific requests. for (auto &kv : request_handlers) { llvm::SmallDenseSet features = kv.second->GetSupportedFeatures(); capabilities.supportedFeatures.insert(features.begin(), features.end()); } // Available filters or options for the setExceptionBreakpoints request. PopulateExceptionBreakpoints(); std::vector filters; for (const auto &exc_bp : exception_breakpoints) filters.emplace_back(CreateExceptionBreakpointFilter(exc_bp)); capabilities.exceptionBreakpointFilters = std::move(filters); // FIXME: This should be registered based on the supported languages? std::vector completion_characters; completion_characters.emplace_back("."); // FIXME: I wonder if we should remove this key... its very aggressive // triggering and accepting completions. completion_characters.emplace_back(" "); completion_characters.emplace_back("\t"); capabilities.completionTriggerCharacters = std::move(completion_characters); // Put in non-DAP specification lldb specific information. capabilities.lldbExtVersion = debugger.GetVersionString(); return capabilities; } void DAP::StartEventThread() { event_thread = std::thread(&DAP::EventThread, this); } void DAP::StartProgressEventThread() { progress_event_thread = std::thread(&DAP::ProgressEventThread, this); } void DAP::ProgressEventThread() { lldb::SBListener listener("lldb-dap.progress.listener"); debugger.GetBroadcaster().AddListener( listener, lldb::SBDebugger::eBroadcastBitProgress | lldb::SBDebugger::eBroadcastBitExternalProgress); broadcaster.AddListener(listener, eBroadcastBitStopProgressThread); lldb::SBEvent event; bool done = false; while (!done) { if (listener.WaitForEvent(1, event)) { const auto event_mask = event.GetType(); if (event.BroadcasterMatchesRef(broadcaster)) { if (event_mask & eBroadcastBitStopProgressThread) { done = true; } } else { lldb::SBStructuredData data = lldb::SBDebugger::GetProgressDataFromEvent(event); const uint64_t progress_id = GetUintFromStructuredData(data, "progress_id"); const uint64_t completed = GetUintFromStructuredData(data, "completed"); const uint64_t total = GetUintFromStructuredData(data, "total"); const std::string details = GetStringFromStructuredData(data, "details"); if (completed == 0) { if (total == UINT64_MAX) { // This progress is non deterministic and won't get updated until it // is completed. Send the "message" which will be the combined title // and detail. The only other progress event for thus // non-deterministic progress will be the completed event So there // will be no need to update the detail. const std::string message = GetStringFromStructuredData(data, "message"); SendProgressEvent(progress_id, message.c_str(), completed, total); } else { // This progress is deterministic and will receive updates, // on the progress creation event VSCode will save the message in // the create packet and use that as the title, so we send just the // title in the progressCreate packet followed immediately by a // detail packet, if there is any detail. const std::string title = GetStringFromStructuredData(data, "title"); SendProgressEvent(progress_id, title.c_str(), completed, total); if (!details.empty()) SendProgressEvent(progress_id, details.c_str(), completed, total); } } else { // This progress event is either the end of the progress dialog, or an // update with possible detail. The "detail" string we send to VS Code // will be appended to the progress dialog's initial text from when it // was created. SendProgressEvent(progress_id, details.c_str(), completed, total); } } } } } // 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 DAP::EventThread() { llvm::set_thread_name(transport.GetClientName() + ".event_handler"); lldb::SBEvent event; lldb::SBListener listener = debugger.GetListener(); broadcaster.AddListener(listener, eBroadcastBitStopEventThread); debugger.GetBroadcaster().AddListener( listener, lldb::eBroadcastBitError | lldb::eBroadcastBitWarning); bool done = false; while (!done) { if (listener.WaitForEvent(1, event)) { const auto event_mask = event.GetType(); if (lldb::SBProcess::EventIsProcessEvent(event)) { lldb::SBProcess process = lldb::SBProcess::GetProcessFromEvent(event); 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(*this, process); if (llvm::Error err = SendThreadStoppedEvent(*this)) DAP_LOG_ERROR(log, std::move(err), "({1}) reporting thread stopped: {0}", transport.GetClientName()); } break; case lldb::eStateRunning: case lldb::eStateStepping: WillContinue(); SendContinuedEvent(*this); break; case lldb::eStateExited: lldb::SBStream stream; process.GetStatus(stream); 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() == restarting_process_id) { restarting_process_id = LLDB_INVALID_PROCESS_ID; } else { // Run any exit LLDB commands the user specified in the // launch.json RunExitCommands(); SendProcessExitedEvent(*this, process); SendTerminatedEvent(); done = true; } break; } } else if ((event_mask & lldb::SBProcess::eBroadcastBitSTDOUT) || (event_mask & lldb::SBProcess::eBroadcastBitSTDERR)) { SendStdOutStdErr(*this, process); } } else if (lldb::SBTarget::EventIsTargetEvent(event)) { 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; std::lock_guard guard(modules_mutex); for (uint32_t i = 0; i < num_modules; ++i) { lldb::SBModule module = lldb::SBTarget::GetModuleAtIndexFromEvent(i, event); std::optional p_module = CreateModule(target, module, remove_module); if (!p_module) continue; llvm::StringRef module_id = p_module->id; const bool module_exists = modules.contains(module_id); if (remove_module && module_exists) { modules.erase(module_id); Send(protocol::Event{ "module", ModuleEventBody{std::move(p_module).value(), ModuleEventBody::eReasonRemoved}}); } else if (module_exists) { Send(protocol::Event{ "module", ModuleEventBody{std::move(p_module).value(), ModuleEventBody::eReasonChanged}}); } else if (!remove_module) { modules.insert(module_id); Send(protocol::Event{ "module", ModuleEventBody{std::move(p_module).value(), ModuleEventBody::eReasonNew}}); } } } } else if (lldb::SBBreakpoint::EventIsBreakpointEvent(event)) { if (event_mask & lldb::SBTarget::eBroadcastBitBreakpointChanged) { auto event_type = lldb::SBBreakpoint::GetBreakpointEventTypeFromEvent(event); auto bp = Breakpoint( *this, lldb::SBBreakpoint::GetBreakpointFromEvent(event)); // 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) && bp.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. llvm::json::Value source_bp = bp.ToProtocolBreakpoint(); source_bp.getAsObject()->erase("source"); llvm::json::Object body; body.try_emplace("breakpoint", source_bp); body.try_emplace("reason", "changed"); llvm::json::Object bp_event = CreateEventObject("breakpoint"); bp_event.try_emplace("body", std::move(body)); SendJSON(llvm::json::Value(std::move(bp_event))); } } } else if (event_mask & lldb::eBroadcastBitError || event_mask & lldb::eBroadcastBitWarning) { 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")); SendOutput(OutputType::Important, llvm::formatv("{0}: {1}", type, message).str()); } else if (event.BroadcasterMatchesRef(broadcaster)) { if (event_mask & eBroadcastBitStopEventThread) { done = true; } } } } } std::vector DAP::SetSourceBreakpoints( const protocol::Source &source, const std::optional> &breakpoints) { std::vector response_breakpoints; if (source.sourceReference) { // Breakpoint set by assembly source. auto &existing_breakpoints = m_source_assembly_breakpoints[*source.sourceReference]; response_breakpoints = SetSourceBreakpoints(source, breakpoints, existing_breakpoints); } else { // Breakpoint set by a regular source file. const auto path = source.path.value_or(""); auto &existing_breakpoints = m_source_breakpoints[path]; response_breakpoints = SetSourceBreakpoints(source, breakpoints, existing_breakpoints); } return response_breakpoints; } std::vector DAP::SetSourceBreakpoints( const protocol::Source &source, const std::optional> &breakpoints, SourceBreakpointMap &existing_breakpoints) { std::vector response_breakpoints; SourceBreakpointMap request_breakpoints; if (breakpoints) { for (const auto &bp : *breakpoints) { SourceBreakpoint src_bp(*this, bp); std::pair bp_pos(src_bp.GetLine(), src_bp.GetColumn()); request_breakpoints.try_emplace(bp_pos, src_bp); const auto [iv, inserted] = existing_breakpoints.try_emplace(bp_pos, src_bp); // We check if this breakpoint already exists to update it. if (inserted) { if (llvm::Error error = iv->second.SetBreakpoint(source)) { protocol::Breakpoint invalid_breakpoint; invalid_breakpoint.message = llvm::toString(std::move(error)); invalid_breakpoint.verified = false; response_breakpoints.push_back(std::move(invalid_breakpoint)); existing_breakpoints.erase(iv); continue; } } else { iv->second.UpdateBreakpoint(src_bp); } protocol::Breakpoint response_breakpoint = iv->second.ToProtocolBreakpoint(); response_breakpoint.source = source; if (!response_breakpoint.line && src_bp.GetLine() != LLDB_INVALID_LINE_NUMBER) response_breakpoint.line = src_bp.GetLine(); if (!response_breakpoint.column && src_bp.GetColumn() != LLDB_INVALID_COLUMN_NUMBER) response_breakpoint.column = src_bp.GetColumn(); response_breakpoints.push_back(std::move(response_breakpoint)); } } // Delete any breakpoints in this source file that aren't in the // request_bps set. There is no call to remove breakpoints other than // calling this function with a smaller or empty "breakpoints" list. for (auto it = existing_breakpoints.begin(); it != existing_breakpoints.end();) { auto request_pos = request_breakpoints.find(it->first); if (request_pos == request_breakpoints.end()) { // This breakpoint no longer exists in this source file, delete it target.BreakpointDelete(it->second.GetID()); it = existing_breakpoints.erase(it); } else { ++it; } } return response_breakpoints; } void DAP::RegisterRequests() { RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); RegisterRequest(); // Custom requests RegisterRequest(); RegisterRequest(); // Testing requests RegisterRequest(); } } // namespace lldb_dap