Skip to content

Commit 9f56e36

Browse files
authored
🎁 Add killOnServerStop to debug configuration (microsoft#163779)
* 🎁 Add `killOnServerStop` to schema Signed-off-by: Babak K. Shandiz <[email protected]> * 📜 Add description for `killOnServerStop` Signed-off-by: Babak K. Shandiz <[email protected]> * 🔨 Stop created debug session on server stop Signed-off-by: Babak K. Shandiz <[email protected]> * 🔨 Push kill listeners into another disposable container Signed-off-by: Babak K. Shandiz <[email protected]> * 🐛 Prevent leak when new debug session fails to start Signed-off-by: Babak K. Shandiz <[email protected]> * 🔨 Use more verbose name for debug session tracker ID Signed-off-by: Babak K. Shandiz <[email protected]> Signed-off-by: Babak K. Shandiz <[email protected]>
1 parent 328ed10 commit 9f56e36

File tree

3 files changed

+128
-10
lines changed

3 files changed

+128
-10
lines changed

extensions/debug-server-ready/package.json

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"additionalProperties": false,
4141
"markdownDescription": "%debug.server.ready.serverReadyAction.description%",
4242
"default": {
43-
"action": "openExternally"
43+
"action": "openExternally",
44+
"killOnServerStop": false
4445
},
4546
"properties": {
4647
"action": {
@@ -63,6 +64,11 @@
6364
"type": "string",
6465
"markdownDescription": "%debug.server.ready.uriFormat.description%",
6566
"default": "http://localhost:%s"
67+
},
68+
"killOnServerStop": {
69+
"type": "boolean",
70+
"markdownDescription": "%debug.server.ready.killOnServerStop.description%",
71+
"default": false
6672
}
6773
}
6874
},
@@ -74,7 +80,8 @@
7480
"action": "debugWithEdge",
7581
"pattern": "listening on port ([0-9]+)",
7682
"uriFormat": "http://localhost:%s",
77-
"webRoot": "${workspaceFolder}"
83+
"webRoot": "${workspaceFolder}",
84+
"killOnServerStop": false
7885
},
7986
"properties": {
8087
"action": {
@@ -103,6 +110,11 @@
103110
"type": "string",
104111
"markdownDescription": "%debug.server.ready.webRoot.description%",
105112
"default": "${workspaceFolder}"
113+
},
114+
"killOnServerStop": {
115+
"type": "boolean",
116+
"markdownDescription": "%debug.server.ready.killOnServerStop.description%",
117+
"default": false
106118
}
107119
}
108120
},
@@ -112,7 +124,8 @@
112124
"markdownDescription": "%debug.server.ready.serverReadyAction.description%",
113125
"default": {
114126
"action": "startDebugging",
115-
"name": "<launch browser config name>"
127+
"name": "<launch browser config name>",
128+
"killOnServerStop": false
116129
},
117130
"required": [
118131
"name"
@@ -138,6 +151,11 @@
138151
"type": "string",
139152
"markdownDescription": "%debug.server.ready.debugConfigName.description%",
140153
"default": "Launch Browser"
154+
},
155+
"killOnServerStop": {
156+
"type": "boolean",
157+
"markdownDescription": "%debug.server.ready.killOnServerStop.description%",
158+
"default": false
141159
}
142160
}
143161
}

extensions/debug-server-ready/package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@
1010
"debug.server.ready.pattern.description": "Server is ready if this pattern appears on the debug console. The first capture group must include a URI or a port number.",
1111
"debug.server.ready.uriFormat.description": "A format string used when constructing the URI from a port number. The first '%s' is substituted with the port number.",
1212
"debug.server.ready.webRoot.description": "Value passed to the debug configuration for the 'Debugger for Chrome'.",
13+
"debug.server.ready.killOnServerStop.description": "Stop the child session when the parent session stopped.",
1314
"debug.server.ready.debugConfigName.description": "Name of the launch configuration to run."
1415
}

extensions/debug-server-ready/src/extension.ts

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import * as vscode from 'vscode';
77
import * as util from 'util';
8+
import { randomUUID } from 'crypto';
89

910
const PATTERN = 'listening on.* (https?://\\S+|[0-9]+)'; // matches "listening on port 3000" or "Now listening on: https://localhost:5001"
1011
const URI_PORT_FORMAT = 'http://localhost:%s';
@@ -17,6 +18,7 @@ interface ServerReadyAction {
1718
uriFormat?: string;
1819
webRoot?: string;
1920
name?: string;
21+
killOnServerStop?: boolean;
2022
}
2123

