diff --git a/lldb/test/Shell/DAP/TestClientLauncher.test b/lldb/test/Shell/DAP/TestClientLauncher.test new file mode 100644 index 0000000000000..a79a940da5a98 --- /dev/null +++ b/lldb/test/Shell/DAP/TestClientLauncher.test @@ -0,0 +1,2 @@ +# RUN: lldb-dap --client vscode-url -- /path/to/foo | FileCheck %s +# CHECK: vscode://llvm-vs-code-extensions.lldb-dap/start?program=%2Fpath%2Fto%2Ffoo diff --git a/lldb/tools/lldb-dap/CMakeLists.txt b/lldb/tools/lldb-dap/CMakeLists.txt index dd1bbbdddfc59..fa940b7b73943 100644 --- a/lldb/tools/lldb-dap/CMakeLists.txt +++ b/lldb/tools/lldb-dap/CMakeLists.txt @@ -5,6 +5,7 @@ set(LLVM_LINK_COMPONENTS Support) add_lldb_library(lldbDAP Breakpoint.cpp BreakpointBase.cpp + ClientLauncher.cpp CommandPlugins.cpp DAP.cpp DAPError.cpp diff --git a/lldb/tools/lldb-dap/ClientLauncher.cpp b/lldb/tools/lldb-dap/ClientLauncher.cpp new file mode 100644 index 0000000000000..4cac1d6346441 --- /dev/null +++ b/lldb/tools/lldb-dap/ClientLauncher.cpp @@ -0,0 +1,74 @@ +//===----------------------------------------------------------------------===// +// +// 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 "ClientLauncher.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringSwitch.h" +#include "llvm/Support/FormatVariadic.h" + +using namespace lldb_dap; + +std::optional +ClientLauncher::GetClientFrom(llvm::StringRef str) { + return llvm::StringSwitch>(str.lower()) + .Case("vscode", ClientLauncher::VSCode) + .Case("vscode-url", ClientLauncher::VSCodeURL) + .Default(std::nullopt); +} + +std::unique_ptr +ClientLauncher::GetLauncher(ClientLauncher::Client client) { + switch (client) { + case ClientLauncher::VSCode: + return std::make_unique(); + case ClientLauncher::VSCodeURL: + return std::make_unique(); + } + return nullptr; +} + +std::string VSCodeLauncher::URLEncode(llvm::StringRef str) { + std::string out; + llvm::raw_string_ostream os(out); + for (char c : str) { + if (std::isalnum(c) || llvm::StringRef("-_.~").contains(c)) + os << c; + else + os << '%' << llvm::utohexstr(c, false, 2); + } + return os.str(); +} + +std::string +VSCodeLauncher::GetLaunchURL(const std::vector args) const { + assert(!args.empty() && "empty launch args"); + + std::vector encoded_launch_args; + for (llvm::StringRef arg : args) + encoded_launch_args.push_back(URLEncode(arg)); + + const std::string args_str = llvm::join(encoded_launch_args, "&args="); + return llvm::formatv( + "vscode://llvm-vs-code-extensions.lldb-dap/start?program={0}", + args_str) + .str(); +} + +llvm::Error VSCodeLauncher::Launch(const std::vector args) { + const std::string launch_url = GetLaunchURL(args); + const std::string command = + llvm::formatv("code --open-url {0}", launch_url).str(); + + std::system(command.c_str()); + return llvm::Error::success(); +} + +llvm::Error VSCodeURLPrinter::Launch(const std::vector args) { + llvm::outs() << GetLaunchURL(args) << '\n'; + return llvm::Error::success(); +} diff --git a/lldb/tools/lldb-dap/ClientLauncher.h b/lldb/tools/lldb-dap/ClientLauncher.h new file mode 100644 index 0000000000000..780b178d2d6ef --- /dev/null +++ b/lldb/tools/lldb-dap/ClientLauncher.h @@ -0,0 +1,50 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_TOOLS_LLDB_DAP_CLIENTLAUNCHER_H +#define LLDB_TOOLS_LLDB_DAP_CLIENTLAUNCHER_H + +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" +#include + +namespace lldb_dap { + +class ClientLauncher { +public: + enum Client { + VSCode, + VSCodeURL, + }; + + virtual ~ClientLauncher() = default; + virtual llvm::Error Launch(const std::vector args) = 0; + + static std::optional GetClientFrom(llvm::StringRef str); + static std::unique_ptr GetLauncher(Client client); +}; + +class VSCodeLauncher : public ClientLauncher { +public: + using ClientLauncher::ClientLauncher; + + llvm::Error Launch(const std::vector args) override; + + std::string GetLaunchURL(const std::vector args) const; + static std::string URLEncode(llvm::StringRef str); +}; + +class VSCodeURLPrinter : public VSCodeLauncher { + using VSCodeLauncher::VSCodeLauncher; + + llvm::Error Launch(const std::vector args) override; +}; + +} // namespace lldb_dap + +#endif diff --git a/lldb/tools/lldb-dap/tool/Options.td b/lldb/tools/lldb-dap/tool/Options.td index 5e9dd7a1d6419..339a64fed6c32 100644 --- a/lldb/tools/lldb-dap/tool/Options.td +++ b/lldb/tools/lldb-dap/tool/Options.td @@ -82,3 +82,11 @@ def connection_timeout: S<"connection-timeout">, "timeout is reached, the server will be closed and the process will exit. " "Not specifying this argument or specifying non-positive values will " "cause the server to wait for new connections indefinitely.">; + +def client + : S<"client">, + MetaVarName<"">, + HelpText< + "Use lldb-dap as a launcher for a curated number of DAP client.">; + +def REM : R<["--"], "">; diff --git a/lldb/tools/lldb-dap/tool/lldb-dap.cpp b/lldb/tools/lldb-dap/tool/lldb-dap.cpp index 45caa1a81059b..f10ed12344cbd 100644 --- a/lldb/tools/lldb-dap/tool/lldb-dap.cpp +++ b/lldb/tools/lldb-dap/tool/lldb-dap.cpp @@ -6,6 +6,7 @@ // //===----------------------------------------------------------------------===// +#include "ClientLauncher.h" #include "DAP.h" #include "DAPLog.h" #include "EventHelper.h" @@ -141,6 +142,12 @@ static void PrintHelp(LLDBDAPOptTable &table, llvm::StringRef tool_name) { debugger to attach to the process. lldb-dap -g + + You can also use lldb-dap to launch a supported client, for example the + LLDB-DAP Visual Studio Code extension. + + lldb-dap --client vscode -- /path/to/binary + )___"; } @@ -150,6 +157,29 @@ static void PrintVersion() { llvm::outs() << "liblldb: " << lldb::SBDebugger::GetVersionString() << '\n'; } +static llvm::Error LaunchClient(const llvm::opt::InputArgList &args) { + auto *client_arg = args.getLastArg(OPT_client); + assert(client_arg && "must have client arg"); + + std::optional client = + ClientLauncher::GetClientFrom(client_arg->getValue()); + if (!client) + return llvm::createStringError( + llvm::formatv("unsupported client: {0}", client_arg->getValue())); + + std::vector launch_args; + if (auto *arg = args.getLastArgNoClaim(OPT_REM)) { + for (auto *value : arg->getValues()) { + launch_args.push_back(value); + } + } + + if (launch_args.empty()) + return llvm::createStringError("no launch arguments provided"); + + return ClientLauncher::GetLauncher(*client)->Launch(launch_args); +} + #if not defined(_WIN32) struct FDGroup { int GetFlags() const { @@ -541,6 +571,14 @@ int main(int argc, char *argv[]) { return EXIT_SUCCESS; } + if (input_args.hasArg(OPT_client)) { + if (llvm::Error error = LaunchClient(input_args)) { + llvm::WithColor::error() << llvm::toString(std::move(error)) << '\n'; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; + } + ReplMode default_repl_mode = ReplMode::Auto; if (input_args.hasArg(OPT_repl_mode)) { llvm::opt::Arg *repl_mode = input_args.getLastArg(OPT_repl_mode); diff --git a/lldb/unittests/DAP/CMakeLists.txt b/lldb/unittests/DAP/CMakeLists.txt index a08414c30e6cd..b1fdef18fddba 100644 --- a/lldb/unittests/DAP/CMakeLists.txt +++ b/lldb/unittests/DAP/CMakeLists.txt @@ -1,4 +1,5 @@ add_lldb_unittest(DAPTests + ClientLauncherTest.cpp DAPErrorTest.cpp DAPTest.cpp DAPTypesTest.cpp diff --git a/lldb/unittests/DAP/ClientLauncherTest.cpp b/lldb/unittests/DAP/ClientLauncherTest.cpp new file mode 100644 index 0000000000000..dbaf9ee786336 --- /dev/null +++ b/lldb/unittests/DAP/ClientLauncherTest.cpp @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// +// 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 "ClientLauncher.h" +#include "llvm/ADT/StringRef.h" +#include "gtest/gtest.h" +#include + +using namespace lldb_dap; +using namespace llvm; + +TEST(ClientLauncherTest, GetClientFromVSCode) { + std::optional result = + ClientLauncher::GetClientFrom("vscode"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(ClientLauncher::VSCode, result.value()); +} + +TEST(ClientLauncherTest, GetClientFromVSCodeUpperCase) { + std::optional result = + ClientLauncher::GetClientFrom("VSCODE"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(ClientLauncher::VSCode, result.value()); +} + +TEST(ClientLauncherTest, GetClientFromVSCodeMixedCase) { + std::optional result = + ClientLauncher::GetClientFrom("VSCode"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(ClientLauncher::VSCode, result.value()); +} + +TEST(ClientLauncherTest, GetClientFromInvalidString) { + std::optional result = + ClientLauncher::GetClientFrom("invalid"); + EXPECT_FALSE(result.has_value()); +} + +TEST(ClientLauncherTest, GetClientFromEmptyString) { + std::optional result = + ClientLauncher::GetClientFrom(""); + EXPECT_FALSE(result.has_value()); +} + +TEST(ClientLauncherTest, URLEncode) { + EXPECT_EQ("", VSCodeLauncher::URLEncode("")); + EXPECT_EQ( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~", + VSCodeLauncher::URLEncode("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRST" + "UVWXYZ0123456789-_.~")); + EXPECT_EQ("hello%20world", VSCodeLauncher::URLEncode("hello world")); + EXPECT_EQ("hello%21%40%23%24", VSCodeLauncher::URLEncode("hello!@#$")); + EXPECT_EQ("%2Fpath%2Fto%2Ffile", VSCodeLauncher::URLEncode("/path/to/file")); + EXPECT_EQ("key%3Dvalue%26key2%3Dvalue2", + VSCodeLauncher::URLEncode("key=value&key2=value2")); + EXPECT_EQ("100%25complete", VSCodeLauncher::URLEncode("100%complete")); + EXPECT_EQ("file_name%20with%20spaces%20%26%20special%21.txt", + VSCodeLauncher::URLEncode("file_name with spaces & special!.txt")); + EXPECT_EQ("%00%01%02", + VSCodeLauncher::URLEncode(llvm::StringRef("\x00\x01\x02", 3))); + EXPECT_EQ("test-file_name.txt~", + VSCodeLauncher::URLEncode("test-file_name.txt~")); + + // UTF-8 encoded characters should be percent-encoded byte by byte. + EXPECT_EQ("%C3%A9", VSCodeLauncher::URLEncode("é")); +}