Path: blob/main/extensions/debug-auto-launch/src/extension.ts
3291 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { promises as fs } from 'fs';6import { createServer, Server } from 'net';7import { dirname } from 'path';8import * as vscode from 'vscode';910const enum State {11Disabled = 'disabled',12OnlyWithFlag = 'onlyWithFlag',13Smart = 'smart',14Always = 'always',15}16const TEXT_STATUSBAR_LABEL = {17[State.Disabled]: vscode.l10n.t('Auto Attach: Disabled'),18[State.Always]: vscode.l10n.t('Auto Attach: Always'),19[State.Smart]: vscode.l10n.t('Auto Attach: Smart'),20[State.OnlyWithFlag]: vscode.l10n.t('Auto Attach: With Flag'),21};2223const TEXT_STATE_LABEL = {24[State.Disabled]: vscode.l10n.t('Disabled'),25[State.Always]: vscode.l10n.t('Always'),26[State.Smart]: vscode.l10n.t('Smart'),27[State.OnlyWithFlag]: vscode.l10n.t('Only With Flag'),28};29const TEXT_STATE_DESCRIPTION = {30[State.Disabled]: vscode.l10n.t('Auto attach is disabled and not shown in status bar'),31[State.Always]: vscode.l10n.t('Auto attach to every Node.js process launched in the terminal'),32[State.Smart]: vscode.l10n.t("Auto attach when running scripts that aren't in a node_modules folder"),33[State.OnlyWithFlag]: vscode.l10n.t('Only auto attach when the `--inspect` flag is given')34};35const TEXT_TOGGLE_WORKSPACE = vscode.l10n.t('Toggle auto attach in this workspace');36const TEXT_TOGGLE_GLOBAL = vscode.l10n.t('Toggle auto attach on this machine');37const TEXT_TEMP_DISABLE = vscode.l10n.t('Temporarily disable auto attach in this session');38const TEXT_TEMP_ENABLE = vscode.l10n.t('Re-enable auto attach');39const TEXT_TEMP_DISABLE_LABEL = vscode.l10n.t('Auto Attach: Disabled');4041const TOGGLE_COMMAND = 'extension.node-debug.toggleAutoAttach';42const STORAGE_IPC = 'jsDebugIpcState';4344const SETTING_SECTION = 'debug.javascript';45const SETTING_STATE = 'autoAttachFilter';4647/**48* settings that, when changed, should cause us to refresh the state vars49*/50const SETTINGS_CAUSE_REFRESH = new Set(51['autoAttachSmartPattern', SETTING_STATE].map(s => `${SETTING_SECTION}.${s}`),52);535455let currentState: Promise<{ context: vscode.ExtensionContext; state: State | null }>;56let statusItem: vscode.StatusBarItem | undefined; // and there is no status bar item57let server: Promise<Server | undefined> | undefined; // auto attach server58let isTemporarilyDisabled = false; // whether the auto attach server is disabled temporarily, reset whenever the state changes5960export function activate(context: vscode.ExtensionContext): void {61currentState = Promise.resolve({ context, state: null });6263context.subscriptions.push(64vscode.commands.registerCommand(TOGGLE_COMMAND, toggleAutoAttachSetting.bind(null, context)),65);6667context.subscriptions.push(68vscode.workspace.onDidChangeConfiguration(e => {69// Whenever a setting is changed, disable auto attach, and re-enable70// it (if necessary) to refresh variables.71if (72e.affectsConfiguration(`${SETTING_SECTION}.${SETTING_STATE}`) ||73[...SETTINGS_CAUSE_REFRESH].some(setting => e.affectsConfiguration(setting))74) {75refreshAutoAttachVars();76}77}),78);7980updateAutoAttach(readCurrentState());81}8283export async function deactivate(): Promise<void> {84await destroyAttachServer();85}8687function refreshAutoAttachVars() {88updateAutoAttach(State.Disabled);89updateAutoAttach(readCurrentState());90}9192function getDefaultScope(info: ReturnType<vscode.WorkspaceConfiguration['inspect']>) {93if (!info) {94return vscode.ConfigurationTarget.Global;95} else if (info.workspaceFolderValue) {96return vscode.ConfigurationTarget.WorkspaceFolder;97} else if (info.workspaceValue) {98return vscode.ConfigurationTarget.Workspace;99} else if (info.globalValue) {100return vscode.ConfigurationTarget.Global;101}102103return vscode.ConfigurationTarget.Global;104}105106type PickResult = { state: State } | { setTempDisabled: boolean } | { scope: vscode.ConfigurationTarget } | undefined;107type PickItem = vscode.QuickPickItem & ({ state: State } | { setTempDisabled: boolean });108109async function toggleAutoAttachSetting(context: vscode.ExtensionContext, scope?: vscode.ConfigurationTarget): Promise<void> {110const section = vscode.workspace.getConfiguration(SETTING_SECTION);111scope = scope || getDefaultScope(section.inspect(SETTING_STATE));112113const isGlobalScope = scope === vscode.ConfigurationTarget.Global;114const quickPick = vscode.window.createQuickPick<PickItem>();115const current = readCurrentState();116117const items: PickItem[] = [State.Always, State.Smart, State.OnlyWithFlag, State.Disabled].map(state => ({118state,119label: TEXT_STATE_LABEL[state],120description: TEXT_STATE_DESCRIPTION[state],121alwaysShow: true,122}));123124if (current !== State.Disabled) {125items.unshift({126setTempDisabled: !isTemporarilyDisabled,127label: isTemporarilyDisabled ? TEXT_TEMP_ENABLE : TEXT_TEMP_DISABLE,128alwaysShow: true,129});130}131132quickPick.items = items;133quickPick.activeItems = isTemporarilyDisabled134? [items[0]]135: quickPick.items.filter(i => 'state' in i && i.state === current);136quickPick.title = isGlobalScope ? TEXT_TOGGLE_GLOBAL : TEXT_TOGGLE_WORKSPACE;137quickPick.buttons = [138{139iconPath: new vscode.ThemeIcon(isGlobalScope ? 'folder' : 'globe'),140tooltip: isGlobalScope ? TEXT_TOGGLE_WORKSPACE : TEXT_TOGGLE_GLOBAL,141},142];143144quickPick.show();145146let result = await new Promise<PickResult>(resolve => {147quickPick.onDidAccept(() => resolve(quickPick.selectedItems[0]));148quickPick.onDidHide(() => resolve(undefined));149quickPick.onDidTriggerButton(() => {150resolve({151scope: isGlobalScope152? vscode.ConfigurationTarget.Workspace153: vscode.ConfigurationTarget.Global,154});155});156});157158quickPick.dispose();159160if (!result) {161return;162}163164if ('scope' in result) {165return await toggleAutoAttachSetting(context, result.scope);166}167168if ('state' in result) {169if (result.state !== current) {170section.update(SETTING_STATE, result.state, scope);171} else if (isTemporarilyDisabled) {172result = { setTempDisabled: false };173}174}175176if ('setTempDisabled' in result) {177updateStatusBar(context, current, true);178isTemporarilyDisabled = result.setTempDisabled;179if (result.setTempDisabled) {180await destroyAttachServer();181} else {182await createAttachServer(context); // unsets temp disabled var internally183}184updateStatusBar(context, current, false);185}186}187188function readCurrentState(): State {189const section = vscode.workspace.getConfiguration(SETTING_SECTION);190return section.get<State>(SETTING_STATE) ?? State.Disabled;191}192193async function clearJsDebugAttachState(context: vscode.ExtensionContext) {194if (server || await context.workspaceState.get(STORAGE_IPC)) {195await context.workspaceState.update(STORAGE_IPC, undefined);196await vscode.commands.executeCommand('extension.js-debug.clearAutoAttachVariables');197await destroyAttachServer();198}199}200201/**202* Turns auto attach on, and returns the server auto attach is listening on203* if it's successful.204*/205async function createAttachServer(context: vscode.ExtensionContext) {206const ipcAddress = await getIpcAddress(context);207if (!ipcAddress) {208return undefined;209}210211server = createServerInner(ipcAddress).catch(async err => {212console.error('[debug-auto-launch] Error creating auto attach server: ', err);213214if (process.platform !== 'win32') {215// On macOS, and perhaps some Linux distros, the temporary directory can216// sometimes change. If it looks like that's the cause of a listener217// error, automatically refresh the auto attach vars.218try {219await fs.access(dirname(ipcAddress));220} catch {221console.error('[debug-auto-launch] Refreshing variables from error');222refreshAutoAttachVars();223return undefined;224}225}226227return undefined;228});229230return await server;231}232233const createServerInner = async (ipcAddress: string) => {234try {235return await createServerInstance(ipcAddress);236} catch (e) {237// On unix/linux, the file can 'leak' if the process exits unexpectedly.238// If we see this, try to delete the file and then listen again.239await fs.unlink(ipcAddress).catch(() => undefined);240return await createServerInstance(ipcAddress);241}242};243244const createServerInstance = (ipcAddress: string) =>245new Promise<Server>((resolve, reject) => {246const s = createServer(socket => {247const data: Buffer[] = [];248socket.on('data', async chunk => {249if (chunk[chunk.length - 1] !== 0) {250// terminated with NUL byte251data.push(chunk);252return;253}254255data.push(chunk.slice(0, -1));256257try {258await vscode.commands.executeCommand(259'extension.js-debug.autoAttachToProcess',260JSON.parse(Buffer.concat(data).toString()),261);262socket.write(Buffer.from([0]));263} catch (err) {264socket.write(Buffer.from([1]));265console.error(err);266}267});268})269.on('error', reject)270.listen(ipcAddress, () => resolve(s));271});272273/**274* Destroys the auto-attach server, if it's running.275*/276async function destroyAttachServer() {277const instance = await server;278if (instance) {279await new Promise(r => instance.close(r));280}281}282283interface CachedIpcState {284ipcAddress: string;285jsDebugPath: string | undefined;286settingsValue: string;287}288289/**290* Map of logic that happens when auto attach states are entered and exited.291* All state transitions are queued and run in order; promises are awaited.292*/293const transitions: { [S in State]: (context: vscode.ExtensionContext) => Promise<void> } = {294async [State.Disabled](context) {295await clearJsDebugAttachState(context);296},297298async [State.OnlyWithFlag](context) {299await createAttachServer(context);300},301302async [State.Smart](context) {303await createAttachServer(context);304},305306async [State.Always](context) {307await createAttachServer(context);308},309};310311/**312* Ensures the status bar text reflects the current state.313*/314function updateStatusBar(context: vscode.ExtensionContext, state: State, busy = false) {315if (state === State.Disabled && !busy) {316statusItem?.hide();317return;318}319320if (!statusItem) {321statusItem = vscode.window.createStatusBarItem('status.debug.autoAttach', vscode.StatusBarAlignment.Left);322statusItem.name = vscode.l10n.t("Debug Auto Attach");323statusItem.command = TOGGLE_COMMAND;324statusItem.tooltip = vscode.l10n.t("Automatically attach to node.js processes in debug mode");325context.subscriptions.push(statusItem);326}327328let text = busy ? '$(loading) ' : '';329text += isTemporarilyDisabled ? TEXT_TEMP_DISABLE_LABEL : TEXT_STATUSBAR_LABEL[state];330statusItem.text = text;331statusItem.show();332}333334/**335* Updates the auto attach feature based on the user or workspace setting336*/337function updateAutoAttach(newState: State) {338currentState = currentState.then(async ({ context, state: oldState }) => {339if (newState === oldState) {340return { context, state: oldState };341}342343if (oldState !== null) {344updateStatusBar(context, oldState, true);345}346347await transitions[newState](context);348isTemporarilyDisabled = false;349updateStatusBar(context, newState, false);350return { context, state: newState };351});352}353354/**355* Gets the IPC address for the server to listen on for js-debug sessions. This356* is cached such that we can reuse the address of previous activations.357*/358async function getIpcAddress(context: vscode.ExtensionContext) {359// Iff the `cachedData` is present, the js-debug registered environment360// variables for this workspace--cachedData is set after successfully361// invoking the attachment command.362const cachedIpc = context.workspaceState.get<CachedIpcState>(STORAGE_IPC);363364// We invalidate the IPC data if the js-debug path changes, since that365// indicates the extension was updated or reinstalled and the366// environment variables will have been lost.367// todo: make a way in the API to read environment data directly without activating js-debug?368const jsDebugPath =369vscode.extensions.getExtension('ms-vscode.js-debug-nightly')?.extensionPath ||370vscode.extensions.getExtension('ms-vscode.js-debug')?.extensionPath;371372const settingsValue = getJsDebugSettingKey();373if (cachedIpc?.jsDebugPath === jsDebugPath && cachedIpc?.settingsValue === settingsValue) {374return cachedIpc.ipcAddress;375}376377const result = await vscode.commands.executeCommand<{ ipcAddress: string }>(378'extension.js-debug.setAutoAttachVariables',379cachedIpc?.ipcAddress,380);381if (!result) {382return;383}384385const ipcAddress = result.ipcAddress;386await context.workspaceState.update(STORAGE_IPC, {387ipcAddress,388jsDebugPath,389settingsValue,390} satisfies CachedIpcState);391392return ipcAddress;393}394395function getJsDebugSettingKey() {396const o: { [key: string]: unknown } = {};397const config = vscode.workspace.getConfiguration(SETTING_SECTION);398for (const setting of SETTINGS_CAUSE_REFRESH) {399o[setting] = config.get(setting);400}401402return JSON.stringify(o);403}404405406