2224
class Trigger {
@@ -40,6 +42,7 @@ class ServerReadyDetector extends vscode.Disposable {
4042
private shellPid?: number;
4143
private regexp: RegExp;
4244
private disposables: vscode.Disposable[] = [];
45+
private lateDisposables = new Set<vscode.Disposable>([]);
4346

4447
static start(session: vscode.DebugSession): ServerReadyDetector | undefined {
4548
if (session.configuration.serverReadyAction) {
@@ -109,6 +112,11 @@ class ServerReadyDetector extends vscode.Disposable {
109112
this.disposables = [];
110113
}
111114

115+
override dispose() {
116+
this.lateDisposables.forEach(d => d.dispose());
117+
return super.dispose();
118+
}
119+
112120
detectPattern(s: string): boolean {
113121
if (!this.trigger.hasFired) {
114122
const matches = this.regexp.exec(s);
@@ -153,25 +161,25 @@ class ServerReadyDetector extends vscode.Disposable {
153161
this.openExternalWithUri(session, uri);
154162
}
155163

156-
private openExternalWithUri(session: vscode.DebugSession, uri: string) {
164+
private async openExternalWithUri(session: vscode.DebugSession, uri: string) {
157165

158166
const args: ServerReadyAction = session.configuration.serverReadyAction;
159167
switch (args.action || 'openExternally') {
160168

161169
case 'openExternally':
162-
vscode.env.openExternal(vscode.Uri.parse(uri));
170+
await vscode.env.openExternal(vscode.Uri.parse(uri));
163171
break;
164172

165173
case 'debugWithChrome':
166-
this.debugWithBrowser('pwa-chrome', session, uri);
174+
await this.debugWithBrowser('pwa-chrome', session, uri);
167175
break;
168176

169177
case 'debugWithEdge':
170-
this.debugWithBrowser('pwa-msedge', session, uri);
178+
await this.debugWithBrowser('pwa-msedge', session, uri);
171179
break;
172180

173181
case 'startDebugging':
174-
vscode.debug.startDebugging(session.workspaceFolder, args.name || 'unspecified');
182+
await this.startNamedDebugSession(session, args.name || 'unspecified');
175183
break;
176184

177185
default:
@@ -180,13 +188,104 @@ class ServerReadyDetector extends vscode.Disposable {
180188
}
181189
}
182190

183-
private debugWithBrowser(type: string, session: vscode.DebugSession, uri: string) {
191+
private async debugWithBrowser(type: string, session: vscode.DebugSession, uri: string) {
192+
const args = session.configuration.serverReadyAction as ServerReadyAction;
193+
if (!args.killOnServerStop) {
194+
await this.startBrowserDebugSession(type, session, uri);
195+
return;
196+
}
197+
198+
const trackerId = randomUUID();
199+
const cts = new vscode.CancellationTokenSource();
200+
const newSessionPromise = this.catchStartedDebugSession(session => session.configuration._debugServerReadySessionId === trackerId, cts.token);
201+
202+
if (!await this.startBrowserDebugSession(type, session, uri, trackerId)) {
203+
cts.cancel();
204+
cts.dispose();
205+
return;
206+
}
207+
208+
const createdSession = await newSessionPromise;
209+
cts.dispose();
210+
211+
if (!createdSession) {
212+
return;
213+
}
214+
215+
const stopListener = vscode.debug.onDidTerminateDebugSession(async (terminated) => {
216+
if (terminated === session) {
217+
stopListener.dispose();
218+
this.lateDisposables.delete(stopListener);
219+
await vscode.debug.stopDebugging(createdSession);
220+
}
221+
});
222+
this.lateDisposables.add(stopListener);
223+
}
224+
225+
private startBrowserDebugSession(type: string, session: vscode.DebugSession, uri: string, trackerId?: string) {
184226
return vscode.debug.startDebugging(session.workspaceFolder, {
185227
type,
186228
name: 'Browser Debug',
187229
request: 'launch',
188230
url: uri,
189-
webRoot: session.configuration.serverReadyAction.webRoot || WEB_ROOT
231+
webRoot: session.configuration.serverReadyAction.webRoot || WEB_ROOT,
232+
_debugServerReadySessionId: trackerId,
233+
});
234+
}
235+
236+
private async startNamedDebugSession(session: vscode.DebugSession, name: string) {
237+
const args = session.configuration.serverReadyAction as ServerReadyAction;
238+
if (!args.killOnServerStop) {
239+
await vscode.debug.startDebugging(session.workspaceFolder, name);
240+
return;
241+
}
242+
243+
const cts = new vscode.CancellationTokenSource();
244+
const newSessionPromise = this.catchStartedDebugSession(x => x.name === name, cts.token);
245+
246+
if (!await vscode.debug.startDebugging(session.workspaceFolder, name)) {
247+
cts.cancel();
248+
cts.dispose();
249+
return;
250+
}
251+
252+
const createdSession = await newSessionPromise;
253+
cts.dispose();
254+
255+
if (!createdSession) {
256+
return;
257+
}
258+
259+
const stopListener = vscode.debug.onDidTerminateDebugSession(async (terminated) => {
260+
if (terminated === session) {
261+
stopListener.dispose();
262+
this.lateDisposables.delete(stopListener);
263+
await vscode.debug.stopDebugging(createdSession);
264+
}
265+
});
266+
this.lateDisposables.add(stopListener);
267+
}
268+
269+
private catchStartedDebugSession(predicate: (session: vscode.DebugSession) => boolean, cancellationToken: vscode.CancellationToken): Promise<vscode.DebugSession | undefined> {
270+
return new Promise<vscode.DebugSession | undefined>(_resolve => {
271+
const done = (value?: vscode.DebugSession) => {
272+
listener.dispose();
273+
cancellationListener.dispose();
274+
this.lateDisposables.delete(listener);
275+
this.lateDisposables.delete(cancellationListener);
276+
_resolve(value);
277+
};
278+
279+
const cancellationListener = cancellationToken.onCancellationRequested(done);
280+
const listener = vscode.debug.onDidStartDebugSession(session => {
281+
if (predicate(session)) {
282+
done(session);
283+
}
284+
});
285+
286+
// In case the debug session of interest was never caught anyhow.
287+
this.lateDisposables.add(listener);
288+
this.lateDisposables.add(cancellationListener);
190289
});
191290
}
192291
}

0 commit comments

Comments
 (0)