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