
On macOS, breakpoints are briefly unresolved between process launch and when the dynamic loader has informed us about the loaded libraries. This information was being forwarded by lldb-dap, but only partially. In the event handler, we were listening for the `LocationsAdded` and `LocationsRemoved` breakpoint events. For the scenario described above, the latter would trigger and we'd send an event reporting the breakpoint as unresolved. The problem is that when the breakpoint location is resolved again, you receive a `LocationsResolved` event, not a `LocationsAdded` event. As a result, the breakpoint would continue to show up as unresolved in the DAP client. I found a test that tried to test part of this behavior, but the test was broken and disabled. I revived the test and added coverage for the situation described above. Fixes #112629 rdar://137968318
419 lines
17 KiB
C++
419 lines
17 KiB
C++
//===-- InitializeRequestHandler.cpp --------------------------------------===//
|
|
//
|
|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
// See https://llvm.org/LICENSE.txt for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "DAP.h"
|
|
#include "EventHelper.h"
|
|
#include "JSONUtils.h"
|
|
#include "RequestHandler.h"
|
|
#include "lldb/API/SBEvent.h"
|
|
#include "lldb/API/SBListener.h"
|
|
#include "lldb/API/SBStream.h"
|
|
|
|
using namespace lldb;
|
|
|
|
namespace lldb_dap {
|
|
|
|
static void ProgressEventThreadFunction(DAP &dap) {
|
|
llvm::set_thread_name(dap.name + ".progress_handler");
|
|
lldb::SBListener listener("lldb-dap.progress.listener");
|
|
dap.debugger.GetBroadcaster().AddListener(
|
|
listener, lldb::SBDebugger::eBroadcastBitProgress |
|
|
lldb::SBDebugger::eBroadcastBitExternalProgress);
|
|
dap.broadcaster.AddListener(listener, eBroadcastBitStopProgressThread);
|
|
lldb::SBEvent event;
|
|
bool done = false;
|
|
while (!done) {
|
|
if (listener.WaitForEvent(1, event)) {
|
|
const auto event_mask = event.GetType();
|
|
if (event.BroadcasterMatchesRef(dap.broadcaster)) {
|
|
if (event_mask & eBroadcastBitStopProgressThread) {
|
|
done = true;
|
|
}
|
|
} else {
|
|
uint64_t progress_id = 0;
|
|
uint64_t completed = 0;
|
|
uint64_t total = 0;
|
|
bool is_debugger_specific = false;
|
|
const char *message = lldb::SBDebugger::GetProgressFromEvent(
|
|
event, progress_id, completed, total, is_debugger_specific);
|
|
if (message)
|
|
dap.SendProgressEvent(progress_id, message, completed, total);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// All events from the debugger, target, process, thread and frames are
|
|
// received in this function that runs in its own thread. We are using a
|
|
// "FILE *" to output packets back to VS Code and they have mutexes in them
|
|
// them prevent multiple threads from writing simultaneously so no locking
|
|
// is required.
|
|
static void EventThreadFunction(DAP &dap) {
|
|
llvm::set_thread_name(dap.name + ".event_handler");
|
|
lldb::SBEvent event;
|
|
lldb::SBListener listener = dap.debugger.GetListener();
|
|
dap.broadcaster.AddListener(listener, eBroadcastBitStopEventThread);
|
|
bool done = false;
|
|
while (!done) {
|
|
if (listener.WaitForEvent(1, event)) {
|
|
const auto event_mask = event.GetType();
|
|
if (lldb::SBProcess::EventIsProcessEvent(event)) {
|
|
lldb::SBProcess process = lldb::SBProcess::GetProcessFromEvent(event);
|
|
if (event_mask & lldb::SBProcess::eBroadcastBitStateChanged) {
|
|
auto state = lldb::SBProcess::GetStateFromEvent(event);
|
|
switch (state) {
|
|
case lldb::eStateInvalid:
|
|
// Not a state event
|
|
break;
|
|
case lldb::eStateUnloaded:
|
|
break;
|
|
case lldb::eStateConnected:
|
|
break;
|
|
case lldb::eStateAttaching:
|
|
break;
|
|
case lldb::eStateLaunching:
|
|
break;
|
|
case lldb::eStateStepping:
|
|
break;
|
|
case lldb::eStateCrashed:
|
|
break;
|
|
case lldb::eStateDetached:
|
|
break;
|
|
case lldb::eStateSuspended:
|
|
break;
|
|
case lldb::eStateStopped:
|
|
// We launch and attach in synchronous mode then the first stop
|
|
// event will not be delivered. If we use "launchCommands" during a
|
|
// launch or "attachCommands" during an attach we might some process
|
|
// stop events which we do not want to send an event for. We will
|
|
// manually send a stopped event in request_configurationDone(...)
|
|
// so don't send any before then.
|
|
if (dap.configuration_done_sent) {
|
|
// Only report a stopped event if the process was not
|
|
// automatically restarted.
|
|
if (!lldb::SBProcess::GetRestartedFromEvent(event)) {
|
|
SendStdOutStdErr(dap, process);
|
|
SendThreadStoppedEvent(dap);
|
|
}
|
|
}
|
|
break;
|
|
case lldb::eStateRunning:
|
|
dap.WillContinue();
|
|
SendContinuedEvent(dap);
|
|
break;
|
|
case lldb::eStateExited:
|
|
lldb::SBStream stream;
|
|
process.GetStatus(stream);
|
|
dap.SendOutput(OutputType::Console, stream.GetData());
|
|
|
|
// When restarting, we can get an "exited" event for the process we
|
|
// just killed with the old PID, or even with no PID. In that case
|
|
// we don't have to terminate the session.
|
|
if (process.GetProcessID() == LLDB_INVALID_PROCESS_ID ||
|
|
process.GetProcessID() == dap.restarting_process_id) {
|
|
dap.restarting_process_id = LLDB_INVALID_PROCESS_ID;
|
|
} else {
|
|
// Run any exit LLDB commands the user specified in the
|
|
// launch.json
|
|
dap.RunExitCommands();
|
|
SendProcessExitedEvent(dap, process);
|
|
dap.SendTerminatedEvent();
|
|
done = true;
|
|
}
|
|
break;
|
|
}
|
|
} else if ((event_mask & lldb::SBProcess::eBroadcastBitSTDOUT) ||
|
|
(event_mask & lldb::SBProcess::eBroadcastBitSTDERR)) {
|
|
SendStdOutStdErr(dap, process);
|
|
}
|
|
} else if (lldb::SBBreakpoint::EventIsBreakpointEvent(event)) {
|
|
if (event_mask & lldb::SBTarget::eBroadcastBitBreakpointChanged) {
|
|
auto event_type =
|
|
lldb::SBBreakpoint::GetBreakpointEventTypeFromEvent(event);
|
|
auto bp = Breakpoint(
|
|
dap, lldb::SBBreakpoint::GetBreakpointFromEvent(event));
|
|
// If the breakpoint was set through DAP, it will have the
|
|
// BreakpointBase::GetBreakpointLabel() label. Regardless
|
|
// of whether locations were added, removed, or resolved, the
|
|
// breakpoint isn't going away and the reason is always "changed".
|
|
if ((event_type & lldb::eBreakpointEventTypeLocationsAdded ||
|
|
event_type & lldb::eBreakpointEventTypeLocationsRemoved ||
|
|
event_type & lldb::eBreakpointEventTypeLocationsResolved) &&
|
|
bp.MatchesName(BreakpointBase::GetBreakpointLabel())) {
|
|
// As the DAP client already knows the path of this breakpoint, we
|
|
// don't need to send it back as part of the "changed" event. This
|
|
// avoids sending paths that should be source mapped. Note that
|
|
// CreateBreakpoint doesn't apply source mapping and certain
|
|
// implementation ignore the source part of this event anyway.
|
|
llvm::json::Value source_bp = CreateBreakpoint(&bp);
|
|
source_bp.getAsObject()->erase("source");
|
|
|
|
llvm::json::Object body;
|
|
body.try_emplace("breakpoint", source_bp);
|
|
body.try_emplace("reason", "changed");
|
|
|
|
llvm::json::Object bp_event = CreateEventObject("breakpoint");
|
|
bp_event.try_emplace("body", std::move(body));
|
|
|
|
dap.SendJSON(llvm::json::Value(std::move(bp_event)));
|
|
}
|
|
}
|
|
} else if (event.BroadcasterMatchesRef(dap.broadcaster)) {
|
|
if (event_mask & eBroadcastBitStopEventThread) {
|
|
done = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// "InitializeRequest": {
|
|
// "allOf": [ { "$ref": "#/definitions/Request" }, {
|
|
// "type": "object",
|
|
// "description": "Initialize request; value of command field is
|
|
// 'initialize'.",
|
|
// "properties": {
|
|
// "command": {
|
|
// "type": "string",
|
|
// "enum": [ "initialize" ]
|
|
// },
|
|
// "arguments": {
|
|
// "$ref": "#/definitions/InitializeRequestArguments"
|
|
// }
|
|
// },
|
|
// "required": [ "command", "arguments" ]
|
|
// }]
|
|
// },
|
|
// "InitializeRequestArguments": {
|
|
// "type": "object",
|
|
// "description": "Arguments for 'initialize' request.",
|
|
// "properties": {
|
|
// "clientID": {
|
|
// "type": "string",
|
|
// "description": "The ID of the (frontend) client using this adapter."
|
|
// },
|
|
// "adapterID": {
|
|
// "type": "string",
|
|
// "description": "The ID of the debug adapter."
|
|
// },
|
|
// "locale": {
|
|
// "type": "string",
|
|
// "description": "The ISO-639 locale of the (frontend) client using
|
|
// this adapter, e.g. en-US or de-CH."
|
|
// },
|
|
// "linesStartAt1": {
|
|
// "type": "boolean",
|
|
// "description": "If true all line numbers are 1-based (default)."
|
|
// },
|
|
// "columnsStartAt1": {
|
|
// "type": "boolean",
|
|
// "description": "If true all column numbers are 1-based (default)."
|
|
// },
|
|
// "pathFormat": {
|
|
// "type": "string",
|
|
// "_enum": [ "path", "uri" ],
|
|
// "description": "Determines in what format paths are specified. The
|
|
// default is 'path', which is the native format."
|
|
// },
|
|
// "supportsVariableType": {
|
|
// "type": "boolean",
|
|
// "description": "Client supports the optional type attribute for
|
|
// variables."
|
|
// },
|
|
// "supportsVariablePaging": {
|
|
// "type": "boolean",
|
|
// "description": "Client supports the paging of variables."
|
|
// },
|
|
// "supportsRunInTerminalRequest": {
|
|
// "type": "boolean",
|
|
// "description": "Client supports the runInTerminal request."
|
|
// }
|
|
// },
|
|
// "required": [ "adapterID" ]
|
|
// },
|
|
// "InitializeResponse": {
|
|
// "allOf": [ { "$ref": "#/definitions/Response" }, {
|
|
// "type": "object",
|
|
// "description": "Response to 'initialize' request.",
|
|
// "properties": {
|
|
// "body": {
|
|
// "$ref": "#/definitions/Capabilities",
|
|
// "description": "The capabilities of this debug adapter."
|
|
// }
|
|
// }
|
|
// }]
|
|
// }
|
|
void InitializeRequestHandler::operator()(
|
|
const llvm::json::Object &request) const {
|
|
llvm::json::Object response;
|
|
FillResponse(request, response);
|
|
llvm::json::Object body;
|
|
|
|
const auto *arguments = request.getObject("arguments");
|
|
// sourceInitFile option is not from formal DAP specification. It is only
|
|
// used by unit tests to prevent sourcing .lldbinit files from environment
|
|
// which may affect the outcome of tests.
|
|
bool source_init_file = GetBoolean(arguments, "sourceInitFile", true);
|
|
|
|
// Do not source init files until in/out/err are configured.
|
|
dap.debugger = lldb::SBDebugger::Create(false);
|
|
dap.debugger.SetInputFile(dap.in);
|
|
auto out_fd = dap.out.GetWriteFileDescriptor();
|
|
if (llvm::Error err = out_fd.takeError()) {
|
|
response["success"] = false;
|
|
EmplaceSafeString(response, "message", llvm::toString(std::move(err)));
|
|
dap.SendJSON(llvm::json::Value(std::move(response)));
|
|
return;
|
|
}
|
|
dap.debugger.SetOutputFile(lldb::SBFile(*out_fd, "w", false));
|
|
auto err_fd = dap.err.GetWriteFileDescriptor();
|
|
if (llvm::Error err = err_fd.takeError()) {
|
|
response["success"] = false;
|
|
EmplaceSafeString(response, "message", llvm::toString(std::move(err)));
|
|
dap.SendJSON(llvm::json::Value(std::move(response)));
|
|
return;
|
|
}
|
|
dap.debugger.SetErrorFile(lldb::SBFile(*err_fd, "w", false));
|
|
|
|
auto interp = dap.debugger.GetCommandInterpreter();
|
|
|
|
if (source_init_file) {
|
|
dap.debugger.SkipLLDBInitFiles(false);
|
|
dap.debugger.SkipAppInitFiles(false);
|
|
lldb::SBCommandReturnObject init;
|
|
interp.SourceInitFileInGlobalDirectory(init);
|
|
interp.SourceInitFileInHomeDirectory(init);
|
|
}
|
|
|
|
if (llvm::Error err = dap.RunPreInitCommands()) {
|
|
response["success"] = false;
|
|
EmplaceSafeString(response, "message", llvm::toString(std::move(err)));
|
|
dap.SendJSON(llvm::json::Value(std::move(response)));
|
|
return;
|
|
}
|
|
|
|
dap.PopulateExceptionBreakpoints();
|
|
auto cmd = dap.debugger.GetCommandInterpreter().AddMultiwordCommand(
|
|
"lldb-dap", "Commands for managing lldb-dap.");
|
|
if (GetBoolean(arguments, "supportsStartDebuggingRequest", false)) {
|
|
cmd.AddCommand(
|
|
"start-debugging", new StartDebuggingRequestHandler(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 ReplModeRequestHandler(dap),
|
|
"Get or set the repl behavior of lldb-dap evaluation requests.");
|
|
cmd.AddCommand("send-event", new SendEventRequestHandler(dap),
|
|
"Sends an DAP event to the client.");
|
|
|
|
dap.progress_event_thread =
|
|
std::thread(ProgressEventThreadFunction, std::ref(dap));
|
|
|
|
// Start our event thread so we can receive events from the debugger, target,
|
|
// process and more.
|
|
dap.event_thread = std::thread(EventThreadFunction, std::ref(dap));
|
|
|
|
// The debug adapter supports the configurationDoneRequest.
|
|
body.try_emplace("supportsConfigurationDoneRequest", true);
|
|
// The debug adapter supports function breakpoints.
|
|
body.try_emplace("supportsFunctionBreakpoints", true);
|
|
// The debug adapter supports conditional breakpoints.
|
|
body.try_emplace("supportsConditionalBreakpoints", true);
|
|
// The debug adapter supports breakpoints that break execution after a
|
|
// specified number of hits.
|
|
body.try_emplace("supportsHitConditionalBreakpoints", true);
|
|
// The debug adapter supports a (side effect free) evaluate request for
|
|
// data hovers.
|
|
body.try_emplace("supportsEvaluateForHovers", true);
|
|
// Available filters or options for the setExceptionBreakpoints request.
|
|
llvm::json::Array filters;
|
|
for (const auto &exc_bp : *dap.exception_breakpoints) {
|
|
filters.emplace_back(CreateExceptionBreakpointFilter(exc_bp));
|
|
}
|
|
body.try_emplace("exceptionBreakpointFilters", std::move(filters));
|
|
// The debug adapter supports launching a debugee in intergrated VSCode
|
|
// terminal.
|
|
body.try_emplace("supportsRunInTerminalRequest", true);
|
|
// The debug adapter supports stepping back via the stepBack and
|
|
// reverseContinue requests.
|
|
body.try_emplace("supportsStepBack", false);
|
|
// The debug adapter supports setting a variable to a value.
|
|
body.try_emplace("supportsSetVariable", true);
|
|
// The debug adapter supports restarting a frame.
|
|
body.try_emplace("supportsRestartFrame", false);
|
|
// The debug adapter supports the gotoTargetsRequest.
|
|
body.try_emplace("supportsGotoTargetsRequest", false);
|
|
// The debug adapter supports the stepInTargetsRequest.
|
|
body.try_emplace("supportsStepInTargetsRequest", true);
|
|
// The debug adapter supports the completions request.
|
|
body.try_emplace("supportsCompletionsRequest", true);
|
|
// The debug adapter supports the disassembly request.
|
|
body.try_emplace("supportsDisassembleRequest", true);
|
|
// The debug adapter supports the `breakpointLocations` request.
|
|
body.try_emplace("supportsBreakpointLocationsRequest", true);
|
|
// The debug adapter supports stepping granularities (argument `granularity`)
|
|
// for the stepping requests.
|
|
body.try_emplace("supportsSteppingGranularity", true);
|
|
// The debug adapter support for instruction breakpoint.
|
|
body.try_emplace("supportsInstructionBreakpoints", true);
|
|
|
|
llvm::json::Array completion_characters;
|
|
completion_characters.emplace_back(".");
|
|
completion_characters.emplace_back(" ");
|
|
completion_characters.emplace_back("\t");
|
|
body.try_emplace("completionTriggerCharacters",
|
|
std::move(completion_characters));
|
|
|
|
// The debug adapter supports the modules request.
|
|
body.try_emplace("supportsModulesRequest", true);
|
|
// The set of additional module information exposed by the debug adapter.
|
|
// body.try_emplace("additionalModuleColumns"] = ColumnDescriptor
|
|
// Checksum algorithms supported by the debug adapter.
|
|
// body.try_emplace("supportedChecksumAlgorithms"] = ChecksumAlgorithm
|
|
// The debug adapter supports the RestartRequest. In this case a client
|
|
// should not implement 'restart' by terminating and relaunching the adapter
|
|
// but by calling the RestartRequest.
|
|
body.try_emplace("supportsRestartRequest", true);
|
|
// The debug adapter supports 'exceptionOptions' on the
|
|
// setExceptionBreakpoints request.
|
|
body.try_emplace("supportsExceptionOptions", true);
|
|
// The debug adapter supports a 'format' attribute on the stackTraceRequest,
|
|
// variablesRequest, and evaluateRequest.
|
|
body.try_emplace("supportsValueFormattingOptions", true);
|
|
// The debug adapter supports the exceptionInfo request.
|
|
body.try_emplace("supportsExceptionInfoRequest", true);
|
|
// The debug adapter supports the 'terminateDebuggee' attribute on the
|
|
// 'disconnect' request.
|
|
body.try_emplace("supportTerminateDebuggee", true);
|
|
// The debug adapter supports the delayed loading of parts of the stack,
|
|
// which requires that both the 'startFrame' and 'levels' arguments and the
|
|
// 'totalFrames' result of the 'StackTrace' request are supported.
|
|
body.try_emplace("supportsDelayedStackTraceLoading", true);
|
|
// The debug adapter supports the 'loadedSources' request.
|
|
body.try_emplace("supportsLoadedSourcesRequest", false);
|
|
// The debug adapter supports sending progress reporting events.
|
|
body.try_emplace("supportsProgressReporting", true);
|
|
// The debug adapter supports 'logMessage' in breakpoint.
|
|
body.try_emplace("supportsLogPoints", true);
|
|
// The debug adapter supports data watchpoints.
|
|
body.try_emplace("supportsDataBreakpoints", true);
|
|
// The debug adapter supports the `readMemory` request.
|
|
body.try_emplace("supportsReadMemoryRequest", true);
|
|
|
|
// Put in non-DAP specification lldb specific information.
|
|
llvm::json::Object lldb_json;
|
|
lldb_json.try_emplace("version", dap.debugger.GetVersionString());
|
|
body.try_emplace("__lldb", std::move(lldb_json));
|
|
|
|
response.try_emplace("body", std::move(body));
|
|
dap.SendJSON(llvm::json::Value(std::move(response)));
|
|
}
|
|
|
|
} // namespace lldb_dap
|