1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
|
import { FSWatcher, watch as chokidarWatch } from 'chokidar';
import * as child_process from "node:child_process";
import * as path from "path";
import { isDeepStrictEqual } from "util";
import * as vscode from "vscode";
/**
* Represents a running lldb-dap process that is accepting connections (i.e. in "server mode").
*
* Handles startup of the process if it isn't running already as well as prompting the user
* to restart when arguments have changed.
*/
export class LLDBDapServer implements vscode.Disposable {
private serverProcess?: child_process.ChildProcessWithoutNullStreams;
private serverInfo?: Promise<{ host: string; port: number }>;
private serverSpawnInfo?: string[];
// Detects changes to the lldb-dap executable file since the server's startup.
private serverFileWatcher?: FSWatcher;
// Indicates whether the lldb-dap executable file has changed since the server's startup.
private serverFileChanged?: boolean;
constructor() {
vscode.commands.registerCommand(
"lldb-dap.getServerProcess",
() => this.serverProcess,
);
}
/**
* Starts the server with the provided options. The server will be restarted or reused as
* necessary.
*
* @param dapPath the path to the debug adapter executable
* @param args the list of arguments to provide to the debug adapter
* @param options the options to provide to the debug adapter process
* @returns a promise that resolves with the host and port information or `undefined` if unable to launch the server.
*/
async start(
dapPath: string,
args: string[],
options?: child_process.SpawnOptionsWithoutStdio,
connectionTimeoutSeconds?: number,
): Promise<{ host: string; port: number } | undefined> {
// 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;
}
if (this.serverInfo) {
return this.serverInfo;
}
this.serverInfo = new Promise((resolve, reject) => {
const process = child_process.spawn(dapPath, dapArgs, options);
process.on("error", (error) => {
reject(error);
this.cleanUp(process);
});
process.on("exit", (code, signal) => {
let errorMessage = "Server process exited early";
if (code !== undefined) {
errorMessage += ` with code ${code}`;
} else if (signal !== undefined) {
errorMessage += ` due to signal ${signal}`;
}
reject(new Error(errorMessage));
this.cleanUp(process);
});
process.stdout.setEncoding("utf8").on("data", (data) => {
const connection = /connection:\/\/\[([^\]]+)\]:(\d+)/.exec(
data.toString(),
);
if (connection) {
const host = connection[1];
const port = Number(connection[2]);
resolve({ host, port });
process.stdout.removeAllListeners();
}
});
this.serverProcess = process;
this.serverSpawnInfo = this.getSpawnInfo(dapPath, dapArgs, options?.env);
this.serverFileChanged = false;
this.serverFileWatcher = chokidarWatch(dapPath);
this.serverFileWatcher
.on('change', () => this.serverFileChanged = true)
.on('unlink', () => this.serverFileChanged = true);
});
return this.serverInfo;
}
/**
* Checks to see if the server needs to be restarted. If so, it will prompt the user
* to ask if they wish to restart.
*
* @param dapPath the path to the debug adapter
* @param args the arguments for the debug adapter
* @returns whether or not startup should continue depending on user input
*/
private async shouldContinueStartup(
dapPath: string,
args: string[],
env: NodeJS.ProcessEnv | { [key: string]: string } | undefined,
): Promise<boolean> {
if (
!this.serverProcess ||
!this.serverInfo ||
!this.serverSpawnInfo ||
!this.serverFileWatcher ||
this.serverFileChanged === undefined
) {
return true;
}
const changeTLDR = [];
const changeDetails = [];
if (this.serverFileChanged) {
changeTLDR.push("an old binary");
}
const newSpawnInfo = this.getSpawnInfo(dapPath, args, env);
if (!isDeepStrictEqual(this.serverSpawnInfo, newSpawnInfo)) {
changeTLDR.push("different arguments");
changeDetails.push(`
The previous lldb-dap server was started with:
${this.serverSpawnInfo.join(" ")}
The new lldb-dap server will be started with:
${newSpawnInfo.join(" ")}
`
);
}
// If the server hasn't changed, continue startup without killing it.
if (changeTLDR.length === 0) {
return true;
}
// The server has changed. Prompt the user to restart it.
const userInput = await vscode.window.showInformationMessage(
"The lldb-dap server has changed. Would you like to restart the server?",
{
modal: true,
detail: `An existing lldb-dap server (${this.serverProcess.pid}) is running with ${changeTLDR.map(s => `*${s}*`).join(" and ")}.
${changeDetails.join("\n")}
Restarting the server will interrupt any existing debug sessions and start a new server.`,
},
"Restart",
"Use Existing",
);
switch (userInput) {
case "Restart":
this.dispose();
return true;
case "Use Existing":
return true;
case undefined:
return false;
}
}
dispose() {
if (!this.serverProcess) {
return;
}
this.serverProcess.kill();
this.cleanUp(this.serverProcess);
}
cleanUp(process: child_process.ChildProcessWithoutNullStreams) {
// If the following don't equal, then the fields have already been updated
// (either a new process has started, or the fields were already cleaned
// up), and so the cleanup should be skipped.
if (this.serverProcess === process) {
this.serverProcess = undefined;
this.serverInfo = undefined;
this.serverSpawnInfo = undefined;
this.serverFileWatcher?.close();
this.serverFileWatcher = undefined;
this.serverFileChanged = undefined;
}
}
getSpawnInfo(
path: string,
args: string[],
env: NodeJS.ProcessEnv | { [key: string]: string } | undefined,
): string[] {
return [
path,
...args,
...Object.entries(env ?? {})
// Filter and sort to avoid restarting the server just because the
// order of env changed or the log path changed.
.filter((entry) => String(entry[0]) !== "LLDBDAP_LOG")
.sort()
.map((entry) => String(entry[0]) + "=" + String(entry[1])),
];
}
}
|