From 671b91211eefd67cc3f5798e30edef9d2f97660c Mon Sep 17 00:00:00 2001 From: John Harrison Date: Thu, 20 Nov 2025 10:15:12 -0800 Subject: [PATCH] [lldb-dap] Refactoring IO handling for the SBDebugger. This refactors the IO handling of lldb-dap and its SBDebugger. This adjusts the SBDebugger instance to have a pty associated with in/out/err. Using a pty allows us to handle commands with raw input modes, such as `script` or `breakpoint command add`. Additionally, to better handle output produced by the debugger and evaluate command I added a print helper. This new print helper will inspect the SBCommandReturnObject and store any associated variables in the variable store. This lets users more easily inspect variables directly in the Debug Console in VSCode. --- .../test/tools/lldb-dap/dap_server.py | 14 +- .../tools/lldb-dap/cancel/TestDAP_cancel.py | 2 - .../lldb-dap/evaluate/TestDAP_evaluate.py | 1 + lldb/tools/lldb-dap/DAP.cpp | 413 ++++++++++++------ lldb/tools/lldb-dap/DAP.h | 55 ++- lldb/tools/lldb-dap/DAPError.cpp | 14 +- lldb/tools/lldb-dap/DAPError.h | 22 +- lldb/tools/lldb-dap/EventHelper.cpp | 12 +- .../lldb-dap/Handler/AttachRequestHandler.cpp | 3 +- .../lldb-dap/Handler/CompletionsHandler.cpp | 3 +- .../ConfigurationDoneRequestHandler.cpp | 15 +- .../Handler/EvaluateRequestHandler.cpp | 70 +-- .../Handler/InitializeRequestHandler.cpp | 46 +- .../tools/lldb-dap/Handler/RequestHandler.cpp | 25 +- lldb/tools/lldb-dap/Handler/RequestHandler.h | 31 +- .../Handler/SetVariableRequestHandler.cpp | 3 +- .../Handler/VariablesRequestHandler.cpp | 63 ++- lldb/tools/lldb-dap/Protocol/ProtocolBase.cpp | 16 +- lldb/tools/lldb-dap/Protocol/ProtocolBase.h | 6 +- .../lldb-dap/Protocol/ProtocolEvents.cpp | 51 +++ lldb/tools/lldb-dap/Protocol/ProtocolEvents.h | 93 ++++ .../lldb-dap/Protocol/ProtocolRequests.cpp | 1 + lldb/tools/lldb-dap/SourceBreakpoint.cpp | 5 +- lldb/tools/lldb-dap/Variables.cpp | 48 +- lldb/tools/lldb-dap/Variables.h | 7 +- lldb/unittests/DAP/Handler/DisconnectTest.cpp | 6 +- lldb/unittests/DAP/TestBase.cpp | 19 +- 27 files changed, 716 insertions(+), 328 deletions(-) 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() {