Skip to content

Commit

Permalink
debug: enable js-debug to auto attach (#95807)
Browse files Browse the repository at this point in the history
* debug: enable js-debug to auto attach

This modifies the debug-auto-launch extension to trigger js-debug as
outlined in #88599 (comment)

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.

* fix typo

* clear js-debug environment variables when disabling auto attach
  • Loading branch information
connor4312 authored Apr 23, 2020
1 parent 044bf17 commit 66744e3
Showing 1 changed file with 187 additions and 66 deletions.
253 changes: 187 additions & 66 deletions extensions/debug-auto-launch/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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 = <AUTO_ATTACH_VALUES>conf.get(AUTO_ATTACH_SETTING);
if (value === 'on') {
Expand Down Expand Up @@ -68,65 +88,166 @@ function toggleAutoAttachSetting() {
}
}

function readCurrentState(): State {
const nodeConfig = vscode.workspace.getConfiguration(NODE_DEBUG_SETTINGS);
const autoAttachState = <AUTO_ATTACH_VALUES>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 = <AUTO_ATTACH_VALUES>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<StateData> {
exit?(stateData: StateData): Promise<void> | void;
enter?(context: vscode.ExtensionContext): Promise<StateData> | 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<unknown> } = {
[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<CachedIpcState>(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<CachedIpcState>(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;
}

0 comments on commit 66744e3

Please sign in to comment.