Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/debug-auto-launch/src/extension.ts
5237 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { promises as fs } from 'fs';
7
import { createServer, Server } from 'net';
8
import { dirname } from 'path';
9
import * as vscode from 'vscode';
10
11
const enum State {
12
Disabled = 'disabled',
13
OnlyWithFlag = 'onlyWithFlag',
14
Smart = 'smart',
15
Always = 'always',
16
}
17
const TEXT_STATUSBAR_LABEL = {
18
[State.Disabled]: vscode.l10n.t('Auto Attach: Disabled'),
19
[State.Always]: vscode.l10n.t('Auto Attach: Always'),
20
[State.Smart]: vscode.l10n.t('Auto Attach: Smart'),
21
[State.OnlyWithFlag]: vscode.l10n.t('Auto Attach: With Flag'),
22
};
23
24
const TEXT_STATE_LABEL = {
25
[State.Disabled]: vscode.l10n.t('Disabled'),
26
[State.Always]: vscode.l10n.t('Always'),
27
[State.Smart]: vscode.l10n.t('Smart'),
28
[State.OnlyWithFlag]: vscode.l10n.t('Only With Flag'),
29
};
30
const TEXT_STATE_DESCRIPTION = {
31
[State.Disabled]: vscode.l10n.t('Auto attach is disabled and not shown in status bar'),
32
[State.Always]: vscode.l10n.t('Auto attach to every Node.js process launched in the terminal'),
33
[State.Smart]: vscode.l10n.t("Auto attach when running scripts that aren't in a node_modules folder"),
34
[State.OnlyWithFlag]: vscode.l10n.t('Only auto attach when the `--inspect` flag is given')
35
};
36
37
const TEXT_TOGGLE_TITLE = vscode.l10n.t('Toggle Auto Attach');
38
const TEXT_TOGGLE_WORKSPACE = vscode.l10n.t('Toggle auto attach in this workspace');
39
const TEXT_TOGGLE_GLOBAL = vscode.l10n.t('Toggle auto attach on this machine');
40
const TEXT_TEMP_DISABLE = vscode.l10n.t('Temporarily disable auto attach in this session');
41
const TEXT_TEMP_ENABLE = vscode.l10n.t('Re-enable auto attach');
42
const TEXT_TEMP_DISABLE_LABEL = vscode.l10n.t('Auto Attach: Disabled');
43
44
const TOGGLE_COMMAND = 'extension.node-debug.toggleAutoAttach';
45
const STORAGE_IPC = 'jsDebugIpcState';
46
47
const SETTING_SECTION = 'debug.javascript';
48
const SETTING_STATE = 'autoAttachFilter';
49
50
/**
51
* settings that, when changed, should cause us to refresh the state vars
52
*/
53
const SETTINGS_CAUSE_REFRESH = new Set(
54
['autoAttachSmartPattern', SETTING_STATE].map(s => `${SETTING_SECTION}.${s}`),
55
);
56
57
58
let currentState: Promise<{ context: vscode.ExtensionContext; state: State | null }>;
59
let statusItem: vscode.StatusBarItem | undefined; // and there is no status bar item
60
let server: Promise<Server | undefined> | undefined; // auto attach server
61
let isTemporarilyDisabled = false; // whether the auto attach server is disabled temporarily, reset whenever the state changes
62
63
export function activate(context: vscode.ExtensionContext): void {
64
currentState = Promise.resolve({ context, state: null });
65
66
context.subscriptions.push(
67
vscode.commands.registerCommand(TOGGLE_COMMAND, toggleAutoAttachSetting.bind(null, context)),
68
);
69
70
context.subscriptions.push(
71
vscode.workspace.onDidChangeConfiguration(e => {
72
// Whenever a setting is changed, disable auto attach, and re-enable
73
// it (if necessary) to refresh variables.
74
if (
75
e.affectsConfiguration(`${SETTING_SECTION}.${SETTING_STATE}`) ||
76
[...SETTINGS_CAUSE_REFRESH].some(setting => e.affectsConfiguration(setting))
77
) {
78
refreshAutoAttachVars();
79
}
80
}),
81
);
82
83
updateAutoAttach(readCurrentState());
84
}
85
86
export async function deactivate(): Promise<void> {
87
await destroyAttachServer();
88
}
89
90
function refreshAutoAttachVars() {
91
updateAutoAttach(State.Disabled);
92
updateAutoAttach(readCurrentState());
93
}
94
95
function getDefaultScope(info: ReturnType<vscode.WorkspaceConfiguration['inspect']>) {
96
if (!info) {
97
return vscode.ConfigurationTarget.Global;
98
} else if (info.workspaceFolderValue) {
99
return vscode.ConfigurationTarget.WorkspaceFolder;
100
} else if (info.workspaceValue) {
101
return vscode.ConfigurationTarget.Workspace;
102
} else if (info.globalValue) {
103
return vscode.ConfigurationTarget.Global;
104
}
105
106
return vscode.ConfigurationTarget.Global;
107
}
108
109
type PickResult = { state: State } | { setTempDisabled: boolean } | { scope: vscode.ConfigurationTarget } | undefined;
110
type PickItem = vscode.QuickPickItem & ({ state: State } | { setTempDisabled: boolean });
111
112
async function toggleAutoAttachSetting(context: vscode.ExtensionContext, scope?: vscode.ConfigurationTarget): Promise<void> {
113
const section = vscode.workspace.getConfiguration(SETTING_SECTION);
114
scope = scope || getDefaultScope(section.inspect(SETTING_STATE));
115
116
const isGlobalScope = scope === vscode.ConfigurationTarget.Global;
117
const quickPick = vscode.window.createQuickPick<PickItem>();
118
const current = readCurrentState();
119
120
const items: PickItem[] = [State.Always, State.Smart, State.OnlyWithFlag, State.Disabled].map(state => ({
121
state,
122
label: TEXT_STATE_LABEL[state],
123
description: TEXT_STATE_DESCRIPTION[state],
124
alwaysShow: true,
125
}));
126
127
if (current !== State.Disabled) {
128
items.unshift({
129
setTempDisabled: !isTemporarilyDisabled,
130
label: isTemporarilyDisabled ? TEXT_TEMP_ENABLE : TEXT_TEMP_DISABLE,
131
alwaysShow: true,
132
});
133
}
134
135
quickPick.items = items;
136
quickPick.activeItems = isTemporarilyDisabled
137
? [items[0]]
138
: quickPick.items.filter(i => 'state' in i && i.state === current);
139
quickPick.title = TEXT_TOGGLE_TITLE;
140
quickPick.placeholder = isGlobalScope ? TEXT_TOGGLE_GLOBAL : TEXT_TOGGLE_WORKSPACE;
141
quickPick.buttons = [
142
{
143
iconPath: new vscode.ThemeIcon(isGlobalScope ? 'folder' : 'globe'),
144
tooltip: isGlobalScope ? TEXT_TOGGLE_WORKSPACE : TEXT_TOGGLE_GLOBAL,
145
},
146
];
147
148
quickPick.show();
149
150
let result = await new Promise<PickResult>(resolve => {
151
quickPick.onDidAccept(() => resolve(quickPick.selectedItems[0]));
152
quickPick.onDidHide(() => resolve(undefined));
153
quickPick.onDidTriggerButton(() => {
154
resolve({
155
scope: isGlobalScope
156
? vscode.ConfigurationTarget.Workspace
157
: vscode.ConfigurationTarget.Global,
158
});
159
});
160
});
161
162
quickPick.dispose();
163
164
if (!result) {
165
return;
166
}
167
168
if ('scope' in result) {
169
return await toggleAutoAttachSetting(context, result.scope);
170
}
171
172
if ('state' in result) {
173
if (result.state !== current) {
174
section.update(SETTING_STATE, result.state, scope);
175
} else if (isTemporarilyDisabled) {
176
result = { setTempDisabled: false };
177
}
178
}
179
180
if ('setTempDisabled' in result) {
181
updateStatusBar(context, current, true);
182
isTemporarilyDisabled = result.setTempDisabled;
183
if (result.setTempDisabled) {
184
await destroyAttachServer();
185
} else {
186
await createAttachServer(context); // unsets temp disabled var internally
187
}
188
updateStatusBar(context, current, false);
189
}
190
}
191
192
function readCurrentState(): State {
193
const section = vscode.workspace.getConfiguration(SETTING_SECTION);
194
return section.get<State>(SETTING_STATE) ?? State.Disabled;
195
}
196
197
async function clearJsDebugAttachState(context: vscode.ExtensionContext) {
198
if (server || await context.workspaceState.get(STORAGE_IPC)) {
199
await context.workspaceState.update(STORAGE_IPC, undefined);
200
await vscode.commands.executeCommand('extension.js-debug.clearAutoAttachVariables');
201
await destroyAttachServer();
202
}
203
}
204
205
/**
206
* Turns auto attach on, and returns the server auto attach is listening on
207
* if it's successful.
208
*/
209
async function createAttachServer(context: vscode.ExtensionContext) {
210
const ipcAddress = await getIpcAddress(context);
211
if (!ipcAddress) {
212
return undefined;
213
}
214
215
server = createServerInner(ipcAddress).catch(async err => {
216
console.error('[debug-auto-launch] Error creating auto attach server: ', err);
217
218
if (process.platform !== 'win32') {
219
// On macOS, and perhaps some Linux distros, the temporary directory can
220
// sometimes change. If it looks like that's the cause of a listener
221
// error, automatically refresh the auto attach vars.
222
try {
223
await fs.access(dirname(ipcAddress));
224
} catch {
225
console.error('[debug-auto-launch] Refreshing variables from error');
226
refreshAutoAttachVars();
227
return undefined;
228
}
229
}
230
231
return undefined;
232
});
233
234
return await server;
235
}
236
237
const createServerInner = async (ipcAddress: string) => {
238
try {
239
return await createServerInstance(ipcAddress);
240
} catch (e) {
241
// On unix/linux, the file can 'leak' if the process exits unexpectedly.
242
// If we see this, try to delete the file and then listen again.
243
await fs.unlink(ipcAddress).catch(() => undefined);
244
return await createServerInstance(ipcAddress);
245
}
246
};
247
248
const createServerInstance = (ipcAddress: string) =>
249
new Promise<Server>((resolve, reject) => {
250
const s = createServer(socket => {
251
const data: Buffer[] = [];
252
socket.on('data', async chunk => {
253
if (chunk[chunk.length - 1] !== 0) {
254
// terminated with NUL byte
255
data.push(chunk);
256
return;
257
}
258
259
data.push(chunk.slice(0, -1));
260
261
try {
262
await vscode.commands.executeCommand(
263
'extension.js-debug.autoAttachToProcess',
264
JSON.parse(Buffer.concat(data).toString()),
265
);
266
socket.write(Buffer.from([0]));
267
} catch (err) {
268
socket.write(Buffer.from([1]));
269
console.error(err);
270
}
271
});
272
})
273
.on('error', reject)
274
.listen(ipcAddress, () => resolve(s));
275
});
276
277
/**
278
* Destroys the auto-attach server, if it's running.
279
*/
280
async function destroyAttachServer() {
281
const instance = await server;
282
if (instance) {
283
await new Promise(r => instance.close(r));
284
}
285
}
286
287
interface CachedIpcState {
288
ipcAddress: string;
289
jsDebugPath: string | undefined;
290
settingsValue: string;
291
}
292
293
/**
294
* Map of logic that happens when auto attach states are entered and exited.
295
* All state transitions are queued and run in order; promises are awaited.
296
*/
297
const transitions: { [S in State]: (context: vscode.ExtensionContext) => Promise<void> } = {
298
async [State.Disabled](context) {
299
await clearJsDebugAttachState(context);
300
},
301
302
async [State.OnlyWithFlag](context) {
303
await createAttachServer(context);
304
},
305
306
async [State.Smart](context) {
307
await createAttachServer(context);
308
},
309
310
async [State.Always](context) {
311
await createAttachServer(context);
312
},
313
};
314
315
/**
316
* Ensures the status bar text reflects the current state.
317
*/
318
function updateStatusBar(context: vscode.ExtensionContext, state: State, busy = false) {
319
if (state === State.Disabled && !busy) {
320
statusItem?.hide();
321
return;
322
}
323
324
if (!statusItem) {
325
statusItem = vscode.window.createStatusBarItem('status.debug.autoAttach', vscode.StatusBarAlignment.Left);
326
statusItem.name = vscode.l10n.t("Debug Auto Attach");
327
statusItem.command = TOGGLE_COMMAND;
328
statusItem.tooltip = vscode.l10n.t("Automatically attach to node.js processes in debug mode");
329
context.subscriptions.push(statusItem);
330
}
331
332
let text = busy ? '$(loading) ' : '';
333
text += isTemporarilyDisabled ? TEXT_TEMP_DISABLE_LABEL : TEXT_STATUSBAR_LABEL[state];
334
statusItem.text = text;
335
statusItem.show();
336
}
337
338
/**
339
* Updates the auto attach feature based on the user or workspace setting
340
*/
341
function updateAutoAttach(newState: State) {
342
currentState = currentState.then(async ({ context, state: oldState }) => {
343
if (newState === oldState) {
344
return { context, state: oldState };
345
}
346
347
if (oldState !== null) {
348
updateStatusBar(context, oldState, true);
349
}
350
351
await transitions[newState](context);
352
isTemporarilyDisabled = false;
353
updateStatusBar(context, newState, false);
354
return { context, state: newState };
355
});
356
}
357
358
/**
359
* Gets the IPC address for the server to listen on for js-debug sessions. This
360
* is cached such that we can reuse the address of previous activations.
361
*/
362
async function getIpcAddress(context: vscode.ExtensionContext) {
363
// Iff the `cachedData` is present, the js-debug registered environment
364
// variables for this workspace--cachedData is set after successfully
365
// invoking the attachment command.
366
const cachedIpc = context.workspaceState.get<CachedIpcState>(STORAGE_IPC);
367
368
// We invalidate the IPC data if the js-debug path changes, since that
369
// indicates the extension was updated or reinstalled and the
370
// environment variables will have been lost.
371
// todo: make a way in the API to read environment data directly without activating js-debug?
372
const jsDebugPath =
373
vscode.extensions.getExtension('ms-vscode.js-debug-nightly')?.extensionPath ||
374
vscode.extensions.getExtension('ms-vscode.js-debug')?.extensionPath;
375
376
const settingsValue = getJsDebugSettingKey();
377
if (cachedIpc?.jsDebugPath === jsDebugPath && cachedIpc?.settingsValue === settingsValue) {
378
return cachedIpc.ipcAddress;
379
}
380
381
const result = await vscode.commands.executeCommand<{ ipcAddress: string }>(
382
'extension.js-debug.setAutoAttachVariables',
383
cachedIpc?.ipcAddress,
384
);
385
if (!result) {
386
return;
387
}
388
389
const ipcAddress = result.ipcAddress;
390
await context.workspaceState.update(STORAGE_IPC, {
391
ipcAddress,
392
jsDebugPath,
393
settingsValue,
394
} satisfies CachedIpcState);
395
396
return ipcAddress;
397
}
398
399
function getJsDebugSettingKey() {
400
const o: { [key: string]: unknown } = {};
401
const config = vscode.workspace.getConfiguration(SETTING_SECTION);
402
for (const setting of SETTINGS_CAUSE_REFRESH) {
403
o[setting] = config.get(setting);
404
}
405
406
return JSON.stringify(o);
407
}
408
409