Path: blob/main/extensions/debug-auto-launch/src/extension.ts
5237 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};3536const TEXT_TOGGLE_TITLE = vscode.l10n.t('Toggle Auto Attach');37const TEXT_TOGGLE_WORKSPACE = vscode.l10n.t('Toggle auto attach in this workspace');38const TEXT_TOGGLE_GLOBAL = vscode.l10n.t('Toggle auto attach on this machine');39const TEXT_TEMP_DISABLE = vscode.l10n.t('Temporarily disable auto attach in this session');40const TEXT_TEMP_ENABLE = vscode.l10n.t('Re-enable auto attach');41const TEXT_TEMP_DISABLE_LABEL = vscode.l10n.t('Auto Attach: Disabled');4243const TOGGLE_COMMAND = 'extension.node-debug.toggleAutoAttach';44const STORAGE_IPC = 'jsDebugIpcState';4546const SETTING_SECTION = 'debug.javascript';47const SETTING_STATE = 'autoAttachFilter';4849/**50* settings that, when changed, should cause us to refresh the state vars51*/52const SETTINGS_CAUSE_REFRESH = new Set(53['autoAttachSmartPattern', SETTING_STATE].map(s => `${SETTING_SECTION}.${s}`),54);555657let currentState: Promise<{ context: vscode.ExtensionContext; state: State | null }>;58let statusItem: vscode.StatusBarItem | undefined; // and there is no status bar item59let server: Promise<Server | undefined> | undefined; // auto attach server60let isTemporarilyDisabled = false; // whether the auto attach server is disabled temporarily, reset whenever the state changes6162export function activate(context: vscode.ExtensionContext): void {63currentState = Promise.resolve({ context, state: null });6465context.subscriptions.push(66vscode.commands.registerCommand(TOGGLE_COMMAND, toggleAutoAttachSetting.bind(null, context)),67);6869context.subscriptions.push(70vscode.workspace.onDidChangeConfiguration(e => {71// Whenever a setting is changed, disable auto attach, and re-enable72// it (if necessary) to refresh variables.73if (74e.affectsConfiguration(`${SETTING_SECTION}.${SETTING_STATE}`) ||75[...SETTINGS_CAUSE_REFRESH].some(setting => e.affectsConfiguration(setting))76) {77refreshAutoAttachVars();78}79}),80);8182updateAutoAttach(readCurrentState());83}8485export async function deactivate(): Promise<void> {86await destroyAttachServer();87}8889function refreshAutoAttachVars() {90updateAutoAttach(State.Disabled);91updateAutoAttach(readCurrentState());92}9394function getDefaultScope(info: ReturnType<vscode.WorkspaceConfiguration['inspect']>) {95if (!info) {96return vscode.ConfigurationTarget.Global;97} else if (info.workspaceFolderValue) {98return vscode.ConfigurationTarget.WorkspaceFolder;99} else if (info.workspaceValue) {100return vscode.ConfigurationTarget.Workspace;101} else if (info.globalValue) {102return vscode.ConfigurationTarget.Global;103}104105return vscode.ConfigurationTarget.Global;106}107108type PickResult = { state: State } | { setTempDisabled: boolean } | { scope: vscode.ConfigurationTarget } | undefined;109type PickItem = vscode.QuickPickItem & ({ state: State } | { setTempDisabled: boolean });110111async function toggleAutoAttachSetting(context: vscode.ExtensionContext, scope?: vscode.ConfigurationTarget): Promise<void> {112const section = vscode.workspace.getConfiguration(SETTING_SECTION);113scope = scope || getDefaultScope(section.inspect(SETTING_STATE));114115const isGlobalScope = scope === vscode.ConfigurationTarget.Global;116const quickPick = vscode.window.createQuickPick<PickItem>();117const current = readCurrentState();118119const items: PickItem[] = [State.Always, State.Smart, State.OnlyWithFlag, State.Disabled].map(state => ({120state,121label: TEXT_STATE_LABEL[state],122description: TEXT_STATE_DESCRIPTION[state],123alwaysShow: true,124}));125126if (current !== State.Disabled) {127items.unshift({128setTempDisabled: !isTemporarilyDisabled,129label: isTemporarilyDisabled ? TEXT_TEMP_ENABLE : TEXT_TEMP_DISABLE,130alwaysShow: true,131});132}133134quickPick.items = items;135quickPick.activeItems = isTemporarilyDisabled136? [items[0]]137: quickPick.items.filter(i => 'state' in i && i.state === current);138quickPick.title = TEXT_TOGGLE_TITLE;139quickPick.placeholder = isGlobalScope ? TEXT_TOGGLE_GLOBAL : TEXT_TOGGLE_WORKSPACE;140quickPick.buttons = [141{142iconPath: new vscode.ThemeIcon(isGlobalScope ? 'folder' : 'globe'),143tooltip: isGlobalScope ? TEXT_TOGGLE_WORKSPACE : TEXT_TOGGLE_GLOBAL,144},145];146147quickPick.show();148149let result = await new Promise<PickResult>(resolve => {150quickPick.onDidAccept(() => resolve(quickPick.selectedItems[0]));151quickPick.onDidHide(() => resolve(undefined));152quickPick.onDidTriggerButton(() => {153resolve({154scope: isGlobalScope155? vscode.ConfigurationTarget.Workspace156: vscode.ConfigurationTarget.Global,157});158});159});160161quickPick.dispose();162163if (!result) {164return;165}166167if ('scope' in result) {168return await toggleAutoAttachSetting(context, result.scope);169}170171if ('state' in result) {172if (result.state !== current) {173section.update(SETTING_STATE, result.state, scope);174} else if (isTemporarilyDisabled) {175result = { setTempDisabled: false };176}177}178179if ('setTempDisabled' in result) {180updateStatusBar(context, current, true);181isTemporarilyDisabled = result.setTempDisabled;182if (result.setTempDisabled) {183await destroyAttachServer();184} else {185await createAttachServer(context); // unsets temp disabled var internally186}187updateStatusBar(context, current, false);188}189}190191function readCurrentState(): State {192const section = vscode.workspace.getConfiguration(SETTING_SECTION);193return section.get<State>(SETTING_STATE) ?? State.Disabled;194}195196async function clearJsDebugAttachState(context: vscode.ExtensionContext) {197if (server || await context.workspaceState.get(STORAGE_IPC)) {198await context.workspaceState.update(STORAGE_IPC, undefined);199await vscode.commands.executeCommand('extension.js-debug.clearAutoAttachVariables');200await destroyAttachServer();201}202}203204/**205* Turns auto attach on, and returns the server auto attach is listening on206* if it's successful.207*/208async function createAttachServer(context: vscode.ExtensionContext) {209const ipcAddress = await getIpcAddress(context);210if (!ipcAddress) {211return undefined;212}213214server = createServerInner(ipcAddress).catch(async err => {215console.error('[debug-auto-launch] Error creating auto attach server: ', err);216217if (process.platform !== 'win32') {218// On macOS, and perhaps some Linux distros, the temporary directory can219// sometimes change. If it looks like that's the cause of a listener220// error, automatically refresh the auto attach vars.221try {222await fs.access(dirname(ipcAddress));223} catch {224console.error('[debug-auto-launch] Refreshing variables from error');225refreshAutoAttachVars();226return undefined;227}228}229230return undefined;231});232233return await server;234}235236const createServerInner = async (ipcAddress: string) => {237try {238return await createServerInstance(ipcAddress);239} catch (e) {240// On unix/linux, the file can 'leak' if the process exits unexpectedly.241// If we see this, try to delete the file and then listen again.242await fs.unlink(ipcAddress).catch(() => undefined);243return await createServerInstance(ipcAddress);244}245};246247const createServerInstance = (ipcAddress: string) =>248new Promise<Server>((resolve, reject) => {249const s = createServer(socket => {250const data: Buffer[] = [];251socket.on('data', async chunk => {252if (chunk[chunk.length - 1] !== 0) {253// terminated with NUL byte254data.push(chunk);255return;256}257258data.push(chunk.slice(0, -1));259260try {261await vscode.commands.executeCommand(262'extension.js-debug.autoAttachToProcess',263JSON.parse(Buffer.concat(data).toString()),264);265socket.write(Buffer.from([0]));266} catch (err) {267socket.write(Buffer.from([1]));268console.error(err);269}270});271})272.on('error', reject)273.listen(ipcAddress, () => resolve(s));274});275276/**277* Destroys the auto-attach server, if it's running.278*/279async function destroyAttachServer() {280const instance = await server;281if (instance) {282await new Promise(r => instance.close(r));283}284}285286interface CachedIpcState {287ipcAddress: string;288jsDebugPath: string | undefined;289settingsValue: string;290}291292/**293* Map of logic that happens when auto attach states are entered and exited.294* All state transitions are queued and run in order; promises are awaited.295*/296const transitions: { [S in State]: (context: vscode.ExtensionContext) => Promise<void> } = {297async [State.Disabled](context) {298await clearJsDebugAttachState(context);299},300301async [State.OnlyWithFlag](context) {302await createAttachServer(context);303},304305async [State.Smart](context) {306await createAttachServer(context);307},308309async [State.Always](context) {310await createAttachServer(context);311},312};313314/**315* Ensures the status bar text reflects the current state.316*/317function updateStatusBar(context: vscode.ExtensionContext, state: State, busy = false) {318if (state === State.Disabled && !busy) {319statusItem?.hide();320return;321}322323if (!statusItem) {324statusItem = vscode.window.createStatusBarItem('status.debug.autoAttach', vscode.StatusBarAlignment.Left);325statusItem.name = vscode.l10n.t("Debug Auto Attach");326statusItem.command = TOGGLE_COMMAND;327statusItem.tooltip = vscode.l10n.t("Automatically attach to node.js processes in debug mode");328context.subscriptions.push(statusItem);329}330331let text = busy ? '$(loading) ' : '';332text += isTemporarilyDisabled ? TEXT_TEMP_DISABLE_LABEL : TEXT_STATUSBAR_LABEL[state];333statusItem.text = text;334statusItem.show();335}336337/**338* Updates the auto attach feature based on the user or workspace setting339*/340function updateAutoAttach(newState: State) {341currentState = currentState.then(async ({ context, state: oldState }) => {342if (newState === oldState) {343return { context, state: oldState };344}345346if (oldState !== null) {347updateStatusBar(context, oldState, true);348}349350await transitions[newState](context);351isTemporarilyDisabled = false;352updateStatusBar(context, newState, false);353return { context, state: newState };354});355}356357/**358* Gets the IPC address for the server to listen on for js-debug sessions. This359* is cached such that we can reuse the address of previous activations.360*/361async function getIpcAddress(context: vscode.ExtensionContext) {362// Iff the `cachedData` is present, the js-debug registered environment363// variables for this workspace--cachedData is set after successfully364// invoking the attachment command.365const cachedIpc = context.workspaceState.get<CachedIpcState>(STORAGE_IPC);366367// We invalidate the IPC data if the js-debug path changes, since that368// indicates the extension was updated or reinstalled and the369// environment variables will have been lost.370// todo: make a way in the API to read environment data directly without activating js-debug?371const jsDebugPath =372vscode.extensions.getExtension('ms-vscode.js-debug-nightly')?.extensionPath ||373vscode.extensions.getExtension('ms-vscode.js-debug')?.extensionPath;374375const settingsValue = getJsDebugSettingKey();376if (cachedIpc?.jsDebugPath === jsDebugPath && cachedIpc?.settingsValue === settingsValue) {377return cachedIpc.ipcAddress;378}379380const result = await vscode.commands.executeCommand<{ ipcAddress: string }>(381'extension.js-debug.setAutoAttachVariables',382cachedIpc?.ipcAddress,383);384if (!result) {385return;386}387388const ipcAddress = result.ipcAddress;389await context.workspaceState.update(STORAGE_IPC, {390ipcAddress,391jsDebugPath,392settingsValue,393} satisfies CachedIpcState);394395return ipcAddress;396}397398function getJsDebugSettingKey() {399const o: { [key: string]: unknown } = {};400const config = vscode.workspace.getConfiguration(SETTING_SECTION);401for (const setting of SETTINGS_CAUSE_REFRESH) {402o[setting] = config.get(setting);403}404405return JSON.stringify(o);406}407408409