diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py @@ -340,6 +340,18 @@ }, set_sequence=False, ) + elif response_or_request["command"] == "startDebugging": + self.send_packet( + { + "type": "response", + "seq": -1, + "request_seq": response_or_request["seq"], + "success": True, + "command": "startDebugging", + "body": {}, + }, + set_sequence=False, + ) else: desc = 'unkonwn reverse request "%s"' % ( response_or_request["command"] diff --git a/lldb/test/API/tools/lldb-vscode/startDebugging/Makefile b/lldb/test/API/tools/lldb-vscode/startDebugging/Makefile new file mode 100644 --- /dev/null +++ b/lldb/test/API/tools/lldb-vscode/startDebugging/Makefile @@ -0,0 +1,3 @@ +C_SOURCES := main.c + +include Makefile.rules diff --git a/lldb/test/API/tools/lldb-vscode/startDebugging/TestVSCode_startDebugging.py b/lldb/test/API/tools/lldb-vscode/startDebugging/TestVSCode_startDebugging.py new file mode 100644 --- /dev/null +++ b/lldb/test/API/tools/lldb-vscode/startDebugging/TestVSCode_startDebugging.py @@ -0,0 +1,30 @@ +""" +Test lldb-vscode startDebugging reverse request +""" + + +import vscode +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbutil +import lldbvscode_testcase + + +class TestVSCode_startDebugging(lldbvscode_testcase.VSCodeTestCaseBase): + def test_startDebugging(self): + """ + Tests the "startDebugging" reverse request. It makes sure that the IDE can + start a child debug session. + """ + program = self.getBuildArtifact("a.out") + source = "main.c" + self.build_and_launch( + program, + # Ensure we can start child debug session. + postRunCommands='lldb-vscode startDebugging launch \'{"program":"{}"}\''.format(program) + ) + + breakpoint_line = line_number(source, "// breakpoint") + + self.set_source_breakpoints(source, [breakpoint_line]) + self.continue_to_next_stop() \ No newline at end of file diff --git a/lldb/test/API/tools/lldb-vscode/startDebugging/main.c b/lldb/test/API/tools/lldb-vscode/startDebugging/main.c new file mode 100644 --- /dev/null +++ b/lldb/test/API/tools/lldb-vscode/startDebugging/main.c @@ -0,0 +1,6 @@ +#include + +int main(int argc, char const *argv[]) { + printf("server/client example\n"); // breakpoint 1 + return 0; +} diff --git a/lldb/tools/lldb-vscode/JSONUtils.cpp b/lldb/tools/lldb-vscode/JSONUtils.cpp --- a/lldb/tools/lldb-vscode/JSONUtils.cpp +++ b/lldb/tools/lldb-vscode/JSONUtils.cpp @@ -1104,10 +1104,6 @@ llvm::StringRef debug_adaptor_path, llvm::StringRef comm_file, lldb::pid_t debugger_pid) { - llvm::json::Object reverse_request; - reverse_request.try_emplace("type", "request"); - reverse_request.try_emplace("command", "runInTerminal"); - llvm::json::Object run_in_terminal_args; // This indicates the IDE to open an embedded terminal, instead of opening the // terminal in a new window. @@ -1143,9 +1139,7 @@ run_in_terminal_args.try_emplace("env", llvm::json::Value(std::move(environment))); - reverse_request.try_emplace( - "arguments", llvm::json::Value(std::move(run_in_terminal_args))); - return reverse_request; + return run_in_terminal_args; } // Keep all the top level items from the statistics dump, except for the diff --git a/lldb/tools/lldb-vscode/README.md b/lldb/tools/lldb-vscode/README.md --- a/lldb/tools/lldb-vscode/README.md +++ b/lldb/tools/lldb-vscode/README.md @@ -11,6 +11,8 @@ - [Attach to process using process ID](#attach-using-pid) - [Attach to process by name](#attach-by-name) - [Loading a core file](#loading-a-core-file) +- [Custom Debugger Commands](#custom-debugger-commands) + - [startDebugging](#startDebugging) # Introduction @@ -203,3 +205,33 @@ "program": "/tmp/a.out" } ``` + +# Custom debugger commands + +The `lldb-vscode` tool includes additional custom commands to support the Debug +Adapter Protocol features. + +## startDebugging + +Using the command `lldb-vscode startDebugging` it is possible to trigger a +reverse request to the client requesting a child debug session with the +specified configuration. For example, this can be used to attached to forked or +spawned processes. For more information see +[Reverse Requests StartDebugging](https://microsoft.github.io/debug-adapter-protocol/specification#Reverse_Requests_StartDebugging). + +The custom command has the following format: + +``` +lldb-vscode startDebugging +``` + +This will launch a server and then request a child debug session for a client. + +```javascript +{ + "program": "server", + "postRunCommand": [ + "lldb-vscode startDebugging launch '{\"program\":\"client\"}'" + ] +} +``` diff --git a/lldb/tools/lldb-vscode/VSCode.h b/lldb/tools/lldb-vscode/VSCode.h --- a/lldb/tools/lldb-vscode/VSCode.h +++ b/lldb/tools/lldb-vscode/VSCode.h @@ -11,8 +11,10 @@ #include "llvm/Config/llvm-config.h" // for LLVM_ON_UNIX +#include #include #include +#include #include #include #include @@ -121,6 +123,11 @@ void Clear(); }; +struct StartDebuggingRequestHandler : public lldb::SBCommandPluginInterface { + bool DoExecute(lldb::SBDebugger debugger, char **command, + lldb::SBCommandReturnObject &result) override; +}; + struct VSCode { std::string debug_adaptor_path; InputStream input; @@ -146,7 +153,7 @@ // arguments if we get a RestartRequest. std::optional last_launch_or_attach_request; lldb::tid_t focus_tid; - bool sent_terminated_event; + std::atomic sent_terminated_event; bool stop_at_entry; bool is_attach; // The process event thread normally responds to process exited events by @@ -154,13 +161,20 @@ // the old process here so we can detect this case and keep running. lldb::pid_t restarting_process_id; bool configuration_done_sent; - uint32_t reverse_request_seq; std::map request_handlers; bool waiting_for_run_in_terminal; ProgressEventReporter progress_event_reporter; // Keep track of the last stop thread index IDs as threads won't go away // unless we send a "thread" event to indicate the thread exited. llvm::DenseSet thread_ids; + uint32_t reverse_request_seq; + std::mutex call_mutex; + std::map< + /* request_seq */ int, + /* response promise */ std::promise>> + inflight_reverse_requests; + StartDebuggingRequestHandler start_debugging_request_handler; + VSCode(); ~VSCode(); VSCode(const VSCode &rhs) = delete; @@ -224,19 +238,21 @@ PacketStatus GetNextObject(llvm::json::Object &object); bool HandleObject(const llvm::json::Object &object); - /// Send a Debug Adapter Protocol reverse request to the IDE + llvm::Error Loop(); + + /// Send a Debug Adapter Protocol reverse request to the IDE. /// - /// \param[in] request - /// The payload of the request to send. + /// \param[in] command + /// The reverse request command. /// - /// \param[out] response - /// The response of the IDE. It might be undefined if there was an error. + /// \param[in] arguments + /// The reverse request arguements. /// /// \return - /// A \a PacketStatus object indicating the sucess or failure of the - /// request. - PacketStatus SendReverseRequest(llvm::json::Object request, - llvm::json::Object &response); + /// A future that resolves to the response object indicating the sucess or + /// failure of the request. + std::future> + SendReverseRequest(llvm::StringRef command, llvm::json::Value arguments); /// Registers a callback handler for a Debug Adapter Protocol request /// diff --git a/lldb/tools/lldb-vscode/VSCode.cpp b/lldb/tools/lldb-vscode/VSCode.cpp --- a/lldb/tools/lldb-vscode/VSCode.cpp +++ b/lldb/tools/lldb-vscode/VSCode.cpp @@ -41,10 +41,10 @@ focus_tid(LLDB_INVALID_THREAD_ID), sent_terminated_event(false), stop_at_entry(false), is_attach(false), restarting_process_id(LLDB_INVALID_PROCESS_ID), - configuration_done_sent(false), reverse_request_seq(0), - waiting_for_run_in_terminal(false), + configuration_done_sent(false), waiting_for_run_in_terminal(false), progress_event_reporter( - [&](const ProgressEvent &event) { SendJSON(event.ToJSON()); }) { + [&](const ProgressEvent &event) { SendJSON(event.ToJSON()); }), + reverse_request_seq(0) { const char *log_file_path = getenv("LLDBVSCODE_LOG"); #if defined(_WIN32) // Windows opens stdout and stdin in text mode which converts \n to 13,10 @@ -505,24 +505,91 @@ return false; // Fail } } + + if (packet_type == "response") { + auto id = GetSigned(object, "request_seq", 0); + std::promise> response_promise; + + { + std::lock_guard locker(call_mutex); + if (inflight_reverse_requests.find(id) != + inflight_reverse_requests.end()) { + response_promise = + std::move(inflight_reverse_requests.find(id)->second); + } else { + if (log) { + *log << "Unhandled reply " << id << std::endl; + } + return true; + } + } + + // Result should be given, use null if not. + if (GetBoolean(object, "success", false)) { + llvm::json::Value Result = nullptr; + if (auto *B = object.get("body")) { + Result = std::move(*B); + } + response_promise.set_value(std::move(Result)); + } else { + llvm::StringRef message = GetString(object, "message"); + if (message.empty()) { + message = "Unknown error, response failed"; + } + response_promise.set_value(llvm::createStringError( + std::error_code(-1, std::generic_category()), message)); + } + + return true; + } + return false; } -PacketStatus VSCode::SendReverseRequest(llvm::json::Object request, - llvm::json::Object &response) { - request.try_emplace("seq", ++reverse_request_seq); - SendJSON(llvm::json::Value(std::move(request))); - while (true) { - PacketStatus status = GetNextObject(response); - const auto packet_type = GetString(response, "type"); - if (packet_type == "response") - return status; - else { - // Not our response, we got another packet - HandleObject(response); +llvm::Error VSCode::Loop() { + while (!sent_terminated_event) { + llvm::json::Object object; + lldb_vscode::PacketStatus status = GetNextObject(object); + + if (status == lldb_vscode::PacketStatus::EndOfFile) { + break; + } + + if (status != lldb_vscode::PacketStatus::Success) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "failed to send packet"); + } + + if (!HandleObject(object)) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "unhandled packet"); } } - return PacketStatus::EndOfFile; + + return llvm::Error::success(); +} + +std::future> +VSCode::SendReverseRequest(llvm::StringRef command, + llvm::json::Value arguments) { + int64_t id; + std::future> response_future; + { + std::lock_guard locker(call_mutex); + id = ++reverse_request_seq; + std::promise> response_promise; + response_future = response_promise.get_future(); + inflight_reverse_requests.emplace(id, std::move(response_promise)); + } + + SendJSON(llvm::json::Object{ + {"type", "request"}, + {"seq", id}, + {"command", command}, + {"arguments", std::move(arguments)}, + }); + + return response_future; } void VSCode::RegisterRequestCallback(std::string request, @@ -610,4 +677,60 @@ return var_ref; } +bool StartDebuggingRequestHandler::DoExecute( + lldb::SBDebugger debugger, char **command, + lldb::SBCommandReturnObject &result) { + // Command format like: `startDebugging ` + if (!command) { + result.SetError("Invalid use of startDebugging"); + result.SetStatus(lldb::eReturnStatusFailed); + return false; + } + + if (!command[0] || llvm::StringRef(command[0]).empty()) { + result.SetError("startDebugging request type missing."); + result.SetStatus(lldb::eReturnStatusFailed); + return false; + } + + if (!command[1] || llvm::StringRef(command[1]).empty()) { + result.SetError("configuration missing."); + result.SetStatus(lldb::eReturnStatusFailed); + return false; + } + + llvm::StringRef request{command[0]}; + llvm::StringRef raw_configuration{command[1]}; + + llvm::Expected configuration = + llvm::json::parse(raw_configuration); + + if (!configuration) { + llvm::Error err = configuration.takeError(); + std::string msg = + "Failed to parse json configuration: " + llvm::toString(std::move(err)); + result.SetError(msg.c_str()); + result.SetStatus(lldb::eReturnStatusFailed); + return false; + } + + auto future = g_vsc.SendReverseRequest( + "startDebugging", + llvm::json::Object{{"request", request}, + {"configuration", std::move(*configuration)}}); + + future.wait(); + auto sent = future.get(); + if (!sent) { + llvm::Error err = configuration.takeError(); + std::string msg = "reverse start debugging request failed: " + + llvm::toString(std::move(err)); + result.SetError(msg.c_str()); + result.SetStatus(lldb::eReturnStatusFailed); + return false; + } + + return true; +} + } // namespace lldb_vscode diff --git a/lldb/tools/lldb-vscode/lldb-vscode.cpp b/lldb/tools/lldb-vscode/lldb-vscode.cpp --- a/lldb/tools/lldb-vscode/lldb-vscode.cpp +++ b/lldb/tools/lldb-vscode/lldb-vscode.cpp @@ -1471,6 +1471,13 @@ g_vsc.debugger = lldb::SBDebugger::Create(source_init_file, log_cb, nullptr); + auto cmd = g_vsc.debugger.GetCommandInterpreter().AddMultiwordCommand( + "lldb-vscode", nullptr); + cmd.AddCommand( + "startDebugging", &g_vsc.start_debugging_request_handler, + "Sends a startDebugging request from the debug adapter to the client to " + "start a child debug session of the same type as the caller."); + g_vsc.progress_event_thread = std::thread(ProgressEventThreadFunction); // Start our event thread so we can receive events from the debugger, target, @@ -1564,7 +1571,8 @@ g_vsc.SendJSON(llvm::json::Value(std::move(response))); } -llvm::Error request_runInTerminal(const llvm::json::Object &launch_request) { +llvm::Error request_runInTerminal(const llvm::json::Object &launch_request, + const uint64_t timeout_seconds) { g_vsc.is_attach = true; lldb::SBAttachInfo attach_info; @@ -1582,10 +1590,10 @@ #endif llvm::json::Object reverse_request = CreateRunInTerminalReverseRequest( launch_request, g_vsc.debug_adaptor_path, comm_file.m_path, debugger_pid); - llvm::json::Object reverse_response; - lldb_vscode::PacketStatus status = - g_vsc.SendReverseRequest(reverse_request, reverse_response); - if (status != lldb_vscode::PacketStatus::Success) + auto sent = + g_vsc.SendReverseRequest("runInTerminal", std::move(reverse_request)); + if (std::future_status::ready != + sent.wait_for(std::chrono::seconds(timeout_seconds))) return llvm::createStringError(llvm::inconvertibleErrorCode(), "Process cannot be launched by the IDE. %s", comm_channel.GetLauncherError().c_str()); @@ -1676,7 +1684,7 @@ const uint64_t timeout_seconds = GetUnsigned(arguments, "timeout", 30); if (GetBoolean(arguments, "runInTerminal", false)) { - if (llvm::Error err = request_runInTerminal(request)) + if (llvm::Error err = request_runInTerminal(request, timeout_seconds)) error.SetErrorString(llvm::toString(std::move(err)).c_str()); } else if (launchCommands.empty()) { // Disable async events so the launch will be successful when we return from @@ -3464,17 +3472,13 @@ g_vsc.output.descriptor = StreamDescriptor::from_file(new_stdout_fd, false); } - while (!g_vsc.sent_terminated_event) { - llvm::json::Object object; - lldb_vscode::PacketStatus status = g_vsc.GetNextObject(object); - if (status == lldb_vscode::PacketStatus::EndOfFile) - break; - if (status != lldb_vscode::PacketStatus::Success) - return 1; // Fatal error - - if (!g_vsc.HandleObject(object)) - return 1; + bool CleanExit = true; + if (auto Err = g_vsc.Loop()) { + if (g_vsc.log) + *g_vsc.log << "Transport Error: " << llvm::toString(std::move(Err)) + << "\n"; + CleanExit = false; } - return EXIT_SUCCESS; + return CleanExit ? EXIT_SUCCESS : EXIT_FAILURE; }