Skip to content

Commit acb38c8

Browse files
authored
[lldb-dap] Add command line option --connection-timeout (llvm#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.
1 parent 7d36727 commit acb38c8

File tree

8 files changed

+188
-13
lines changed

8 files changed

+188
-13
lines changed

lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1533,6 +1533,7 @@ def launch(
15331533
env: Optional[dict[str, str]] = None,
15341534
log_file: Optional[TextIO] = None,
15351535
connection: Optional[str] = None,
1536+
connection_timeout: Optional[int] = None,
15361537
additional_args: list[str] = [],
15371538
) -> tuple[subprocess.Popen, Optional[str]]:
15381539
adapter_env = os.environ.copy()
@@ -1550,6 +1551,10 @@ def launch(
15501551
args.append("--connection")
15511552
args.append(connection)
15521553

1554+
if connection_timeout is not None:
1555+
args.append("--connection-timeout")
1556+
args.append(str(connection_timeout))
1557+
15531558
process = subprocess.Popen(
15541559
args,
15551560
stdin=subprocess.PIPE,

lldb/test/API/tools/lldb-dap/server/TestDAP_server.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import signal
77
import tempfile
8+
import time
89

910
import dap_server
1011
from lldbsuite.test.decorators import *
@@ -13,22 +14,28 @@
1314

1415

1516
class TestDAP_server(lldbdap_testcase.DAPTestCaseBase):
16-
def start_server(self, connection):
17+
def start_server(
18+
self, connection, connection_timeout=None, wait_seconds_for_termination=None
19+
):
1720
log_file_path = self.getBuildArtifact("dap.txt")
1821
(process, connection) = dap_server.DebugAdapterServer.launch(
1922
executable=self.lldbDAPExec,
2023
connection=connection,
24+
connection_timeout=connection_timeout,
2125
log_file=log_file_path,
2226
)
2327

2428
def cleanup():
25-
process.terminate()
29+
if wait_seconds_for_termination is not None:
30+
process.wait(wait_seconds_for_termination)
31+
else:
32+
process.terminate()
2633

2734
self.addTearDownHook(cleanup)
2835

2936
return (process, connection)
3037

31-
def run_debug_session(self, connection, name):
38+
def run_debug_session(self, connection, name, sleep_seconds_in_middle=None):
3239
self.dap_server = dap_server.DebugAdapterServer(
3340
connection=connection,
3441
)
@@ -41,6 +48,8 @@ def run_debug_session(self, connection, name):
4148
args=[name],
4249
disconnectAutomatically=False,
4350
)
51+
if sleep_seconds_in_middle is not None:
52+
time.sleep(sleep_seconds_in_middle)
4453
self.set_source_breakpoints(source, [breakpoint_line])
4554
self.continue_to_next_stop()
4655
self.continue_to_exit()
@@ -108,3 +117,47 @@ def test_server_interrupt(self):
108117
self.dap_server.exit_status,
109118
"Process exited before interrupting lldb-dap server",
110119
)
120+
121+
@skipIfWindows
122+
def test_connection_timeout_at_server_start(self):
123+
"""
124+
Test launching lldb-dap in server mode with connection timeout and waiting for it to terminate automatically when no client connects.
125+
"""
126+
self.build()
127+
self.start_server(
128+
connection="listen://localhost:0",
129+
connection_timeout=1,
130+
wait_seconds_for_termination=2,
131+
)
132+
133+
@skipIfWindows
134+
def test_connection_timeout_long_debug_session(self):
135+
"""
136+
Test launching lldb-dap in server mode with connection timeout and terminating the server after the a long debug session.
137+
"""
138+
self.build()
139+
(_, connection) = self.start_server(
140+
connection="listen://localhost:0",
141+
connection_timeout=1,
142+
wait_seconds_for_termination=2,
143+
)
144+
# The connection timeout should not cut off the debug session
145+
self.run_debug_session(connection, "Alice", 1.5)
146+
147+
@skipIfWindows
148+
def test_connection_timeout_multiple_sessions(self):
149+
"""
150+
Test launching lldb-dap in server mode with connection timeout and terminating the server after the last debug session.
151+
"""
152+
self.build()
153+
(_, connection) = self.start_server(
154+
connection="listen://localhost:0",
155+
connection_timeout=1,
156+
wait_seconds_for_termination=2,
157+
)
158+
time.sleep(0.5)
159+
# Should be able to connect to the server.
160+
self.run_debug_session(connection, "Alice")
161+
time.sleep(0.5)
162+
# Should be able to connect to the server, because it's still within the connection timeout.
163+
self.run_debug_session(connection, "Bob")

lldb/tools/lldb-dap/Options.td

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,12 @@ def no_lldbinit: F<"no-lldbinit">,
6767
def: Flag<["-"], "x">,
6868
Alias<no_lldbinit>,
6969
HelpText<"Alias for --no-lldbinit">;
70+
71+
def connection_timeout: S<"connection-timeout">,
72+
MetaVarName<"<timeout>">,
73+
HelpText<"When using --connection, the number of seconds to wait for new "
74+
"connections after the server has started and after all clients have "
75+
"disconnected. Each new connection will reset the timeout. When the "
76+
"timeout is reached, the server will be closed and the process will exit. "
77+
"Not specifying this argument or specifying non-positive values will "
78+
"cause the server to wait for new connections indefinitely.">;

lldb/tools/lldb-dap/README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,9 @@ User settings can set the default value for the following supported
275275
| **exitCommands** | [string] | `[]` |
276276
| **terminateCommands** | [string] | `[]` |
277277

278-
To adjust your settings, open the Settings editor via the
279-
`File > Preferences > Settings` menu or press `Ctrl+`, on Windows/Linux and
280-
`Cmd+`, on Mac.
278+
To adjust your settings, open the Settings editor
279+
via the `File > Preferences > Settings` menu or press `Ctrl+,` on Windows/Linux,
280+
and the `VS Code > Settings... > Settings` menu or press `Cmd+,` on Mac.
281281

282282
## Debug Console
283283

@@ -372,6 +372,19 @@ for more details on Debug Adapter Protocol events and the VS Code
372372
[debug.onDidReceiveDebugSessionCustomEvent](https://code.visualstudio.com/api/references/vscode-api#debug.onDidReceiveDebugSessionCustomEvent)
373373
API for handling a custom event from an extension.
374374

375+
## Server Mode
376+
377+
lldb-dap supports a server mode that can be enabled via the following user settings.
378+
379+
| Setting | Type | Default | |
380+
| -------------------------- | -------- | :-----: | --------- |
381+
| **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.
382+
| **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.
383+
384+
To adjust your settings, open the Settings editor
385+
via the `File > Preferences > Settings` menu or press `Ctrl+,` on Windows/Linux,
386+
and the `VS Code > Settings... > Settings` menu or press `Cmd+,` on Mac.
387+
375388
## Contributing
376389

377390
`lldb-dap` and `lldb` are developed under the umbrella of the [LLVM project](https://llvm.org/).

lldb/tools/lldb-dap/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@
106106
"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.",
107107
"default": false
108108
},
109+
"lldb-dap.connectionTimeout": {
110+
"order": 0,
111+
"scope": "resource",
112+
"type": "number",
113+
"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.",
114+
"default": 0
115+
},
109116
"lldb-dap.arguments": {
110117
"scope": "resource",
111118
"type": "array",

lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,15 @@ export class LLDBDapConfigurationProvider
207207
config.get<boolean>("serverMode", false) &&
208208
(await isServerModeSupported(executable.command))
209209
) {
210+
const connectionTimeoutSeconds = config.get<number | undefined>(
211+
"connectionTimeout",
212+
undefined,
213+
);
210214
const serverInfo = await this.server.start(
211215
executable.command,
212216
executable.args,
213217
executable.options,
218+
connectionTimeoutSeconds,
214219
);
215220
if (!serverInfo) {
216221
return undefined;

lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,19 @@ export class LLDBDapServer implements vscode.Disposable {
3333
dapPath: string,
3434
args: string[],
3535
options?: child_process.SpawnOptionsWithoutStdio,
36+
connectionTimeoutSeconds?: number,
3637
): Promise<{ host: string; port: number } | undefined> {
37-
const dapArgs = [...args, "--connection", "listen://localhost:0"];
38+
// Both the --connection and --connection-timeout arguments are subject to the shouldContinueStartup() check.
39+
const connectionTimeoutArgs =
40+
connectionTimeoutSeconds && connectionTimeoutSeconds > 0
41+
? ["--connection-timeout", `${connectionTimeoutSeconds}`]
42+
: [];
43+
const dapArgs = [
44+
...args,
45+
"--connection",
46+
"listen://localhost:0",
47+
...connectionTimeoutArgs,
48+
];
3849
if (!(await this.shouldContinueStartup(dapPath, dapArgs, options?.env))) {
3950
return undefined;
4051
}

lldb/tools/lldb-dap/tool/lldb-dap.cpp

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,35 @@ static int DuplicateFileDescriptor(int fd) {
223223
#endif
224224
}
225225

226+
static void
227+
ResetConnectionTimeout(std::mutex &connection_timeout_mutex,
228+
MainLoopBase::TimePoint &conncetion_timeout_time_point) {
229+
std::scoped_lock<std::mutex> lock(connection_timeout_mutex);
230+
conncetion_timeout_time_point = MainLoopBase::TimePoint();
231+
}
232+
233+
static void
234+
TrackConnectionTimeout(MainLoop &loop, std::mutex &connection_timeout_mutex,
235+
MainLoopBase::TimePoint &conncetion_timeout_time_point,
236+
std::chrono::seconds ttl_seconds) {
237+
MainLoopBase::TimePoint next_checkpoint =
238+
std::chrono::steady_clock::now() + std::chrono::seconds(ttl_seconds);
239+
{
240+
std::scoped_lock<std::mutex> lock(connection_timeout_mutex);
241+
// We don't need to take the max of `ttl_time_point` and `next_checkpoint`,
242+
// because `next_checkpoint` must be the latest.
243+
conncetion_timeout_time_point = next_checkpoint;
244+
}
245+
loop.AddCallback(
246+
[&connection_timeout_mutex, &conncetion_timeout_time_point,
247+
next_checkpoint](MainLoopBase &loop) {
248+
std::scoped_lock<std::mutex> lock(connection_timeout_mutex);
249+
if (conncetion_timeout_time_point == next_checkpoint)
250+
loop.RequestTermination();
251+
},
252+
next_checkpoint);
253+
}
254+
226255
static llvm::Expected<std::pair<Socket::SocketProtocol, std::string>>
227256
validateConnection(llvm::StringRef conn) {
228257
auto uri = lldb_private::URI::Parse(conn);
@@ -255,11 +284,11 @@ validateConnection(llvm::StringRef conn) {
255284
return make_error();
256285
}
257286

258-
static llvm::Error
259-
serveConnection(const Socket::SocketProtocol &protocol, const std::string &name,
260-
Log *log, const ReplMode default_repl_mode,
261-
const std::vector<std::string> &pre_init_commands,
262-
bool no_lldbinit) {
287+
static llvm::Error serveConnection(
288+
const Socket::SocketProtocol &protocol, const std::string &name, Log *log,
289+
const ReplMode default_repl_mode,
290+
const std::vector<std::string> &pre_init_commands, bool no_lldbinit,
291+
std::optional<std::chrono::seconds> connection_timeout_seconds) {
263292
Status status;
264293
static std::unique_ptr<Socket> listener = Socket::Create(protocol, status);
265294
if (status.Fail()) {
@@ -284,6 +313,12 @@ serveConnection(const Socket::SocketProtocol &protocol, const std::string &name,
284313
g_loop.AddPendingCallback(
285314
[](MainLoopBase &loop) { loop.RequestTermination(); });
286315
});
316+
static MainLoopBase::TimePoint g_connection_timeout_time_point;
317+
static std::mutex g_connection_timeout_mutex;
318+
if (connection_timeout_seconds)
319+
TrackConnectionTimeout(g_loop, g_connection_timeout_mutex,
320+
g_connection_timeout_time_point,
321+
connection_timeout_seconds.value());
287322
std::condition_variable dap_sessions_condition;
288323
std::mutex dap_sessions_mutex;
289324
std::map<MainLoop *, DAP *> dap_sessions;
@@ -292,6 +327,11 @@ serveConnection(const Socket::SocketProtocol &protocol, const std::string &name,
292327
&dap_sessions_mutex, &dap_sessions,
293328
&clientCount](
294329
std::unique_ptr<Socket> sock) {
330+
// Reset the keep alive timer, because we won't be killing the server
331+
// while this connection is being served.
332+
if (connection_timeout_seconds)
333+
ResetConnectionTimeout(g_connection_timeout_mutex,
334+
g_connection_timeout_time_point);
295335
std::string client_name = llvm::formatv("client_{0}", clientCount++).str();
296336
DAP_LOG(log, "({0}) client connected", client_name);
297337

@@ -328,6 +368,12 @@ serveConnection(const Socket::SocketProtocol &protocol, const std::string &name,
328368
std::unique_lock<std::mutex> lock(dap_sessions_mutex);
329369
dap_sessions.erase(&loop);
330370
std::notify_all_at_thread_exit(dap_sessions_condition, std::move(lock));
371+
372+
// Start the countdown to kill the server at the end of each connection.
373+
if (connection_timeout_seconds)
374+
TrackConnectionTimeout(g_loop, g_connection_timeout_mutex,
375+
g_connection_timeout_time_point,
376+
connection_timeout_seconds.value());
331377
});
332378
client.detach();
333379
});
@@ -457,6 +503,31 @@ int main(int argc, char *argv[]) {
457503
connection.assign(path);
458504
}
459505

506+
std::optional<std::chrono::seconds> connection_timeout_seconds;
507+
if (llvm::opt::Arg *connection_timeout_arg =
508+
input_args.getLastArg(OPT_connection_timeout)) {
509+
if (!connection.empty()) {
510+
llvm::StringRef connection_timeout_string_value =
511+
connection_timeout_arg->getValue();
512+
int connection_timeout_int_value;
513+
if (connection_timeout_string_value.getAsInteger(
514+
10, connection_timeout_int_value)) {
515+
llvm::errs() << "'" << connection_timeout_string_value
516+
<< "' is not a valid connection timeout value\n";
517+
return EXIT_FAILURE;
518+
}
519+
// Ignore non-positive values.
520+
if (connection_timeout_int_value > 0)
521+
connection_timeout_seconds =
522+
std::chrono::seconds(connection_timeout_int_value);
523+
} else {
524+
llvm::errs()
525+
<< "\"--connection-timeout\" requires \"--connection\" to be "
526+
"specified\n";
527+
return EXIT_FAILURE;
528+
}
529+
}
530+
460531
#if !defined(_WIN32)
461532
if (input_args.hasArg(OPT_wait_for_debugger)) {
462533
printf("Paused waiting for debugger to attach (pid = %i)...\n", getpid());
@@ -523,7 +594,8 @@ int main(int argc, char *argv[]) {
523594
std::string name;
524595
std::tie(protocol, name) = *maybeProtoclAndName;
525596
if (auto Err = serveConnection(protocol, name, log.get(), default_repl_mode,
526-
pre_init_commands, no_lldbinit)) {
597+
pre_init_commands, no_lldbinit,
598+
connection_timeout_seconds)) {
527599
llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(),
528600
"Connection failed: ");
529601
return EXIT_FAILURE;

0 commit comments

Comments
 (0)