diff --git a/extensions/debug-auto-launch/src/extension.ts b/extensions/debug-auto-launch/src/extension.ts index e0fd5e76659a5..05ee2476a66b5 100644 --- a/extensions/debug-auto-launch/src/extension.ts +++ b/extensions/debug-auto-launch/src/extension.ts @@ -5,41 +5,61 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import { createServer, Server } from 'net'; const localize = nls.loadMessageBundle(); -const ON_TEXT = localize('status.text.auto.attach.on', "Auto Attach: On"); -const OFF_TEXT = localize('status.text.auto.attach.off', "Auto Attach: Off"); +const ON_TEXT = localize('status.text.auto.attach.on', 'Auto Attach: On'); +const OFF_TEXT = localize('status.text.auto.attach.off', 'Auto Attach: Off'); const TOGGLE_COMMAND = 'extension.node-debug.toggleAutoAttach'; -const DEBUG_SETTINGS = 'debug.node'; +const JS_DEBUG_SETTINGS = 'debug.javascript'; +const JS_DEBUG_USEPREVIEW = 'usePreview'; +const JS_DEBUG_IPC_KEY = 'jsDebugIpcState'; +const NODE_DEBUG_SETTINGS = 'debug.node'; +const NODE_DEBUG_USEV3 = 'useV3'; const AUTO_ATTACH_SETTING = 'autoAttach'; type AUTO_ATTACH_VALUES = 'disabled' | 'on' | 'off'; -let currentState: AUTO_ATTACH_VALUES = 'disabled'; // on activation this feature is always disabled and -let statusItem: vscode.StatusBarItem | undefined; // there is no status bar item -let autoAttachStarted = false; +const enum State { + Disabled, + Off, + OnWithJsDebug, + OnWithNodeDebug, +} -export function activate(context: vscode.ExtensionContext): void { +// on activation this feature is always disabled... +let currentState = Promise.resolve({ state: State.Disabled, transitionData: null as unknown }); +let statusItem: vscode.StatusBarItem | undefined; // and there is no status bar item +export function activate(context: vscode.ExtensionContext): void { context.subscriptions.push(vscode.commands.registerCommand(TOGGLE_COMMAND, toggleAutoAttachSetting)); - context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(DEBUG_SETTINGS + '.' + AUTO_ATTACH_SETTING)) { - updateAutoAttach(context); - } - })); + // settings that can result in the "state" being changed--on/off/disable or useV3 toggles + const effectualConfigurationSettings = [ + `${NODE_DEBUG_SETTINGS}.${AUTO_ATTACH_SETTING}`, + `${NODE_DEBUG_SETTINGS}.${NODE_DEBUG_USEV3}`, + `${JS_DEBUG_SETTINGS}.${JS_DEBUG_USEPREVIEW}`, + ]; + + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration((e) => { + if (effectualConfigurationSettings.some(setting => e.affectsConfiguration(setting))) { + updateAutoAttach(context); + } + }) + ); updateAutoAttach(context); } -export function deactivate(): void { +export async function deactivate(): Promise { + const { state, transitionData } = await currentState; + await transitions[state].exit?.(transitionData); } - function toggleAutoAttachSetting() { - - const conf = vscode.workspace.getConfiguration(DEBUG_SETTINGS); + const conf = vscode.workspace.getConfiguration(NODE_DEBUG_SETTINGS); if (conf) { let value = conf.get(AUTO_ATTACH_SETTING); if (value === 'on') { @@ -68,65 +88,166 @@ function toggleAutoAttachSetting() { } } +function readCurrentState(): State { + const nodeConfig = vscode.workspace.getConfiguration(NODE_DEBUG_SETTINGS); + const autoAttachState = nodeConfig.get(AUTO_ATTACH_SETTING); + switch (autoAttachState) { + case 'off': + return State.Off; + case 'on': + const jsDebugConfig = vscode.workspace.getConfiguration(JS_DEBUG_SETTINGS); + const useV3 = nodeConfig.get(NODE_DEBUG_USEV3) || jsDebugConfig.get(JS_DEBUG_USEPREVIEW); + return useV3 ? State.OnWithJsDebug : State.OnWithNodeDebug; + case 'disabled': + default: + return State.Disabled; + } +} + /** - * Updates the auto attach feature based on the user or workspace setting + * Makes sure the status bar exists and is visible. */ -function updateAutoAttach(context: vscode.ExtensionContext) { - - const newState = vscode.workspace.getConfiguration(DEBUG_SETTINGS).get(AUTO_ATTACH_SETTING); +function ensureStatusBarExists(context: vscode.ExtensionContext) { + if (!statusItem) { + statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + statusItem.command = TOGGLE_COMMAND; + statusItem.tooltip = localize( + 'status.tooltip.auto.attach', + 'Automatically attach to node.js processes in debug mode' + ); + statusItem.show(); + context.subscriptions.push(statusItem); + } else { + statusItem.show(); + } - if (newState !== currentState) { + return statusItem; +} - if (newState === 'disabled') { +interface CachedIpcState { + ipcAddress: string; + jsDebugPath: string; +} - // turn everything off - if (statusItem) { - statusItem.hide(); - statusItem.text = OFF_TEXT; - } - if (autoAttachStarted) { - vscode.commands.executeCommand('extension.node-debug.stopAutoAttach').then(_ => { - currentState = newState; - autoAttachStarted = false; - }); - } +interface StateTransition { + exit?(stateData: StateData): Promise | void; + enter?(context: vscode.ExtensionContext): Promise | StateData; +} - } else { // 'on' or 'off' - - // make sure status bar item exists and is visible - if (!statusItem) { - statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); - statusItem.command = TOGGLE_COMMAND; - statusItem.tooltip = localize('status.tooltip.auto.attach', "Automatically attach to node.js processes in debug mode"); - statusItem.show(); - context.subscriptions.push(statusItem); - } else { - statusItem.show(); +/** + * Map of logic that happens when auto attach states are entered and exited. + * All state transitions are queued and run in order; promises are awaited. + */ +const transitions: { [S in State]: StateTransition } = { + [State.Disabled]: { + async enter(context) { + statusItem?.hide(); + + // If there was js-debug state set, clear it and clear any environment variables + if (context.workspaceState.get(JS_DEBUG_IPC_KEY)) { + await context.workspaceState.update(JS_DEBUG_IPC_KEY, undefined); + await vscode.commands.executeCommand('extension.js-debug.clearAutoAttachVariables'); } + }, + }, + + [State.Off]: { + enter(context) { + const statusItem = ensureStatusBarExists(context); + statusItem.text = OFF_TEXT; + }, + }, + + [State.OnWithNodeDebug]: { + async enter(context) { + const statusItem = ensureStatusBarExists(context); + const vscode_pid = process.env['VSCODE_PID']; + const rootPid = vscode_pid ? parseInt(vscode_pid) : 0; + await vscode.commands.executeCommand('extension.node-debug.startAutoAttach', rootPid); + statusItem.text = ON_TEXT; + }, + + async exit() { + await vscode.commands.executeCommand('extension.node-debug.stopAutoAttach'); + }, + }, + + [State.OnWithJsDebug]: { + async enter(context) { + const ipcAddress = await getIpcAddress(context); + const server = await new Promise((resolve, reject) => { + const s = createServer((socket) => { + let data: Buffer[] = []; + socket.on('data', (chunk) => data.push(chunk)); + socket.on('end', () => + vscode.commands.executeCommand( + 'extension.js-debug.autoAttachToProcess', + JSON.parse(Buffer.concat(data).toString()) + ) + ); + }) + .on('error', reject) + .listen(ipcAddress, () => resolve(s)); + }); + + const statusItem = ensureStatusBarExists(context); + statusItem.text = ON_TEXT; + return server; + }, + + async exit(server: Server) { + // we don't need to clear the environment variables--the bootloader will + // no-op if the debug server is closed. This prevents having to reload + // terminals if users want to turn it back on. + await new Promise((resolve) => server.close(resolve)); + }, + }, +}; - if (newState === 'off') { - if (autoAttachStarted) { - vscode.commands.executeCommand('extension.node-debug.stopAutoAttach').then(_ => { - currentState = newState; - if (statusItem) { - statusItem.text = OFF_TEXT; - } - autoAttachStarted = false; - }); - } +/** + * Updates the auto attach feature based on the user or workspace setting + */ +function updateAutoAttach(context: vscode.ExtensionContext) { + const newState = readCurrentState(); - } else if (newState === 'on') { - - const vscode_pid = process.env['VSCODE_PID']; - const rootPid = vscode_pid ? parseInt(vscode_pid) : 0; - vscode.commands.executeCommand('extension.node-debug.startAutoAttach', rootPid).then(_ => { - if (statusItem) { - statusItem.text = ON_TEXT; - } - currentState = newState; - autoAttachStarted = true; - }); - } + currentState = currentState.then(async ({ state: oldState, transitionData }) => { + if (newState === oldState) { + return { state: oldState, transitionData }; } + + await transitions[oldState].exit?.(transitionData); + const newData = await transitions[newState].enter?.(context); + + return { state: newState, transitionData: newData }; + }); +} + +/** + * Gets the IPC address for the server to listen on for js-debug sessions. This + * is cached such that we can reuse the address of previous activations. + */ +async function getIpcAddress(context: vscode.ExtensionContext) { + // Iff the `cachedData` is present, the js-debug registered environment + // variables for this workspace--cachedData is set after successfully + // invoking the attachment command. + const cachedIpc = context.workspaceState.get(JS_DEBUG_IPC_KEY); + + // We invalidate the IPC data if the js-debug path changes, since that + // indicates the extension was updated or reinstalled and the + // environment variables will have been lost. + // todo: make a way in the API to read environment data directly without activating js-debug? + const jsDebugPath = vscode.extensions.getExtension('ms-vscode.js-debug-nightly')?.extensionPath + || vscode.extensions.getExtension('ms-vscode.js-debug')?.extensionPath; + + if (cachedIpc && cachedIpc.jsDebugPath === jsDebugPath) { + return cachedIpc.ipcAddress; } + + const result = await vscode.commands.executeCommand<{ ipcAddress: string; }>( + 'extension.js-debug.setAutoAttachVariables' + ); + + const ipcAddress = result!.ipcAddress; + await context.workspaceState.update(JS_DEBUG_IPC_KEY, { ipcAddress, jsDebugPath }); + return ipcAddress; }