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 @@ -126,6 +126,7 @@ self.thread_stop_reasons = {} self.breakpoint_events = [] self.progress_events = [] + self.reverse_requests = [] self.sequence = 1 self.threads = None self.recv_thread.start() @@ -324,6 +325,7 @@ self.validate_response(command, response_or_request) return response_or_request else: + self.reverse_requests.append(response_or_request) if response_or_request["command"] == "runInTerminal": subprocess.Popen( response_or_request["arguments"]["args"], @@ -340,8 +342,20 @@ }, 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"' % ( + desc = 'unknown reverse request "%s"' % ( response_or_request["command"] ) raise ValueError(desc) diff --git a/lldb/test/API/tools/lldb-vscode/runInTerminal/TestVSCode_runInTerminal.py b/lldb/test/API/tools/lldb-vscode/runInTerminal/TestVSCode_runInTerminal.py --- a/lldb/test/API/tools/lldb-vscode/runInTerminal/TestVSCode_runInTerminal.py +++ b/lldb/test/API/tools/lldb-vscode/runInTerminal/TestVSCode_runInTerminal.py @@ -30,6 +30,7 @@ return file.readline() def isTestSupported(self): + return True # For some strange reason, this test fails on python3.6 if not (sys.version_info.major == 3 and sys.version_info.minor >= 7): return False @@ -45,7 +46,7 @@ @skipIfWindows @skipIfRemote - @skipIf(archs=no_match(["x86_64"])) + @skipIf(archs=no_match(["x86_64", "arm64"])) def test_runInTerminal(self): if not self.isTestSupported(): return @@ -59,6 +60,16 @@ program, runInTerminal=True, args=["foobar"], env=["FOO=bar"] ) + self.assertEqual( + len(self.vscode.reverse_requests), 1, "make sure we got a reverse request" + ) + + request = self.vscode.reverse_requests[0] + self.assertIn(self.lldbVSCodeExec, request["arguments"]["args"]) + self.assertIn(program, request["arguments"]["args"]) + self.assertIn("foobar", request["arguments"]["args"]) + self.assertIn("FOO", request["arguments"]["env"]) + breakpoint_line = line_number(source, "// breakpoint") self.set_source_breakpoints(source, [breakpoint_line]) @@ -81,7 +92,7 @@ @skipIfWindows @skipIfRemote - @skipIf(archs=no_match(["x86_64"])) + @skipIf(archs=no_match(["x86_64", "arm64"])) def test_runInTerminalInvalidTarget(self): if not self.isTestSupported(): return @@ -101,7 +112,7 @@ @skipIfWindows @skipIfRemote - @skipIf(archs=no_match(["x86_64"])) + @skipIf(archs=no_match(["x86_64", "arm64"])) def test_missingArgInRunInTerminalLauncher(self): if not self.isTestSupported(): return @@ -117,7 +128,7 @@ @skipIfWindows @skipIfRemote - @skipIf(archs=no_match(["x86_64"])) + @skipIf(archs=no_match(["x86_64", "arm64"])) def test_FakeAttachedRunInTerminalLauncherWithInvalidProgram(self): if not self.isTestSupported(): return @@ -145,7 +156,7 @@ @skipIfWindows @skipIfRemote - @skipIf(archs=no_match(["x86_64"])) + @skipIf(archs=no_match(["x86_64", "arm64"])) def test_FakeAttachedRunInTerminalLauncherWithValidProgram(self): if not self.isTestSupported(): return @@ -173,7 +184,7 @@ @skipIfWindows @skipIfRemote - @skipIf(archs=no_match(["x86_64"])) + @skipIf(archs=no_match(["x86_64", "arm64"])) def test_FakeAttachedRunInTerminalLauncherAndCheckEnvironment(self): if not self.isTestSupported(): return @@ -195,7 +206,7 @@ @skipIfWindows @skipIfRemote - @skipIf(archs=no_match(["x86_64"])) + @skipIf(archs=no_match(["x86_64", "arm64"])) def test_NonAttachedRunInTerminalLauncher(self): if not self.isTestSupported(): return 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,37 @@ +""" +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) + + breakpoint_line = line_number(source, "// breakpoint") + + self.set_source_breakpoints(source, [breakpoint_line]) + self.continue_to_next_stop() + self.vscode.request_evaluate( + '`lldb-vscode startDebugging attach \'{"pid":321}\'', context='repl' + ) + + self.assertEqual( + len(self.vscode.reverse_requests), 1, "make sure we got a reverse request" + ) + + request = self.vscode.reverse_requests[0] + self.assertEqual(request["arguments"]["configuration"]["pid"], 321) + self.assertEqual(request["arguments"]["request"], "attach") \ 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 @@ -73,6 +75,7 @@ }; typedef void (*RequestCallback)(const llvm::json::Object &command); +typedef void (*ResponseCallback)(llvm::Expected value); enum class PacketStatus { Success = 0, @@ -121,6 +124,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 +154,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 +162,18 @@ // 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 + inflight_reverse_requests; + StartDebuggingRequestHandler start_debugging_request_handler; + VSCode(); ~VSCode(); VSCode(const VSCode &rhs) = delete; @@ -224,19 +237,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. + void SendReverseRequest(llvm::StringRef command, llvm::json::Value arguments, + ResponseCallback callback); /// 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,130 @@ return false; // Fail } } + + if (packet_type == "response") { + auto id = GetSigned(object, "request_seq", 0); + ResponseCallback response_handler = [](llvm::Expected) { + llvm::errs() << "Unhandled response\n"; + }; + + { + std::lock_guard locker(call_mutex); + auto inflight = inflight_reverse_requests.find(id); + if (inflight != inflight_reverse_requests.end()) { + response_handler = std::move(inflight->second); + inflight_reverse_requests.erase(inflight); + } + } + + // 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_handler(Result); + } else { + llvm::StringRef message = GetString(object, "message"); + if (message.empty()) { + message = "Unknown error, response failed"; + } + response_handler(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(); +} + +// llvm::Error VSCode::Loop() { +// std::condition_variable cv; +// std::mutex mux; +// std::deque messages; +// std::thread enqueue_thread([&](){ +// 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) { +// if (log) +// *log << "failed to receive packet\n"; +// return; +// } + +// { +// std::unique_lock lock(mux); +// messages.emplace_back(std::move(object)); +// lock.unlock(); +// cv.notify_one(); +// } +// } +// }); + +// while (!sent_terminated_event) { +// llvm::json::Object object; +// { +// std::unique_lock lock(mux); +// cv.wait(lock, [&]{return !messages.empty();}); +// object = messages.front(); +// messages.pop_front(); +// } + +// if (!HandleObject(object)) { +// return llvm::createStringError(llvm::inconvertibleErrorCode(), +// "unhandled packet"); +// } +// } + +// enqueue_thread.join(); + +// return llvm::Error::success(); +// } + +void VSCode::SendReverseRequest(llvm::StringRef command, + llvm::json::Value arguments, + ResponseCallback callback) { + int64_t id; + { + std::lock_guard locker(call_mutex); + id = ++reverse_request_seq; + inflight_reverse_requests.emplace(id, std::move(callback)); + } + + SendJSON(llvm::json::Object{ + {"type", "request"}, + {"seq", id}, + {"command", command}, + {"arguments", std::move(arguments)}, + }); } void VSCode::RegisterRequestCallback(std::string request, @@ -610,4 +716,63 @@ 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]}; + std::string raw_configuration{command[1]}; + + int i = 2; + while (command[i]) { + raw_configuration.append(" ").append(command[i]); + } + + 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; + } + + g_vsc.SendReverseRequest( + "startDebugging", + llvm::json::Object{{"request", request}, + {"configuration", std::move(*configuration)}}, + [](llvm::Expected value) { + if (!value) { + llvm::Error err = value.takeError(); + llvm::errs() << "reverse start debugging request failed: " + << llvm::toString(std::move(err)) << "\n"; + } + }); + + result.SetStatus(lldb::eReturnStatusSuccessFinishNoResult); + + 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,13 +1590,15 @@ #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) - return llvm::createStringError(llvm::inconvertibleErrorCode(), - "Process cannot be launched by the IDE. %s", - comm_channel.GetLauncherError().c_str()); + g_vsc.SendReverseRequest("runInTerminal", std::move(reverse_request), + [](llvm::Expected value) { + if (!value) { + llvm::Error err = value.takeError(); + llvm::errs() + << "runInTerminal request failed: " + << llvm::toString(std::move(err)) << "\n"; + } + }); if (llvm::Expected pid = comm_channel.GetLauncherPid()) attach_info.SetProcessID(*pid); @@ -1676,7 +1686,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 +3474,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; }