Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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,
Expand Down
59 changes: 56 additions & 3 deletions lldb/test/API/tools/lldb-dap/server/TestDAP_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
import signal
import tempfile
import time

import dap_server
from lldbsuite.test.decorators import *
Expand All @@ -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,
)
Expand All @@ -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()
Expand Down Expand Up @@ -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")
9 changes: 9 additions & 0 deletions lldb/tools/lldb-dap/Options.td
Original file line number Diff line number Diff line change
Expand Up @@ -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.">;
19 changes: 16 additions & 3 deletions lldb/tools/lldb-dap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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/).
Expand Down
7 changes: 7 additions & 0 deletions lldb/tools/lldb-dap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 12 additions & 1 deletion lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
84 changes: 78 additions & 6 deletions lldb/tools/lldb-dap/tool/lldb-dap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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()) {
Expand All @@ -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;
Expand All @@ -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);

Expand Down Expand Up @@ -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();
});
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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;
Expand Down
Loading