From d4e6c788fb792aec015cf4a0113ced9afd5853cc Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 21 Apr 2020 16:07:41 -0700 Subject: [PATCH 1/3] debug: enable js-debug to auto attach This modifies the debug-auto-launch extension to trigger js-debug as outlined in https://github.com/microsoft/vscode/issues/88599#issuecomment-617242405 Since we now have four states, I moved the previous combinational logic to a `transitions` map, which is more clear and reliable. The state changes are also now a queue (in the form of a promise chain) which should avoid race conditions. There's some subtlety around how we cached the "ipcAddress" and know that environment variables are set. The core desire is being able to send a command to js-debug to set the environment variables only if they haven't previously been set--otherwise, reused the cached ones and the address. This process (in `getIpcAddress`) would be vastly simpler if extensions could read the environment variables that others provide, though there may be security considerations since secrets are sometimes stashed (though I could technically implement this today by manually creating and terminal and running the appropriate `echo $FOO` command). This seems to work fairly well in my testing. Fixes #88599. --- extensions/debug-auto-launch/src/extension.ts | 252 +++++++++++++----- 1 file changed, 185 insertions(+), 67 deletions(-) diff --git a/extensions/debug-auto-launch/src/extension.ts b/extensions/debug-auto-launch/src/extension.ts index e0fd5e76659a5..a3358e3d7ac78 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,163 @@ 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(); + // not strictly necessary, but clearing the cached state if autoattach is + // disabled provices an escape hatch if state gets corrupted somehow: + await context.workspaceState.update(JS_DEBUG_IPC_KEY, undefined); + }, + }, + + [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; } From c0b208be45a01bb391910922f70db3cf1215b8b3 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 22 Apr 2020 08:42:09 -0700 Subject: [PATCH 2/3] fix typo --- extensions/debug-auto-launch/src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/debug-auto-launch/src/extension.ts b/extensions/debug-auto-launch/src/extension.ts index a3358e3d7ac78..031d112b76044 100644 --- a/extensions/debug-auto-launch/src/extension.ts +++ b/extensions/debug-auto-launch/src/extension.ts @@ -143,7 +143,7 @@ const transitions: { [S in State]: StateTransition } = { async enter(context) { statusItem?.hide(); // not strictly necessary, but clearing the cached state if autoattach is - // disabled provices an escape hatch if state gets corrupted somehow: + // disabled provides an escape hatch if state gets corrupted somehow: await context.workspaceState.update(JS_DEBUG_IPC_KEY, undefined); }, }, From 324b76ee45b382ad1ce78326ac087f9ca6283ea5 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 22 Apr 2020 13:30:22 -0700 Subject: [PATCH 3/3] clear js-debug environment variables when disabling auto attach --- extensions/debug-auto-launch/src/extension.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/extensions/debug-auto-launch/src/extension.ts b/extensions/debug-auto-launch/src/extension.ts index 031d112b76044..05ee2476a66b5 100644 --- a/extensions/debug-auto-launch/src/extension.ts +++ b/extensions/debug-auto-launch/src/extension.ts @@ -142,9 +142,12 @@ const transitions: { [S in State]: StateTransition } = { [State.Disabled]: { async enter(context) { statusItem?.hide(); - // not strictly necessary, but clearing the cached state if autoattach is - // disabled provides an escape hatch if state gets corrupted somehow: - await context.workspaceState.update(JS_DEBUG_IPC_KEY, undefined); + + // 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'); + } }, },