diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py index f85ab1910a2eb..540fa205945b1 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py @@ -987,17 +987,23 @@ def request_writeMemory(self, memoryReference, data, offset=0, allowPartial=Fals } return self._send_recv(command_dict) - def request_evaluate(self, expression, frameIndex=0, threadId=None, context=None): + def request_evaluate( + self, + expression: str, + frameIndex=0, + threadId: Optional[int] = None, + context: Optional[ + Literal["watch", "repl", "hover", "clipboard", "variables"] + ] = None, + ) -> Optional[Response]: stackFrame = self.get_stackFrame(frameIndex=frameIndex, threadId=threadId) - if stackFrame is None: - return [] args_dict = { "expression": expression, "frameId": stackFrame["id"], } if context: args_dict["context"] = context - command_dict = { + command_dict: Request = { "command": "evaluate", "type": "request", "arguments": args_dict, diff --git a/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py b/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py index 14789a6694686..c1bb6fad32b6b 100644 --- a/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py +++ b/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py @@ -2,8 +2,6 @@ Test lldb-dap cancel request """ -import time - from lldbsuite.test.decorators import * from lldbsuite.test.lldbtest import * import lldbdap_testcase diff --git a/lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py b/lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py index 3c233a5b43ebb..86c77b8c0a1c9 100644 --- a/lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py +++ b/lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py @@ -125,6 +125,7 @@ def run_test_evaluate_expressions( self.assertEvaluate("var1", "20", want_type="int") # Empty expression should equate to the previous expression. if context == "repl": + self.assertEvaluate("p var1", "20") self.assertEvaluate("", "20") else: self.assertEvaluateFailure("") diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp index d4203a2f00983..03dd17d71124a 100644 --- a/lldb/tools/lldb-dap/DAP.cpp +++ b/lldb/tools/lldb-dap/DAP.cpp @@ -7,6 +7,7 @@ //===----------------------------------------------------------------------===// #include "DAP.h" +#include "CommandPlugins.h" #include "DAPLog.h" #include "EventHelper.h" #include "ExceptionBreakpoint.h" @@ -20,21 +21,23 @@ #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/SBMutex.h" #include "lldb/API/SBProcess.h" #include "lldb/API/SBStream.h" -#include "lldb/Host/JSONTransport.h" +#include "lldb/Host/File.h" #include "lldb/Host/MainLoop.h" #include "lldb/Host/MainLoopBase.h" +#include "lldb/Host/PseudoTerminal.h" #include "lldb/Utility/Status.h" #include "lldb/lldb-defines.h" #include "lldb/lldb-enumerations.h" +#include "lldb/lldb-forward.h" #include "lldb/lldb-types.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/STLExtras.h" @@ -47,7 +50,6 @@ #include "llvm/Support/ErrorHandling.h" #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/raw_ostream.h" -#include #include #include #include @@ -55,11 +57,12 @@ #include #include #include -#include #include #include #include +#include #include +#include #include #include #include @@ -77,14 +80,6 @@ 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, @@ -119,6 +114,95 @@ static std::string capitalize(llvm::StringRef str) { return ((llvm::Twine)llvm::toUpper(str[0]) + str.drop_front()).str(); } +ReplContext::ReplContext(DAP &dap, llvm::StringRef line_ref) + : line_ref(line_ref), dap(dap), + progress(lldb::SBProgress("Evaluating expression", line_ref.str().c_str(), + dap.debugger)) { + assert(dap.repl_context == nullptr); + dap.repl_context = this; + UpdateWantsMore(); +} + +ReplContext::~ReplContext() { + assert(dap.repl_context == this); + dap.repl_context = nullptr; +} + +bool ReplContext::WantsRawInput() { + return !dap.debugger.GetCommandInterpreter().IsActive(); +} + +void ReplContext::UpdateWantsMore() { + if (!WantsRawInput()) + return; + + stop_on_next_write = true; + // If the command interpreter is not active, we're in a raw input. However, + // the input handler may not write any new output after receiving the input + // for example, if the continuation prompt has no `... ` indicator. + // + // ``` + // (lldb) script + // >>> sys.ps1 = '' + // + // ```` + // + // Use a timeout to ensure we don't hang forever if the command entered raw + // input mode. We may exit the loop early if output is produced, but we don't + // want to hang forever. + loop.AddCallback([](auto &loop) { loop.RequestTermination(); }, + kRawInputTimeout); +} + +llvm::Error ReplContext::Run() { + // Ensure the command is terminated. + std::string line = line_ref.str() + "\n"; + + size_t num_bytes = line.size(); + if (llvm::Error err = + dap.pty_primary->Write(line.data(), num_bytes).takeError()) + return err; + + // Unblock the IOThread from handling the input. + dap.GetAPIMutex().unlock(); + llvm::Error err = loop.Run().takeError(); + dap.GetAPIMutex().lock(); + + return err; +} + +void ReplContext::Write(llvm::StringRef str) { + // Skip the line if its the input echo. + if (str == std::string(line_ref) + "\r\n") + return; + + output.append(str); + if (stop_on_next_write) + loop.AddPendingCallback([](auto &loop) { loop.RequestTermination(); }); +} + +void ReplContext::Write(lldb::SBCommandReturnObject &result) { + if (result.GetOutputSize()) + output.append(result.GetOutput()); + if (result.GetErrorSize()) + output.append(result.GetError()); + + if (!result.Succeeded()) + succeeded = false; + + if (result.GetStatus() == lldb::eReturnStatusSuccessFinishResult) { + lldb::SBValueList v = result.GetValues(lldb::eNoDynamicValues); + for (uint32_t i = 0; i < v.GetSize(); ++i) + if (v.GetValueAtIndex(i).IsValid()) + values.Append(v.GetValueAtIndex(i)); + } + + UpdateWantsMore(); + + if (!WantsRawInput()) + loop.AddPendingCallback([](auto &loop) { loop.RequestTermination(); }); +} + llvm::StringRef DAP::debug_adapter_path = ""; DAP::DAP(Log *log, const ReplMode default_repl_mode, @@ -226,15 +310,13 @@ ExceptionBreakpoint *DAP::GetExceptionBreakpoint(const lldb::break_id_t bp_id) { } 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); + SendOutput(eOutputCategoryConsole, output); })) return Error; if (auto Error = err.RedirectTo(overrideErr, [this](llvm::StringRef output) { - SendOutput(OutputType::Console, output); + SendOutput(eOutputCategoryConsole, output); })) return Error; @@ -272,7 +354,7 @@ Id DAP::Send(const Message &message) { Message msg = std::visit( [this](auto &&msg) -> Message { if (msg.seq == kCalculateSeq) - msg.seq = seq++; + msg.seq = ++seq; return msg; }, Message(message)); @@ -315,106 +397,33 @@ Id DAP::Send(const Message &message) { llvm_unreachable("Unexpected message type"); } -// "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) { +void DAP::SendOutput(OutputCategory cat, 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); + protocol::OutputEventBody body; + body.category = cat; 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))); + body.output = output.slice(idx, end + 1).str(); + SendOutput(body); idx = end + 1; } while (idx < output.size()); } +void DAP::SendOutput(const protocol::OutputEventBody &body) { + protocol::Event event{/*event=*/"output", + /*body=*/body}; + if (body.group != protocol::eOutputGroupNone) { + } + Send(event); +} + // interface ProgressStartEvent extends Event { // event: 'progressStart'; // @@ -507,23 +516,11 @@ void DAP::SendOutput(OutputType o, const llvm::StringRef output) { // 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); @@ -604,6 +601,14 @@ ReplMode DAP::DetectReplMode(lldb::SBFrame frame, std::string &expression, return ReplMode::Command; } + // If the command interpreter is not active then its waiting on raw input, + // (e.g. `breakpoint command add`). + if (!debugger.GetCommandInterpreter().IsActive()) + return ReplMode::Command; + + if (expression.empty()) + return ReplMode::Command; + switch (repl_mode) { case ReplMode::Variable: return ReplMode::Variable; @@ -721,7 +726,7 @@ bool DAP::RunLLDBCommands(llvm::StringRef prefix, std::string output = ::RunLLDBCommands( debugger, prefix, commands, required_command_failed, /*parse_command_directives*/ true, /*echo_commands*/ true); - SendOutput(OutputType::Console, output); + SendOutput(eOutputCategoryConsole, output); return !required_command_failed; } @@ -1077,11 +1082,15 @@ llvm::Error DAP::Loop() { auto thread = std::thread(std::bind(&DAP::TransportHandler, this)); auto cleanup = llvm::make_scope_exit([this]() { - // FIXME: Merge these into the MainLoop handler. + DAP_LOG(log, "Cleanup DAP handlers"); out.Stop(); err.Stop(); StopEventHandlers(); + // Close the pty before destroying the debugger to ensure the IOThread + // closes. + pty_primary.reset(); + // Destroy the debugger when the session ends. This will trigger the // debugger's destroy callbacks for earlier logging and clean-ups, rather // than waiting for the termination of the lldb-dap process. @@ -1194,7 +1203,7 @@ void DAP::SetFrameFormat(llvm::StringRef format) { lldb::SBError error; frame_format = lldb::SBFormat(format.str().c_str(), error); if (error.Fail()) { - SendOutput(OutputType::Console, + SendOutput(eOutputCategoryConsole, llvm::formatv( "The provided frame format '{0}' couldn't be parsed: {1}\n", format, error.GetCString()) @@ -1206,7 +1215,7 @@ void DAP::SetThreadFormat(llvm::StringRef format) { lldb::SBError error; thread_format = lldb::SBFormat(format.str().c_str(), error); if (error.Fail()) { - SendOutput(OutputType::Console, + SendOutput(eOutputCategoryConsole, llvm::formatv( "The provided thread format '{0}' couldn't be parsed: {1}\n", format, error.GetCString()) @@ -1306,6 +1315,148 @@ void DAP::StartEventThread() { event_thread = std::thread(&DAP::EventThread, this); } +llvm::Error DAP::CreateDebugger(const llvm::DenseSet &features) { + client_features = features; + // Setup the PTY that used for evaluating user repl commands. + lldb_private::PseudoTerminal pty; + if (auto err = pty.OpenFirstAvailablePrimary(O_RDWR | O_NOCTTY | O_NONBLOCK)) + return err; + + if (auto err = pty.OpenSecondary(O_RDWR | O_NOCTTY)) + return err; + + pty_primary = std::make_shared( + pty.ReleasePrimaryFileDescriptor(), + lldb_private::NativeFile::eOpenOptionReadWrite | + lldb_private::NativeFile::eOpenOptionNonBlocking, + NativeFile::Owned); + + int pty_replica = pty.ReleaseSecondaryFileDescriptor(); + lldb::FileSP in = std::make_shared( + pty_replica, lldb_private::NativeFile::eOpenOptionReadOnly, + NativeFile::Owned); + lldb::FileSP out = std::make_shared( + pty_replica, lldb_private::NativeFile::eOpenOptionWriteOnly, + NativeFile::Unowned); + + lldb_private::Terminal term(pty_primary->GetDescriptor()); + if (llvm::Error err = term.SetCanonical(true)) + return err; + + // Do not source init files until in/out/err are configured. + debugger = lldb::SBDebugger::Create(false); + target = debugger.GetDummyTarget(); + + debugger.SetInputFile(in); + debugger.SetOutputFile(out); + debugger.SetErrorFile(out); + + // Disable the prompt in the Debug Console, otherwise the console has a number + // of prompts that clutter the output and we don't have direct control over + // where they show up in the output. + debugger.SetPrompt(nullptr); + + if (client_features.contains(eClientFeatureProgressReporting)) + StartProgressEventThread(); + + // Start our event thread so we can receive events from the debugger, target, + // process and more. + StartEventThread(); + + Status error; + m_pty_handle = m_loop.RegisterReadObject( + pty_primary, + [this](MainLoopBase &loop) { + lldb::SBMutex lock = GetAPIMutex(); + std::lock_guard guard(lock); + + char buffer[4096] = {0}; + size_t bytes_read = sizeof(buffer); + if (auto err = pty_primary->Read(buffer, bytes_read).takeError()) + DAP_LOG_ERROR(log, std::move(err), "Reading from pty failed {0}"); + if (bytes_read == 0) { // EOF + loop.RequestTermination(); + return; + } + std::string str(buffer, bytes_read); + + if (repl_context) + repl_context->Write(str); + else + SendOutput(eOutputCategoryConsole, str); + }, + error); + if (error.Fail()) + DAP_LOG_ERROR(log, error.takeError(), + "Failed to register pty read handler: {0}"); + + // Register the print callback helper to print to the debug console. + debugger.GetCommandInterpreter().SetPrintCallback( + [](lldb::SBCommandReturnObject &result, void *baton) { + if (!result.GetCommand()) // skip internal commands. + return lldb::eCommandReturnObjectPrintCallbackSkipped; + + DAP *dap = static_cast(baton); + lldb::SBMutex lock = dap->GetAPIMutex(); + std::lock_guard guard(lock); + + if (dap->repl_context) { + dap->repl_context->Write(result); + return lldb::eCommandReturnObjectPrintCallbackHandled; + } + + lldb::SBValueList variables = result.GetValues(lldb::eNoDynamicValues); + + // Check if we have something to output. + if (result.GetOutputSize() == 0 && result.GetErrorSize() == 0 && + !variables) + return lldb::eCommandReturnObjectPrintCallbackHandled; + + protocol::OutputEventBody body; + body.output = "print_callback: "; + body.category = eOutputCategoryConsole; + if (result.GetOutputSize()) + body.output += result.GetOutput(); + if (result.GetErrorSize()) + body.output += result.GetError(); + if (variables && variables.GetSize() > 0) { + if (variables.GetSize() == 1) { + lldb::SBValue v = variables.GetValueAtIndex(0); + if (!is_permanent(v)) + v = v.Persist(); + VariableDescription desc( + v, dap->configuration.enableAutoVariableSummaries); + if (v.MightHaveChildren() || ValuePointsToCode(v)) + body.variablesReference = dap->variables.InsertVariable(v); + } else { + body.variablesReference = dap->variables.InsertVariables(variables); + } + } + + dap->SendOutput(body); + + return lldb::eCommandReturnObjectPrintCallbackHandled; + }, + this); + + auto cmd = debugger.GetCommandInterpreter().AddMultiwordCommand( + "lldb-dap", "Commands for managing lldb-dap."); + if (client_features.contains(eClientFeatureStartDebuggingRequest)) { + cmd.AddCommand( + "start-debugging", new StartDebuggingCommand(*this), + "Sends a startDebugging request from the debug adapter to the client " + "to start a child debug session of the same type as the caller."); + } + + cmd.AddCommand( + "repl-mode", new ReplModeCommand(*this), + "Get or set the repl behavior of lldb-dap evaluation requests."); + cmd.AddCommand("send-event", new SendEventCommand(*this), + "Sends an DAP event to the client."); + + return llvm::Error::success(); +} + void DAP::StartProgressEventThread() { progress_event_thread = std::thread(&DAP::ProgressEventThread, this); } @@ -1338,9 +1489,9 @@ void DAP::ProgressEventThread() { 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 + // 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 = @@ -1349,9 +1500,9 @@ void DAP::ProgressEventThread() { } 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. + // 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); @@ -1359,10 +1510,10 @@ void DAP::ProgressEventThread() { 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. + // 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); } } @@ -1446,7 +1597,7 @@ void DAP::HandleProcessEvent(const lldb::SBEvent &event, bool &process_exited) { case lldb::eStateExited: lldb::SBStream stream; process.GetStatus(stream); - SendOutput(OutputType::Console, stream.GetData()); + SendOutput(eOutputCategoryConsole, 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 @@ -1573,7 +1724,7 @@ void DAP::HandleDiagnosticEvent(const lldb::SBEvent &event) { std::string type = GetStringValue(data.GetValueForKey("type")); std::string message = GetStringValue(data.GetValueForKey("message")); - SendOutput(OutputType::Important, + SendOutput(eOutputCategoryImportant, llvm::formatv("{0}: {1}", type, message).str()); } diff --git a/lldb/tools/lldb-dap/DAP.h b/lldb/tools/lldb-dap/DAP.h index 5d40341329f34..aa4c5b7ccef69 100644 --- a/lldb/tools/lldb-dap/DAP.h +++ b/lldb/tools/lldb-dap/DAP.h @@ -16,27 +16,26 @@ #include "OutputRedirector.h" #include "ProgressEvent.h" #include "Protocol/ProtocolBase.h" +#include "Protocol/ProtocolEvents.h" #include "Protocol/ProtocolRequests.h" #include "Protocol/ProtocolTypes.h" #include "SourceBreakpoint.h" #include "Transport.h" #include "Variables.h" #include "lldb/API/SBBroadcaster.h" -#include "lldb/API/SBCommandInterpreter.h" +#include "lldb/API/SBCommandReturnObject.h" #include "lldb/API/SBDebugger.h" #include "lldb/API/SBError.h" -#include "lldb/API/SBFile.h" #include "lldb/API/SBFormat.h" #include "lldb/API/SBFrame.h" #include "lldb/API/SBMutex.h" +#include "lldb/API/SBProgress.h" #include "lldb/API/SBTarget.h" #include "lldb/API/SBThread.h" #include "lldb/Host/MainLoop.h" -#include "lldb/Utility/Status.h" #include "lldb/lldb-types.h" #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/DenseSet.h" -#include "llvm/ADT/FunctionExtras.h" #include "llvm/ADT/SmallSet.h" #include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringRef.h" @@ -44,12 +43,15 @@ #include "llvm/Support/Error.h" #include "llvm/Support/JSON.h" #include "llvm/Support/Threading.h" +#include #include #include #include +#include #include #include #include +#include #include #include @@ -65,8 +67,7 @@ typedef llvm::DenseMap using AdapterFeature = protocol::AdapterFeature; using ClientFeature = protocol::ClientFeature; - -enum class OutputType { Console, Important, Stdout, Stderr, Telemetry }; +using OutputCategory = protocol::OutputCategory; /// Buffer size for handling output events. constexpr uint64_t OutputBufferSize = (1u << 12); @@ -77,6 +78,34 @@ enum DAPBroadcasterBits { }; enum class ReplMode { Variable = 0, Command, Auto }; +struct ReplContext { + llvm::StringRef line_ref; + bool succeeded = true; + bool did_timeout = false; + bool stop_on_next_write = false; + static constexpr std::chrono::milliseconds kRawInputTimeout = + std::chrono::milliseconds(250); + DAP &dap; + lldb_private::MainLoop loop; + llvm::SmallString<256> output; + lldb::SBValueList values; + + // FIXME: It would be nice to be able to cancel this operation, at the + // moment we do not support progress cancellation. + lldb::SBProgress progress; + + explicit ReplContext(DAP &dap, llvm::StringRef line); + ~ReplContext(); + + bool WantsRawInput(); + + void UpdateWantsMore(); + + void Write(llvm::StringRef str); + void Write(lldb::SBCommandReturnObject &result); + + llvm::Error Run(); +}; using DAPTransport = lldb_private::transport::JSONTransport; @@ -86,7 +115,7 @@ struct DAP final : public DAPTransport::MessageHandler { Log *log; DAPTransport &transport; - lldb::SBFile in; + lldb::FileSP pty_primary; OutputRedirector out; OutputRedirector err; @@ -141,6 +170,7 @@ struct DAP final : public DAPTransport::MessageHandler { llvm::SmallDenseMap> inflight_reverse_requests; ReplMode repl_mode; + ReplContext *repl_context = nullptr; lldb::SBFormat frame_format; lldb::SBFormat thread_format; @@ -152,7 +182,7 @@ struct DAP final : public DAPTransport::MessageHandler { std::string last_nonempty_var_expression; /// The set of features supported by the connected client. - llvm::DenseSet clientFeatures; + llvm::DenseSet client_features; /// Whether to disable sourcing .lldbinit files. bool no_lldbinit; @@ -223,14 +253,13 @@ struct DAP final : public DAPTransport::MessageHandler { /// Send the given message to the client. protocol::Id Send(const protocol::Message &message); - void SendOutput(OutputType o, const llvm::StringRef output); + /// Send output to the client. + void SendOutput(OutputCategory o, const llvm::StringRef output); + void SendOutput(const protocol::OutputEventBody &); void SendProgressEvent(uint64_t progress_id, const char *message, uint64_t completed, uint64_t total); - void __attribute__((format(printf, 3, 4))) - SendFormattedOutput(OutputType o, const char *format, ...); - int32_t CreateSourceReference(lldb::addr_t address); std::optional GetSourceReferenceAddress(int32_t reference); @@ -410,6 +439,7 @@ struct DAP final : public DAPTransport::MessageHandler { void StartEventThread(); void StartProgressEventThread(); + llvm::Error CreateDebugger(const llvm::DenseSet &); /// Sets the given protocol `breakpoints` in the given `source`, while /// removing any existing breakpoints in the given source if they are not in @@ -480,6 +510,7 @@ struct DAP final : public DAPTransport::MessageHandler { // Loop for managing reading from the client. lldb_private::MainLoop &m_loop; + lldb_private::MainLoop::ReadHandleUP m_pty_handle; std::mutex m_cancelled_requests_mutex; llvm::SmallSet m_cancelled_requests; diff --git a/lldb/tools/lldb-dap/DAPError.cpp b/lldb/tools/lldb-dap/DAPError.cpp index 5c5bae37cc600..1fd031e78b965 100644 --- a/lldb/tools/lldb-dap/DAPError.cpp +++ b/lldb/tools/lldb-dap/DAPError.cpp @@ -15,9 +15,19 @@ namespace lldb_dap { char DAPError::ID; +DAPError::DAPError(std::string message) : DAPError(std::move(message), true) {} + +DAPError::DAPError(std::string message, std::error_code EC) + : DAPError(std::move(message), std::move(EC), true) {} + +DAPError::DAPError(std::string message, bool show_user) + : DAPError(std::move(message), llvm::inconvertibleErrorCode(), show_user) {} + +DAPError::DAPError(std::string message, std::error_code EC, bool show_user) + : DAPError(std::move(message), std::move(EC), show_user, "", "") {} + DAPError::DAPError(std::string message, std::error_code EC, bool show_user, - std::optional url, - std::optional url_label) + std::string url, std::string url_label) : m_message(std::move(message)), m_ec(EC), m_show_user(show_user), m_url(std::move(url)), m_url_label(std::move(url_label)) {} diff --git a/lldb/tools/lldb-dap/DAPError.h b/lldb/tools/lldb-dap/DAPError.h index 26b1daae59340..d866c26ee0249 100644 --- a/lldb/tools/lldb-dap/DAPError.h +++ b/lldb/tools/lldb-dap/DAPError.h @@ -22,25 +22,31 @@ class DAPError : public llvm::ErrorInfo { public: static char ID; - DAPError(std::string message, - std::error_code EC = llvm::inconvertibleErrorCode(), - bool show_user = true, std::optional url = std::nullopt, - std::optional url_label = std::nullopt); + explicit DAPError(std::string message); + + DAPError(std::string message, std::error_code EC); + + DAPError(std::string message, bool show_user); + + DAPError(std::string message, std::error_code EC, bool show_user); + + DAPError(std::string message, std::error_code EC, bool show_user, + std::string url, std::string url_label); void log(llvm::raw_ostream &OS) const override; std::error_code convertToErrorCode() const override; const std::string &getMessage() const { return m_message; } bool getShowUser() const { return m_show_user; } - const std::optional &getURL() const { return m_url; } - const std::optional &getURLLabel() const { return m_url_label; } + const std::string &getURL() const { return m_url; } + const std::string &getURLLabel() const { return m_url_label; } private: std::string m_message; std::error_code m_ec; bool m_show_user; - std::optional m_url; - std::optional m_url_label; + std::string m_url; + std::string m_url_label; }; /// An error that indicates the current request handler cannot execute because diff --git a/lldb/tools/lldb-dap/EventHelper.cpp b/lldb/tools/lldb-dap/EventHelper.cpp index 12d9e21c52ab3..aa71332b854b7 100644 --- a/lldb/tools/lldb-dap/EventHelper.cpp +++ b/lldb/tools/lldb-dap/EventHelper.cpp @@ -243,12 +243,14 @@ 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]; + char buffer[OutputBufferSize] = {0}; size_t count; while ((count = process.GetSTDOUT(buffer, sizeof(buffer))) > 0) - dap.SendOutput(OutputType::Stdout, llvm::StringRef(buffer, count)); + dap.SendOutput(protocol::eOutputCategoryStderr, + llvm::StringRef(buffer, count)); while ((count = process.GetSTDERR(buffer, sizeof(buffer))) > 0) - dap.SendOutput(OutputType::Stderr, llvm::StringRef(buffer, count)); + dap.SendOutput(protocol::eOutputCategoryStderr, + llvm::StringRef(buffer, count)); } // Send a "continued" event to indicate the process is in the running state. @@ -284,7 +286,7 @@ void SendProcessExitedEvent(DAP &dap, lldb::SBProcess &process) { void SendInvalidatedEvent( DAP &dap, llvm::ArrayRef areas, lldb::tid_t tid) { - if (!dap.clientFeatures.contains(protocol::eClientFeatureInvalidatedEvent)) + if (!dap.client_features.contains(protocol::eClientFeatureInvalidatedEvent)) return; protocol::InvalidatedEventBody body; body.areas = areas; @@ -296,7 +298,7 @@ void SendInvalidatedEvent( } void SendMemoryEvent(DAP &dap, lldb::SBValue variable) { - if (!dap.clientFeatures.contains(protocol::eClientFeatureMemoryEvent)) + if (!dap.client_features.contains(protocol::eClientFeatureMemoryEvent)) return; protocol::MemoryEventBody body; body.memoryReference = variable.GetLoadAddress(); diff --git a/lldb/tools/lldb-dap/Handler/AttachRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/AttachRequestHandler.cpp index 490513fe8a0b8..5daa0928c4240 100644 --- a/lldb/tools/lldb-dap/Handler/AttachRequestHandler.cpp +++ b/lldb/tools/lldb-dap/Handler/AttachRequestHandler.cpp @@ -10,6 +10,7 @@ #include "EventHelper.h" #include "JSONUtils.h" #include "LLDBUtils.h" +#include "Protocol/ProtocolEvents.h" #include "Protocol/ProtocolRequests.h" #include "RequestHandler.h" #include "lldb/API/SBAttachInfo.h" @@ -77,7 +78,7 @@ Error AttachRequestHandler::Run(const AttachRequestArguments &args) const { if ((args.pid == LLDB_INVALID_PROCESS_ID || args.gdbRemotePort == LLDB_DAP_INVALID_PORT) && args.waitFor) { - dap.SendOutput(OutputType::Console, + dap.SendOutput(eOutputCategoryConsole, llvm::formatv("Waiting to attach to \"{0}\"...", dap.target.GetExecutable().GetFilename()) .str()); diff --git a/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp b/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp index de9a15dcb73f4..c8a221b4284de 100644 --- a/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp +++ b/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp @@ -7,10 +7,11 @@ //===----------------------------------------------------------------------===// #include "DAP.h" -#include "JSONUtils.h" #include "Protocol/ProtocolRequests.h" #include "Protocol/ProtocolTypes.h" #include "RequestHandler.h" +#include "lldb/API/SBCommandInterpreter.h" +#include "lldb/API/SBDebugger.h" #include "lldb/API/SBStringList.h" using namespace llvm; diff --git a/lldb/tools/lldb-dap/Handler/ConfigurationDoneRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/ConfigurationDoneRequestHandler.cpp index 1bfe7b7f6ef5c..e6e2b55bb9d2d 100644 --- a/lldb/tools/lldb-dap/Handler/ConfigurationDoneRequestHandler.cpp +++ b/lldb/tools/lldb-dap/Handler/ConfigurationDoneRequestHandler.cpp @@ -13,6 +13,7 @@ #include "Protocol/ProtocolRequests.h" #include "ProtocolUtils.h" #include "RequestHandler.h" +#include "lldb/API/SBCommandInterpreterRunOptions.h" #include "lldb/API/SBDebugger.h" using namespace llvm; @@ -42,14 +43,16 @@ ConfigurationDoneRequestHandler::Run(const ConfigurationDoneArguments &) const { "any debugger command scripts are not resuming the process during the " "launch sequence."); - // Waiting until 'configurationDone' to send target based capabilities in case - // the launch or attach scripts adjust the target. The initial dummy target - // may have different capabilities than the final target. - - /// Also send here custom capabilities to the client, which is consumed by the - /// lldb-dap specific editor extension. + // Send custom capabilities to the client, which is consumed by the lldb-dap + // specific editor extension. SendExtraCapabilities(dap); + PrintIntroductionMessage(); + + // Spawn the IOHandler thread. + dap.debugger.RunCommandInterpreter(/*auto_handle_events=*/false, + /*spawn_thread=*/true); + // Clients can request a baseline of currently existing threads after // we acknowledge the configurationDone request. // Client requests the baseline of currently existing threads after diff --git a/lldb/tools/lldb-dap/Handler/EvaluateRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/EvaluateRequestHandler.cpp index ea8c3a2a4a296..d78ba1e919c99 100644 --- a/lldb/tools/lldb-dap/Handler/EvaluateRequestHandler.cpp +++ b/lldb/tools/lldb-dap/Handler/EvaluateRequestHandler.cpp @@ -10,12 +10,18 @@ #include "EventHelper.h" #include "JSONUtils.h" #include "LLDBUtils.h" +#include "Protocol/ProtocolEvents.h" #include "Protocol/ProtocolRequests.h" #include "Protocol/ProtocolTypes.h" #include "RequestHandler.h" +#include "Variables.h" +#include "lldb/API/SBValue.h" +#include "lldb/Host/File.h" +#include "lldb/Utility/Status.h" #include "lldb/lldb-enumerations.h" -#include "llvm/ADT/StringRef.h" #include "llvm/Support/Error.h" +#include +#include using namespace llvm; using namespace lldb_dap; @@ -31,38 +37,44 @@ EvaluateRequestHandler::Run(const EvaluateArguments &arguments) const { EvaluateResponseBody body; lldb::SBFrame frame = dap.GetLLDBFrame(arguments.frameId); std::string expression = arguments.expression; - bool repeat_last_command = - expression.empty() && dap.last_nonempty_var_expression.empty(); - - if (arguments.context == protocol::eEvaluateContextRepl && - (repeat_last_command || - (!expression.empty() && - dap.DetectReplMode(frame, expression, false) == ReplMode::Command))) { - // Since the current expression is not for a variable, clear the - // last_nonempty_var_expression field. - dap.last_nonempty_var_expression.clear(); + + if (arguments.context == eEvaluateContextRepl && + dap.DetectReplMode(frame, expression, false) == ReplMode::Command) { // If we're evaluating a command relative to the current frame, set the // focus_tid to the current frame for any thread related events. if (frame.IsValid()) { dap.focus_tid = frame.GetThread().GetThreadID(); } - bool required_command_failed = false; - body.result = RunLLDBCommands( - dap.debugger, llvm::StringRef(), {expression}, required_command_failed, - /*parse_command_directives=*/false, /*echo_commands=*/false); - return body; - } + for (const auto &line_ref : llvm::split(expression, "\n")) { + ReplContext context{dap, line_ref}; + if (llvm::Error err = context.Run()) + return err; + + if (!context.succeeded) + return llvm::make_error(std::string(context.output), + /*show_user=*/false); + + body.result += std::string(context.output); + + if (context.values && context.values.GetSize()) { + if (context.values.GetSize() == 1) { + lldb::SBValue v = context.values.GetValueAtIndex(0); + if (!is_permanent(v)) + v = v.Persist(); + VariableDescription desc( + v, dap.configuration.enableAutoVariableSummaries); + body.type = desc.display_type_name; + if (v.MightHaveChildren() || ValuePointsToCode(v)) + body.variablesReference = dap.variables.InsertVariable(v); + } else { + body.variablesReference = + dap.variables.InsertVariables(context.values); + } + } + } - if (arguments.context == eEvaluateContextRepl) { - // If the expression is empty and the last expression was for a - // variable, set the expression to the previous expression (repeat the - // evaluation); otherwise save the current non-empty expression for the - // next (possibly empty) variable expression. - if (expression.empty()) - expression = dap.last_nonempty_var_expression; - else - dap.last_nonempty_var_expression = expression; + return body; } // Always try to get the answer from the local variables if possible. If @@ -86,14 +98,10 @@ EvaluateRequestHandler::Run(const EvaluateArguments &arguments) const { VariableDescription desc(value, dap.configuration.enableAutoVariableSummaries); - body.result = desc.GetResult(arguments.context); body.type = desc.display_type_name; - if (value.MightHaveChildren() || ValuePointsToCode(value)) - body.variablesReference = dap.variables.InsertVariable( - value, /*is_permanent=*/arguments.context == eEvaluateContextRepl); - + body.variablesReference = dap.variables.InsertVariable(value); if (lldb::addr_t addr = value.GetLoadAddress(); addr != LLDB_INVALID_ADDRESS) body.memoryReference = EncodeMemoryReference(addr); diff --git a/lldb/tools/lldb-dap/Handler/InitializeRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/InitializeRequestHandler.cpp index 9069de4a3a690..cf46ed254f15a 100644 --- a/lldb/tools/lldb-dap/Handler/InitializeRequestHandler.cpp +++ b/lldb/tools/lldb-dap/Handler/InitializeRequestHandler.cpp @@ -6,14 +6,12 @@ // //===----------------------------------------------------------------------===// -#include "CommandPlugins.h" #include "DAP.h" #include "EventHelper.h" -#include "JSONUtils.h" -#include "LLDBUtils.h" #include "Protocol/ProtocolRequests.h" #include "RequestHandler.h" -#include "lldb/API/SBTarget.h" +#include "lldb/API/SBCommandInterpreter.h" +#include "lldb/API/SBCommandReturnObject.h" using namespace lldb_dap; using namespace lldb_dap::protocol; @@ -21,22 +19,8 @@ using namespace lldb_dap::protocol; /// Initialize request; value of command field is 'initialize'. llvm::Expected InitializeRequestHandler::Run( const InitializeRequestArguments &arguments) const { - dap.clientFeatures = arguments.supportedFeatures; - - // Do not source init files until in/out/err are configured. - dap.debugger = lldb::SBDebugger::Create(false); - dap.debugger.SetInputFile(dap.in); - dap.target = dap.debugger.GetDummyTarget(); - - llvm::Expected out_fd = dap.out.GetWriteFileDescriptor(); - if (!out_fd) - return out_fd.takeError(); - dap.debugger.SetOutputFile(lldb::SBFile(*out_fd, "w", false)); - - llvm::Expected err_fd = dap.err.GetWriteFileDescriptor(); - if (!err_fd) - return err_fd.takeError(); - dap.debugger.SetErrorFile(lldb::SBFile(*err_fd, "w", false)); + if (auto err = dap.CreateDebugger(arguments.supportedFeatures)) + return err; auto interp = dap.debugger.GetCommandInterpreter(); @@ -57,27 +41,5 @@ llvm::Expected InitializeRequestHandler::Run( if (llvm::Error err = dap.RunPreInitCommands()) return err; - auto cmd = dap.debugger.GetCommandInterpreter().AddMultiwordCommand( - "lldb-dap", "Commands for managing lldb-dap."); - if (arguments.supportedFeatures.contains( - eClientFeatureStartDebuggingRequest)) { - cmd.AddCommand( - "start-debugging", new StartDebuggingCommand(dap), - "Sends a startDebugging request from the debug adapter to the client " - "to start a child debug session of the same type as the caller."); - } - cmd.AddCommand( - "repl-mode", new ReplModeCommand(dap), - "Get or set the repl behavior of lldb-dap evaluation requests."); - cmd.AddCommand("send-event", new SendEventCommand(dap), - "Sends an DAP event to the client."); - - if (arguments.supportedFeatures.contains(eClientFeatureProgressReporting)) - dap.StartProgressEventThread(); - - // Start our event thread so we can receive events from the debugger, target, - // process and more. - dap.StartEventThread(); - return dap.GetCapabilities(); } diff --git a/lldb/tools/lldb-dap/Handler/RequestHandler.cpp b/lldb/tools/lldb-dap/Handler/RequestHandler.cpp index d67437ad5b3ae..0ce4b00104662 100644 --- a/lldb/tools/lldb-dap/Handler/RequestHandler.cpp +++ b/lldb/tools/lldb-dap/Handler/RequestHandler.cpp @@ -13,10 +13,13 @@ #include "JSONUtils.h" #include "LLDBUtils.h" #include "Protocol/ProtocolBase.h" +#include "Protocol/ProtocolEvents.h" #include "Protocol/ProtocolRequests.h" #include "RunInTerminal.h" #include "lldb/API/SBDefines.h" #include "lldb/API/SBEnvironment.h" +#include "lldb/API/SBStream.h" +#include "lldb/lldb-types.h" #include "llvm/Support/Error.h" #include @@ -80,7 +83,7 @@ SetupIORedirection(const std::vector> &stdio, static llvm::Error RunInTerminal(DAP &dap, const protocol::LaunchRequestArguments &arguments) { - if (!dap.clientFeatures.contains( + if (!dap.client_features.contains( protocol::eClientFeatureRunInTerminalRequest)) return llvm::make_error("Cannot use runInTerminal, feature is " "not supported by the connected client"); @@ -261,6 +264,26 @@ void BaseRequestHandler::PrintWelcomeMessage() const { #endif } +void BaseRequestHandler::PrintIntroductionMessage() const { + lldb::SBStream msg; + msg.Print("To get started with the lldb-dap debug console try " + "\"\", \"help []\", or \"apropos " + "\".\r\nFor more information visit " + "https://github.com/llvm/llvm-project/blob/main/lldb/tools/" + "lldb-dap/README.md\r\n"); + if (dap.target.GetProcess()) { + msg.Printf("Attached to process %llu.\r\n", + dap.target.GetProcess().GetProcessID()); + } + if (dap.target && dap.target.GetExecutable()) { + char path[PATH_MAX] = {0}; + dap.target.GetExecutable().GetPath(path, sizeof(path)); + msg.Printf("Executable binary set to '%s' (%s).\r\n", path, + dap.target.GetTriple()); + } + dap.SendOutput(eOutputCategoryConsole, {msg.GetData(), msg.GetSize()}); +} + bool BaseRequestHandler::HasInstructionGranularity( const llvm::json::Object &arguments) const { if (std::optional value = arguments.getString("granularity")) diff --git a/lldb/tools/lldb-dap/Handler/RequestHandler.h b/lldb/tools/lldb-dap/Handler/RequestHandler.h index 65a52075ebd79..c9918d24c4ab4 100644 --- a/lldb/tools/lldb-dap/Handler/RequestHandler.h +++ b/lldb/tools/lldb-dap/Handler/RequestHandler.h @@ -64,6 +64,10 @@ class BaseRequestHandler { /// LLDB_DAP_WELCOME_MESSAGE is defined. void PrintWelcomeMessage() const; + /// Prints an introduction to the debug console and information about the + /// debug session. + void PrintIntroductionMessage() const; + // Takes a LaunchRequest object and launches the process, also handling // runInTerminal if applicable. It doesn't do any of the additional // initialization and bookkeeping stuff that is needed for `request_launch`. @@ -187,26 +191,31 @@ class RequestHandler : public BaseRequestHandler { response.message = lldb_dap::protocol::eResponseMessageNotStopped; }, [&](const DAPError &err) { - protocol::ErrorMessage error_message; - error_message.sendTelemetry = false; - error_message.format = err.getMessage(); - error_message.showUser = err.getShowUser(); - error_message.id = err.convertToErrorCode().value(); - error_message.url = err.getURL(); - error_message.urlLabel = err.getURLLabel(); - protocol::ErrorResponseBody body; - body.error = error_message; - response.body = body; + // If we don't need to show the user, then we can simply return a + // message instead. + if (err.getShowUser()) { + protocol::ErrorMessage error_message; + error_message.format = err.getMessage(); + error_message.showUser = err.getShowUser(); + error_message.id = err.convertToErrorCode().value(); + error_message.url = err.getURL(); + error_message.urlLabel = err.getURLLabel(); + protocol::ErrorResponseBody body; + body.error = error_message; + response.body = body; + } else { + response.message = err.getMessage(); + } }, [&](const llvm::ErrorInfoBase &err) { protocol::ErrorMessage error_message; error_message.showUser = true; - error_message.sendTelemetry = false; error_message.format = err.message(); error_message.id = err.convertToErrorCode().value(); protocol::ErrorResponseBody body; body.error = error_message; response.body = body; + response.message = err.message(); }); } }; diff --git a/lldb/tools/lldb-dap/Handler/SetVariableRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/SetVariableRequestHandler.cpp index b9ae28d6772ac..b942b023e4822 100644 --- a/lldb/tools/lldb-dap/Handler/SetVariableRequestHandler.cpp +++ b/lldb/tools/lldb-dap/Handler/SetVariableRequestHandler.cpp @@ -61,8 +61,7 @@ SetVariableRequestHandler::Run(const SetVariableArguments &args) const { // so always insert a new one to get its variablesReference. // is_permanent is false because debug console does not support // setVariable request. - const int64_t new_var_ref = - dap.variables.InsertVariable(variable, /*is_permanent=*/false); + const int64_t new_var_ref = dap.variables.InsertVariable(variable); if (variable.MightHaveChildren()) { body.variablesReference = new_var_ref; if (desc.type_obj.IsArrayType()) diff --git a/lldb/tools/lldb-dap/Handler/VariablesRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/VariablesRequestHandler.cpp index 5fa2b1ef5e20d..4a6fee81fa7db 100644 --- a/lldb/tools/lldb-dap/Handler/VariablesRequestHandler.cpp +++ b/lldb/tools/lldb-dap/Handler/VariablesRequestHandler.cpp @@ -106,8 +106,7 @@ VariablesRequestHandler::Run(const VariablesArguments &arguments) const { if (stop_return_value.MightHaveChildren() || stop_return_value.IsSynthetic()) { - return_var_ref = dap.variables.InsertVariable(stop_return_value, - /*is_permanent=*/false); + return_var_ref = dap.variables.InsertVariable(stop_return_value); } variables.emplace_back(CreateVariable( renamed_return_value, return_var_ref, hex, @@ -123,8 +122,7 @@ VariablesRequestHandler::Run(const VariablesArguments &arguments) const { if (!variable.IsValid()) break; - const int64_t frame_var_ref = - dap.variables.InsertVariable(variable, /*is_permanent=*/false); + const int64_t frame_var_ref = dap.variables.InsertVariable(variable); variables.emplace_back(CreateVariable( variable, frame_var_ref, hex, dap.configuration.enableAutoVariableSummaries, @@ -135,35 +133,36 @@ VariablesRequestHandler::Run(const VariablesArguments &arguments) const { // We are expanding a variable that has children, so we will return its // children. lldb::SBValue variable = dap.variables.GetVariable(var_ref); - if (variable.IsValid()) { - const bool is_permanent = - dap.variables.IsPermanentVariableReference(var_ref); - auto addChild = [&](lldb::SBValue child, - std::optional custom_name = {}) { - if (!child.IsValid()) - return; - const int64_t child_var_ref = - dap.variables.InsertVariable(child, is_permanent); - variables.emplace_back( - CreateVariable(child, child_var_ref, hex, - dap.configuration.enableAutoVariableSummaries, - dap.configuration.enableSyntheticChildDebugging, - /*is_name_duplicated=*/false, custom_name)); - }; - const int64_t num_children = variable.GetNumChildren(); - const int64_t end_idx = start + ((count == 0) ? num_children : count); - int64_t i = start; - for (; i < end_idx && i < num_children; ++i) - addChild(variable.GetChildAtIndex(i)); - - // If we haven't filled the count quota from the request, we insert a new - // "[raw]" child that can be used to inspect the raw version of a - // synthetic member. That eliminates the need for the user to go to the - // debug console and type `frame var to get these values. - if (dap.configuration.enableSyntheticChildDebugging && - variable.IsSynthetic() && i == num_children) - addChild(variable.GetNonSyntheticValue(), "[raw]"); + if (!variable.IsValid()) { + return llvm::make_error(llvm::formatv("").str(), + llvm::inconvertibleErrorCode(), + /*show_user=*/false); } + + auto addChild = [&](lldb::SBValue child, + std::optional custom_name = {}) { + if (!child.IsValid()) + return; + const int64_t child_var_ref = dap.variables.InsertVariable(child); + variables.emplace_back( + CreateVariable(child, child_var_ref, hex, + dap.configuration.enableAutoVariableSummaries, + dap.configuration.enableSyntheticChildDebugging, + /*is_name_duplicated=*/false, custom_name)); + }; + const int64_t num_children = variable.GetNumChildren(); + const int64_t end_idx = start + ((count == 0) ? num_children : count); + int64_t i = start; + for (; i < end_idx && i < num_children; ++i) + addChild(variable.GetChildAtIndex(i)); + + // If we haven't filled the count quota from the request, we insert a new + // "[raw]" child that can be used to inspect the raw version of a + // synthetic member. That eliminates the need for the user to go to the + // debug console and type `frame var to get these values. + if (dap.configuration.enableSyntheticChildDebugging && + variable.IsSynthetic() && i == num_children) + addChild(variable.GetNonSyntheticValue(), "[raw]"); } return VariablesResponseBody{variables}; diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolBase.cpp b/lldb/tools/lldb-dap/Protocol/ProtocolBase.cpp index 72359214c8537..0fbd9546abfae 100644 --- a/lldb/tools/lldb-dap/Protocol/ProtocolBase.cpp +++ b/lldb/tools/lldb-dap/Protocol/ProtocolBase.cpp @@ -188,9 +188,9 @@ bool operator==(const Response &a, const Response &b) { json::Value toJSON(const ErrorMessage &EM) { json::Object Result{{"id", EM.id}, {"format", EM.format}}; - if (EM.variables) { + if (!EM.variables.empty()) { json::Object variables; - for (auto &var : *EM.variables) + for (auto &var : EM.variables) variables[var.first] = var.second; Result.insert({"variables", std::move(variables)}); } @@ -198,9 +198,9 @@ json::Value toJSON(const ErrorMessage &EM) { Result.insert({"sendTelemetry", EM.sendTelemetry}); if (EM.showUser) Result.insert({"showUser", EM.showUser}); - if (EM.url) + if (!EM.url.empty()) Result.insert({"url", EM.url}); - if (EM.urlLabel) + if (!EM.urlLabel.empty()) Result.insert({"urlLabel", EM.urlLabel}); return std::move(Result); @@ -209,10 +209,10 @@ json::Value toJSON(const ErrorMessage &EM) { bool fromJSON(json::Value const &Params, ErrorMessage &EM, json::Path P) { json::ObjectMapper O(Params, P); return O && O.map("id", EM.id) && O.map("format", EM.format) && - O.map("variables", EM.variables) && - O.map("sendTelemetry", EM.sendTelemetry) && - O.map("showUser", EM.showUser) && O.map("url", EM.url) && - O.map("urlLabel", EM.urlLabel); + O.mapOptional("variables", EM.variables) && + O.mapOptional("sendTelemetry", EM.sendTelemetry) && + O.mapOptional("showUser", EM.showUser) && + O.mapOptional("url", EM.url) && O.mapOptional("urlLabel", EM.urlLabel); } json::Value toJSON(const Event &E) { diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolBase.h b/lldb/tools/lldb-dap/Protocol/ProtocolBase.h index 42c6c8890af24..30b55feda1ee4 100644 --- a/lldb/tools/lldb-dap/Protocol/ProtocolBase.h +++ b/lldb/tools/lldb-dap/Protocol/ProtocolBase.h @@ -148,7 +148,7 @@ struct ErrorMessage { /// An object used as a dictionary for looking up the variables in the format /// string. - std::optional> variables; + std::map variables; /// If true send to telemetry. bool sendTelemetry = false; @@ -157,10 +157,10 @@ struct ErrorMessage { bool showUser = false; /// A url where additional information about this message can be found. - std::optional url; + std::string url; /// A label that is presented to the user as the UI for opening the url. - std::optional urlLabel; + std::string urlLabel; }; bool fromJSON(const llvm::json::Value &, ErrorMessage &, llvm::json::Path); llvm::json::Value toJSON(const ErrorMessage &); diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp index df6be06637a13..6656b01f12164 100644 --- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp +++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp @@ -64,4 +64,55 @@ llvm::json::Value toJSON(const MemoryEventBody &MEB) { {"count", MEB.count}}; } +static llvm::json::Value toJSON(const OutputCategory &OC) { + switch (OC) { + case eOutputCategoryConsole: + return "console"; + case eOutputCategoryImportant: + return "important"; + case eOutputCategoryStdout: + return "stdout"; + case eOutputCategoryStderr: + return "stderr"; + case eOutputCategoryTelemetry: + return "telemetry"; + } + llvm_unreachable("unhandled output category!."); +} + +static llvm::json::Value toJSON(const OutputGroup &OG) { + switch (OG) { + case eOutputGroupStart: + return "start"; + case eOutputGroupStartCollapsed: + return "startCollapsed"; + case eOutputGroupEnd: + return "end"; + case eOutputGroupNone: + break; + } + llvm_unreachable("unhandled output category!."); +} + +llvm::json::Value toJSON(const OutputEventBody &OEB) { + json::Object Result{{"output", OEB.output}, {"category", OEB.category}}; + + if (OEB.group != eOutputGroupNone) + Result.insert({"group", OEB.group}); + if (OEB.variablesReference) + Result.insert({"variablesReference", OEB.variablesReference}); + if (OEB.source) + Result.insert({"source", OEB.source}); + if (OEB.line != LLDB_INVALID_LINE_NUMBER) + Result.insert({"line", OEB.line}); + if (OEB.column != LLDB_INVALID_COLUMN_NUMBER) + Result.insert({"column", OEB.column}); + if (OEB.data) + Result.insert({"data", OEB.data}); + if (OEB.locationReference) + Result.insert({"locationReference", OEB.locationReference}); + + return Result; +} + } // namespace lldb_dap::protocol diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h index 5cd5a843d284e..bf1857d5c7f08 100644 --- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h +++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h @@ -117,6 +117,99 @@ struct MemoryEventBody { }; llvm::json::Value toJSON(const MemoryEventBody &); +/// The output category. +enum OutputCategory : unsigned { + /// Show the output in the client's default message UI, e.g. a 'debug + /// console'. This category should only be used for informational output from + /// the debugger (as opposed to the debuggee). + eOutputCategoryConsole, + /// A hint for the client to show the output in the client's UI for important + /// and highly visible information, e.g. as a popup notification. This + /// category should only be used for important messages from the debugger (as + /// opposed to the debuggee). Since this category value is a hint, clients + /// might ignore the hint and assume the `console` category. + eOutputCategoryImportant, + /// Show the output as normal program output from the debuggee. + eOutputCategoryStdout, + /// Show the output as error program output from the debuggee. + eOutputCategoryStderr, + /// Send the output to telemetry instead of showing it to the user + eOutputCategoryTelemetry, +}; + +enum OutputGroup : unsigned { + /// No grouping of output. + eOutputGroupNone, + /// Start a new group in expanded mode. Subsequent output events are members + /// of the group and should be shown indented. + /// The `output` attribute becomes the name of the group and is not indented. + eOutputGroupStart, + /// Start a new group in collapsed mode. Subsequent output events are members + /// of the group and should be shown indented (as soon as the group is + /// expanded). + /// The `output` attribute becomes the name of the group and is not indented. + eOutputGroupStartCollapsed, + /// End the current group and decrease the indentation of subsequent output + /// events. + /// A non-empty `output` attribute is shown as the unindented end of the + /// group. + eOutputGroupEnd, +}; + +/// The event indicates that the target has produced some output. +struct OutputEventBody { + /// The output category. If not specified or if the category is not understood + /// by the client, `console` is assumed. + OutputCategory category = eOutputCategoryConsole; + + /// The output to report. + /// + /// ANSI escape sequences may be used to influence text color and styling if + /// `supportsANSIStyling` is present in both the adapter's `Capabilities` and + /// the client's `InitializeRequestArguments`. A client may strip any + /// unrecognized ANSI sequences. + /// + /// If the `supportsANSIStyling` capabilities are not both true, then the + /// client should display the output literally. + std::string output; + + /// Support for keeping an output log organized by grouping related messages. + OutputGroup group = eOutputGroupNone; + + /// If an attribute `variablesReference` exists and its value is > 0, the + /// output contains objects which can be retrieved by passing + /// `variablesReference` to the `variables` request as long as execution + /// remains suspended. See 'Lifetime of Object References' in the Overview + /// section for details. + uint64_t variablesReference = 0; + + /// The source location where the output was produced. + std::optional source; + + /// The source location's line where the output was produced. + uint32_t line = LLDB_INVALID_LINE_NUMBER; + + /// The position in `line` where the output was produced. It is measured in + /// UTF-16 code units and the client capability `columnsStartAt1` determines + /// whether it is 0- or 1-based. + uint32_t column = LLDB_INVALID_COLUMN_NUMBER; + + /// Additional data to report. For the `telemetry` category the data is sent + /// to telemetry, for the other categories the data is shown in JSON format. + std::optional data; + + /// A reference that allows the client to request the location where the new + /// value is declared. For example, if the logged value is function pointer, + /// the adapter may be able to look up the function's location. This should + /// be present only if the adapter is likely to be able to resolve the + /// location. + /// + /// This reference shares the same lifetime as the `variablesReference`. See + /// 'Lifetime of Object References' in the Overview section for details. + int64_t locationReference = 0; +}; +llvm::json::Value toJSON(const OutputEventBody &); + } // end namespace lldb_dap::protocol #endif diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp index ac01cfb95dd41..c8e30b2458a91 100644 --- a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp +++ b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp @@ -7,6 +7,7 @@ //===----------------------------------------------------------------------===// #include "Protocol/ProtocolRequests.h" +#include "DAP.h" #include "JSONUtils.h" #include "Protocol/ProtocolTypes.h" #include "lldb/lldb-defines.h" diff --git a/lldb/tools/lldb-dap/SourceBreakpoint.cpp b/lldb/tools/lldb-dap/SourceBreakpoint.cpp index 843a5eb09c7ae..4fb8c03f4ee99 100644 --- a/lldb/tools/lldb-dap/SourceBreakpoint.cpp +++ b/lldb/tools/lldb-dap/SourceBreakpoint.cpp @@ -10,6 +10,7 @@ #include "BreakpointBase.h" #include "DAP.h" #include "JSONUtils.h" +#include "Protocol/ProtocolEvents.h" #include "ProtocolUtils.h" #include "lldb/API/SBBreakpoint.h" #include "lldb/API/SBFileSpec.h" @@ -377,7 +378,7 @@ void SourceBreakpoint::SetLogMessage() { void SourceBreakpoint::NotifyLogMessageError(llvm::StringRef error) { std::string message = "Log message has error: "; message += error; - m_dap.SendOutput(OutputType::Console, message); + m_dap.SendOutput(protocol::eOutputCategoryConsole, message); } /*static*/ @@ -411,7 +412,7 @@ bool SourceBreakpoint::BreakpointHitCallback( } if (!output.empty() && output.back() != '\n') output.push_back('\n'); // Ensure log message has line break. - bp->m_dap.SendOutput(OutputType::Console, output.c_str()); + bp->m_dap.SendOutput(protocol::eOutputCategoryConsole, output.c_str()); // Do not stop. return false; diff --git a/lldb/tools/lldb-dap/Variables.cpp b/lldb/tools/lldb-dap/Variables.cpp index 777e3183d8c0d..5390747d85f1b 100644 --- a/lldb/tools/lldb-dap/Variables.cpp +++ b/lldb/tools/lldb-dap/Variables.cpp @@ -8,9 +8,17 @@ #include "Variables.h" #include "JSONUtils.h" +#include "lldb/API/SBValueList.h" using namespace lldb_dap; +bool lldb_dap::is_permanent(lldb::SBValue &v) { + llvm::StringRef name = v.GetName(); + // Variables stored by the REPL are permanent, like $0, $1, etc. + uint64_t dummy_idx; + return name.consume_front("$") && !name.consumeInteger(0, dummy_idx); +} + lldb::SBValueList *Variables::GetTopLevelScope(int64_t variablesReference) { switch (variablesReference) { case VARREF_LOCALS: @@ -20,6 +28,9 @@ lldb::SBValueList *Variables::GetTopLevelScope(int64_t variablesReference) { case VARREF_REGS: return ®isters; default: + if (m_referencedlists.contains(variablesReference) && + m_referencedlists[variablesReference].IsValid()) + return &m_referencedlists[variablesReference]; return nullptr; } } @@ -54,12 +65,37 @@ lldb::SBValue Variables::GetVariable(int64_t var_ref) const { return lldb::SBValue(); } -int64_t Variables::InsertVariable(lldb::SBValue variable, bool is_permanent) { - int64_t var_ref = GetNewVariableReference(is_permanent); - if (is_permanent) - m_referencedpermanent_variables.insert(std::make_pair(var_ref, variable)); - else - m_referencedvariables.insert(std::make_pair(var_ref, variable)); +int64_t Variables::InsertVariable(lldb::SBValue variable) { + bool perm = is_permanent(variable); + + llvm::DenseMap &var_map = + perm ? m_referencedpermanent_variables : m_referencedvariables; + for (auto &[var_ref, var] : var_map) { + if (var.IsValid() && var.GetID() == variable.GetID()) + return var_ref; + } + int64_t var_ref = GetNewVariableReference(perm); + var_map.emplace_or_assign(var_ref, variable); + return var_ref; +} + +int64_t Variables::InsertVariables(lldb::SBValueList variables) { + for (const auto &[var_ref, variable] : m_referencedlists) + if (variable.IsValid() && variable.GetSize() == variables.GetSize()) { + bool all_match = true; + for (uint32_t i = 0; i < variables.GetSize(); ++i) { + if (variable.GetValueAtIndex(i).GetID() != + variables.GetValueAtIndex(i).GetID()) { + all_match = false; + break; + } + } + if (all_match) + return var_ref; + } + + int64_t var_ref = GetNewVariableReference(true); + m_referencedlists.insert_or_assign(var_ref, variables); return var_ref; } diff --git a/lldb/tools/lldb-dap/Variables.h b/lldb/tools/lldb-dap/Variables.h index 0ed84b36aef99..8820e7b64d74c 100644 --- a/lldb/tools/lldb-dap/Variables.h +++ b/lldb/tools/lldb-dap/Variables.h @@ -20,6 +20,8 @@ namespace lldb_dap { +bool is_permanent(lldb::SBValue &); + struct Variables { lldb::SBValueList locals; lldb::SBValueList globals; @@ -41,7 +43,8 @@ struct Variables { /// Insert a new \p variable. /// \return variableReference assigned to this expandable variable. - int64_t InsertVariable(lldb::SBValue variable, bool is_permanent); + int64_t InsertVariable(lldb::SBValue variable); + int64_t InsertVariables(lldb::SBValueList variables); lldb::SBValueList *GetTopLevelScope(int64_t variablesReference); @@ -62,6 +65,8 @@ struct Variables { /// These are the variables evaluated from debug console REPL. llvm::DenseMap m_referencedpermanent_variables; + llvm::DenseMap m_referencedlists; + int64_t m_next_temporary_var_ref{VARREF_FIRST_VAR_IDX}; int64_t m_next_permanent_var_ref{PermanentVariableStartIndex}; }; diff --git a/lldb/unittests/DAP/Handler/DisconnectTest.cpp b/lldb/unittests/DAP/Handler/DisconnectTest.cpp index 212c5698feea8..3307f4497296f 100644 --- a/lldb/unittests/DAP/Handler/DisconnectTest.cpp +++ b/lldb/unittests/DAP/Handler/DisconnectTest.cpp @@ -46,12 +46,10 @@ TEST_F(DisconnectRequestHandlerTest, DisconnectTriggersTerminateCommands) { DisconnectRequestHandler handler(*dap); - dap->configuration.terminateCommands = {"?script print(1)", - "script print(2)"}; + dap->configuration.terminateCommands = {"?help", "script print(2)"}; EXPECT_EQ(dap->target.GetProcess().GetState(), lldb::eStateStopped); ASSERT_THAT_ERROR(handler.Run(std::nullopt), Succeeded()); - EXPECT_CALL(client, Received(Output("1\n"))); - EXPECT_CALL(client, Received(Output("2\n"))).Times(2); + EXPECT_CALL(client, Received(Output("2\n"))); EXPECT_CALL(client, Received(Output("(lldb) script print(2)\n"))); EXPECT_CALL(client, Received(Output("Running terminateCommands:\n"))); EXPECT_CALL(client, Received(IsEvent("terminated", _))); diff --git a/lldb/unittests/DAP/TestBase.cpp b/lldb/unittests/DAP/TestBase.cpp index 8cb459964f7d8..717db118b3eaf 100644 --- a/lldb/unittests/DAP/TestBase.cpp +++ b/lldb/unittests/DAP/TestBase.cpp @@ -55,8 +55,9 @@ void TransportBase::SetUp() { } void TransportBase::Run() { - bool addition_succeeded = loop.AddPendingCallback( - [](lldb_private::MainLoopBase &loop) { loop.RequestTermination(); }); + bool addition_succeeded = loop.AddCallback( + [](lldb_private::MainLoopBase &loop) { loop.RequestTermination(); }, + std::chrono::milliseconds(5000)); EXPECT_TRUE(addition_succeeded); EXPECT_THAT_ERROR(loop.Run().takeError(), llvm::Succeeded()); } @@ -93,10 +94,6 @@ bool DAPTestBase::GetDebuggerSupportsTarget(StringRef platform) { } void DAPTestBase::CreateDebugger() { - dap->debugger = lldb::SBDebugger::Create(); - ASSERT_TRUE(dap->debugger); - dap->target = dap->debugger.GetDummyTarget(); - Expected dev_null = FileSystem::Instance().Open( FileSpec(FileSystem::DEV_NULL), File::eOpenOptionReadWrite); ASSERT_THAT_EXPECTED(dev_null, Succeeded()); @@ -106,13 +103,9 @@ void DAPTestBase::CreateDebugger() { ASSERT_THAT_ERROR(dap->ConfigureIO(dev_null_stream, dev_null_stream), Succeeded()); - dap->debugger.SetInputFile(dap->in); - auto out_fd = dap->out.GetWriteFileDescriptor(); - ASSERT_THAT_EXPECTED(out_fd, Succeeded()); - dap->debugger.SetOutputFile(lldb::SBFile(*out_fd, "w", false)); - auto err_fd = dap->out.GetWriteFileDescriptor(); - ASSERT_THAT_EXPECTED(err_fd, Succeeded()); - dap->debugger.SetErrorFile(lldb::SBFile(*err_fd, "w", false)); + ASSERT_THAT_ERROR(dap->CreateDebugger({}), Succeeded()); + dap->debugger.RunCommandInterpreter(/*auto_handle_events=*/false, + /*spawn_thread=*/true); } void DAPTestBase::LoadCore() {