mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-27 12:04:04 +01:00
debug: enable js-debug to auto attach (#95807)
* 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. * fix typo * clear js-debug environment variables when disabling auto attach
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
// 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') {
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure the status bar exists and is visible.
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
return statusItem;
|
||||
}
|
||||
|
||||
interface CachedIpcState {
|
||||
ipcAddress: string;
|
||||
jsDebugPath: string;
|
||||
}
|
||||
|
||||
interface StateTransition<StateData> {
|
||||
exit?(stateData: StateData): Promise<void> | void;
|
||||
enter?(context: vscode.ExtensionContext): Promise<StateData> | StateData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the auto attach feature based on the user or workspace setting
|
||||
*/
|
||||
function updateAutoAttach(context: vscode.ExtensionContext) {
|
||||
const newState = readCurrentState();
|
||||
|
||||
const newState = <AUTO_ATTACH_VALUES>vscode.workspace.getConfiguration(DEBUG_SETTINGS).get(AUTO_ATTACH_SETTING);
|
||||
|
||||
if (newState !== currentState) {
|
||||
|
||||
if (newState === 'disabled') {
|
||||
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
|
||||
} 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();
|
||||
}
|
||||
|
||||
if (newState === 'off') {
|
||||
if (autoAttachStarted) {
|
||||
vscode.commands.executeCommand('extension.node-debug.stopAutoAttach').then(_ => {
|
||||
currentState = newState;
|
||||
if (statusItem) {
|
||||
statusItem.text = OFF_TEXT;
|
||||
}
|
||||
autoAttachStarted = false;
|
||||
});
|
||||
}
|
||||
|
||||
} 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user