Author: Roy Shi Date: 2025-09-10T08:21:12-07:00 New Revision: acb38c8be7152aa26e6958752499619eeeeddd1c
URL: https://github.com/llvm/llvm-project/commit/acb38c8be7152aa26e6958752499619eeeeddd1c DIFF: https://github.com/llvm/llvm-project/commit/acb38c8be7152aa26e6958752499619eeeeddd1c.diff LOG: [lldb-dap] Add command line option `--connection-timeout` (#156803) # Usage This is an optional new command line option to use with `--connection`. ``` --connection-timeout <timeout> When using --connection, the number of seconds to wait for new connections after the server has started and after all clients have disconnected. Each new connection will reset the timeout. When the 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. ``` A corresponding extension setting `Connection Timeout` is added to the `lldb-dap` VS Code extension. # Benefits Automatic release of resources when lldb-dap is no longer being used (e.g. release memory used by module cache). # Test See PR. Added: Modified: lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py lldb/test/API/tools/lldb-dap/server/TestDAP_server.py lldb/tools/lldb-dap/Options.td lldb/tools/lldb-dap/README.md lldb/tools/lldb-dap/package.json lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts lldb/tools/lldb-dap/tool/lldb-dap.cpp Removed: ################################################################################ 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 66aa070a537e0..51debcf477a9d 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 @@ -1533,6 +1533,7 @@ def launch( env: Optional[dict[str, str]] = None, log_file: Optional[TextIO] = None, connection: Optional[str] = None, + connection_timeout: Optional[int] = None, additional_args: list[str] = [], ) -> tuple[subprocess.Popen, Optional[str]]: adapter_env = os.environ.copy() @@ -1550,6 +1551,10 @@ def launch( args.append("--connection") args.append(connection) + if connection_timeout is not None: + args.append("--connection-timeout") + args.append(str(connection_timeout)) + process = subprocess.Popen( args, stdin=subprocess.PIPE, diff --git a/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py b/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py index e01320c25b155..2ab6c7ed24710 100644 --- a/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py +++ b/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py @@ -5,6 +5,7 @@ import os import signal import tempfile +import time import dap_server from lldbsuite.test.decorators import * @@ -13,22 +14,28 @@ class TestDAP_server(lldbdap_testcase.DAPTestCaseBase): - def start_server(self, connection): + def start_server( + self, connection, connection_timeout=None, wait_seconds_for_termination=None + ): log_file_path = self.getBuildArtifact("dap.txt") (process, connection) = dap_server.DebugAdapterServer.launch( executable=self.lldbDAPExec, connection=connection, + connection_timeout=connection_timeout, log_file=log_file_path, ) def cleanup(): - process.terminate() + if wait_seconds_for_termination is not None: + process.wait(wait_seconds_for_termination) + else: + process.terminate() self.addTearDownHook(cleanup) return (process, connection) - def run_debug_session(self, connection, name): + def run_debug_session(self, connection, name, sleep_seconds_in_middle=None): self.dap_server = dap_server.DebugAdapterServer( connection=connection, ) @@ -41,6 +48,8 @@ def run_debug_session(self, connection, name): args=[name], disconnectAutomatically=False, ) + if sleep_seconds_in_middle is not None: + time.sleep(sleep_seconds_in_middle) self.set_source_breakpoints(source, [breakpoint_line]) self.continue_to_next_stop() self.continue_to_exit() @@ -108,3 +117,47 @@ def test_server_interrupt(self): self.dap_server.exit_status, "Process exited before interrupting lldb-dap server", ) + + @skipIfWindows + def test_connection_timeout_at_server_start(self): + """ + Test launching lldb-dap in server mode with connection timeout and waiting for it to terminate automatically when no client connects. + """ + self.build() + self.start_server( + connection="listen://localhost:0", + connection_timeout=1, + wait_seconds_for_termination=2, + ) + + @skipIfWindows + def test_connection_timeout_long_debug_session(self): + """ + Test launching lldb-dap in server mode with connection timeout and terminating the server after the a long debug session. + """ + self.build() + (_, connection) = self.start_server( + connection="listen://localhost:0", + connection_timeout=1, + wait_seconds_for_termination=2, + ) + # The connection timeout should not cut off the debug session + self.run_debug_session(connection, "Alice", 1.5) + + @skipIfWindows + def test_connection_timeout_multiple_sessions(self): + """ + Test launching lldb-dap in server mode with connection timeout and terminating the server after the last debug session. + """ + self.build() + (_, connection) = self.start_server( + connection="listen://localhost:0", + connection_timeout=1, + wait_seconds_for_termination=2, + ) + time.sleep(0.5) + # Should be able to connect to the server. + self.run_debug_session(connection, "Alice") + time.sleep(0.5) + # Should be able to connect to the server, because it's still within the connection timeout. + self.run_debug_session(connection, "Bob") diff --git a/lldb/tools/lldb-dap/Options.td b/lldb/tools/lldb-dap/Options.td index b3fb592c84a8e..c8492c6a62b25 100644 --- a/lldb/tools/lldb-dap/Options.td +++ b/lldb/tools/lldb-dap/Options.td @@ -67,3 +67,12 @@ def no_lldbinit: F<"no-lldbinit">, def: Flag<["-"], "x">, Alias<no_lldbinit>, HelpText<"Alias for --no-lldbinit">; + +def connection_timeout: S<"connection-timeout">, + MetaVarName<"<timeout>">, + HelpText<"When using --connection, the number of seconds to wait for new " + "connections after the server has started and after all clients have " + "disconnected. Each new connection will reset the timeout. When the " + "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.">; diff --git a/lldb/tools/lldb-dap/README.md b/lldb/tools/lldb-dap/README.md index f88f3ced6f25f..39dabcc1342c8 100644 --- a/lldb/tools/lldb-dap/README.md +++ b/lldb/tools/lldb-dap/README.md @@ -275,9 +275,9 @@ User settings can set the default value for the following supported | **exitCommands** | [string] | `[]` | | **terminateCommands** | [string] | `[]` | -To adjust your settings, open the Settings editor via the -`File > Preferences > Settings` menu or press `Ctrl+`, on Windows/Linux and -`Cmd+`, on Mac. +To adjust your settings, open the Settings editor +via the `File > Preferences > Settings` menu or press `Ctrl+,` on Windows/Linux, +and the `VS Code > Settings... > Settings` menu or press `Cmd+,` on Mac. ## Debug Console @@ -372,6 +372,19 @@ for more details on Debug Adapter Protocol events and the VS Code [debug.onDidReceiveDebugSessionCustomEvent](https://code.visualstudio.com/api/references/vscode-api#debug.onDidReceiveDebugSessionCustomEvent) API for handling a custom event from an extension. +## Server Mode + +lldb-dap supports a server mode that can be enabled via the following user settings. + +| Setting | Type | Default | | +| -------------------------- | -------- | :-----: | --------- | +| **Server Mode** | string | `False` | Run lldb-dap in server mode. When enabled, lldb-dap will start a background server that will be reused between debug sessions. This allows caching of debug symbols between sessions and improves launch performance. +| **Connection Timeout** | number | `0` | When running lldb-dap in server mode, the time in seconds to wait for new connections after the server has started and after all clients have disconnected. Each new connection will reset the timeout. When the timeout is reached, the server will be closed and the process will exit. Specifying non-positive values will cause the server to wait for new connections indefinitely. + +To adjust your settings, open the Settings editor +via the `File > Preferences > Settings` menu or press `Ctrl+,` on Windows/Linux, +and the `VS Code > Settings... > Settings` menu or press `Cmd+,` on Mac. + ## Contributing `lldb-dap` and `lldb` are developed under the umbrella of the [LLVM project](https://llvm.org/). diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json index 8c6c1b4ae6ebb..9cc653cee405b 100644 --- a/lldb/tools/lldb-dap/package.json +++ b/lldb/tools/lldb-dap/package.json @@ -106,6 +106,13 @@ "markdownDescription": "Run lldb-dap in server mode.\n\nWhen enabled, lldb-dap will start a background server that will be reused between debug sessions. This allows caching of debug symbols between sessions and improves launch performance.", "default": false }, + "lldb-dap.connectionTimeout": { + "order": 0, + "scope": "resource", + "type": "number", + "markdownDescription": "When running lldb-dap in server mode, the time in seconds to wait for new connections after the server has started and after all clients have disconnected. Each new connection will reset the timeout. When the timeout is reached, the server will be closed and the process will exit. Specifying non-positive values will cause the server to wait for new connections indefinitely.", + "default": 0 + }, "lldb-dap.arguments": { "scope": "resource", "type": "array", diff --git a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts index 1ae87116141f1..d35460ab68f00 100644 --- a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts +++ b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts @@ -207,10 +207,15 @@ export class LLDBDapConfigurationProvider config.get<boolean>("serverMode", false) && (await isServerModeSupported(executable.command)) ) { + const connectionTimeoutSeconds = config.get<number | undefined>( + "connectionTimeout", + undefined, + ); const serverInfo = await this.server.start( executable.command, executable.args, executable.options, + connectionTimeoutSeconds, ); if (!serverInfo) { return undefined; diff --git a/lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts b/lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts index 300b12d1cce1b..774be50053a17 100644 --- a/lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts +++ b/lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts @@ -33,8 +33,19 @@ export class LLDBDapServer implements vscode.Disposable { dapPath: string, args: string[], options?: child_process.SpawnOptionsWithoutStdio, + connectionTimeoutSeconds?: number, ): Promise<{ host: string; port: number } | undefined> { - const dapArgs = [...args, "--connection", "listen://localhost:0"]; + // Both the --connection and --connection-timeout arguments are subject to the shouldContinueStartup() check. + const connectionTimeoutArgs = + connectionTimeoutSeconds && connectionTimeoutSeconds > 0 + ? ["--connection-timeout", `${connectionTimeoutSeconds}`] + : []; + const dapArgs = [ + ...args, + "--connection", + "listen://localhost:0", + ...connectionTimeoutArgs, + ]; if (!(await this.shouldContinueStartup(dapPath, dapArgs, options?.env))) { return undefined; } diff --git a/lldb/tools/lldb-dap/tool/lldb-dap.cpp b/lldb/tools/lldb-dap/tool/lldb-dap.cpp index 2431cf8fb2f24..93446c051eb54 100644 --- a/lldb/tools/lldb-dap/tool/lldb-dap.cpp +++ b/lldb/tools/lldb-dap/tool/lldb-dap.cpp @@ -223,6 +223,35 @@ static int DuplicateFileDescriptor(int fd) { #endif } +static void +ResetConnectionTimeout(std::mutex &connection_timeout_mutex, + MainLoopBase::TimePoint &conncetion_timeout_time_point) { + std::scoped_lock<std::mutex> lock(connection_timeout_mutex); + conncetion_timeout_time_point = MainLoopBase::TimePoint(); +} + +static void +TrackConnectionTimeout(MainLoop &loop, std::mutex &connection_timeout_mutex, + MainLoopBase::TimePoint &conncetion_timeout_time_point, + std::chrono::seconds ttl_seconds) { + MainLoopBase::TimePoint next_checkpoint = + std::chrono::steady_clock::now() + std::chrono::seconds(ttl_seconds); + { + std::scoped_lock<std::mutex> lock(connection_timeout_mutex); + // We don't need to take the max of `ttl_time_point` and `next_checkpoint`, + // because `next_checkpoint` must be the latest. + conncetion_timeout_time_point = next_checkpoint; + } + loop.AddCallback( + [&connection_timeout_mutex, &conncetion_timeout_time_point, + next_checkpoint](MainLoopBase &loop) { + std::scoped_lock<std::mutex> lock(connection_timeout_mutex); + if (conncetion_timeout_time_point == next_checkpoint) + loop.RequestTermination(); + }, + next_checkpoint); +} + static llvm::Expected<std::pair<Socket::SocketProtocol, std::string>> validateConnection(llvm::StringRef conn) { auto uri = lldb_private::URI::Parse(conn); @@ -255,11 +284,11 @@ validateConnection(llvm::StringRef conn) { return make_error(); } -static llvm::Error -serveConnection(const Socket::SocketProtocol &protocol, const std::string &name, - Log *log, const ReplMode default_repl_mode, - const std::vector<std::string> &pre_init_commands, - bool no_lldbinit) { +static llvm::Error serveConnection( + const Socket::SocketProtocol &protocol, const std::string &name, Log *log, + const ReplMode default_repl_mode, + const std::vector<std::string> &pre_init_commands, bool no_lldbinit, + std::optional<std::chrono::seconds> connection_timeout_seconds) { Status status; static std::unique_ptr<Socket> listener = Socket::Create(protocol, status); if (status.Fail()) { @@ -284,6 +313,12 @@ serveConnection(const Socket::SocketProtocol &protocol, const std::string &name, g_loop.AddPendingCallback( [](MainLoopBase &loop) { loop.RequestTermination(); }); }); + static MainLoopBase::TimePoint g_connection_timeout_time_point; + static std::mutex g_connection_timeout_mutex; + if (connection_timeout_seconds) + TrackConnectionTimeout(g_loop, g_connection_timeout_mutex, + g_connection_timeout_time_point, + connection_timeout_seconds.value()); std::condition_variable dap_sessions_condition; std::mutex dap_sessions_mutex; std::map<MainLoop *, DAP *> dap_sessions; @@ -292,6 +327,11 @@ serveConnection(const Socket::SocketProtocol &protocol, const std::string &name, &dap_sessions_mutex, &dap_sessions, &clientCount]( std::unique_ptr<Socket> sock) { + // Reset the keep alive timer, because we won't be killing the server + // while this connection is being served. + if (connection_timeout_seconds) + ResetConnectionTimeout(g_connection_timeout_mutex, + g_connection_timeout_time_point); std::string client_name = llvm::formatv("client_{0}", clientCount++).str(); DAP_LOG(log, "({0}) client connected", client_name); @@ -328,6 +368,12 @@ serveConnection(const Socket::SocketProtocol &protocol, const std::string &name, std::unique_lock<std::mutex> lock(dap_sessions_mutex); dap_sessions.erase(&loop); std::notify_all_at_thread_exit(dap_sessions_condition, std::move(lock)); + + // Start the countdown to kill the server at the end of each connection. + if (connection_timeout_seconds) + TrackConnectionTimeout(g_loop, g_connection_timeout_mutex, + g_connection_timeout_time_point, + connection_timeout_seconds.value()); }); client.detach(); }); @@ -457,6 +503,31 @@ int main(int argc, char *argv[]) { connection.assign(path); } + std::optional<std::chrono::seconds> connection_timeout_seconds; + if (llvm::opt::Arg *connection_timeout_arg = + input_args.getLastArg(OPT_connection_timeout)) { + if (!connection.empty()) { + llvm::StringRef connection_timeout_string_value = + connection_timeout_arg->getValue(); + int connection_timeout_int_value; + if (connection_timeout_string_value.getAsInteger( + 10, connection_timeout_int_value)) { + llvm::errs() << "'" << connection_timeout_string_value + << "' is not a valid connection timeout value\n"; + return EXIT_FAILURE; + } + // Ignore non-positive values. + if (connection_timeout_int_value > 0) + connection_timeout_seconds = + std::chrono::seconds(connection_timeout_int_value); + } else { + llvm::errs() + << "\"--connection-timeout\" requires \"--connection\" to be " + "specified\n"; + return EXIT_FAILURE; + } + } + #if !defined(_WIN32) if (input_args.hasArg(OPT_wait_for_debugger)) { printf("Paused waiting for debugger to attach (pid = %i)...\n", getpid()); @@ -523,7 +594,8 @@ int main(int argc, char *argv[]) { std::string name; std::tie(protocol, name) = *maybeProtoclAndName; if (auto Err = serveConnection(protocol, name, log.get(), default_repl_mode, - pre_init_commands, no_lldbinit)) { + pre_init_commands, no_lldbinit, + connection_timeout_seconds)) { llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), "Connection failed: "); return EXIT_FAILURE; _______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits