debug: use only js-debug auto attach, collapse settings

This PR removes the hook in node-debug's auto attach, and uses only
js-debug auto attach. As referenced in the linked issues, this involves
removing `debug.javascript.usePreviewAutoAttach` and collapsing
`debug.node.autoAttach` into `debug.javascript.autoAttachFilter`. The
latter option gains a new state: `disabled`. Since there's no runtime
cost to having auto attach around, there is now no distinct off versus
disabled state.

The status bar item and the `Debug: Toggle Auto Attach` command now
open a quickpick, which looks like this:

![](https://memes.peet.io/img/20-09-9d2b6c0a-8b3f-4481-b2df-0753c54ee02b.png)

The current setting value is selected in the quickpick. If there is a
workspace setting for auto attach, the quickpick toggle the setting
there by default. Otherwise (as in the image) it will target the user
settings. The targeting is more explicit and defaults to the user
instead of the workspace, which should help reduce confusion (#97087).
Selecting the "scope change" item will reopen the quickpick in that
location.

Aside from the extra options for the `disabled` state in js-debug's
contributions, there's no changes required to it or its interaction
with debug-auto-launch.

Side note: I really wanted a separator between the states and the
scope change item, but this is not possible from an extension #74967.

Fixes https://github.com/microsoft/vscode/issues/105883
Fixes https://github.com/microsoft/vscode-js-debug/issues/732 (the rest of it)
Fixes https://github.com/microsoft/vscode/issues/105963
Fixes https://github.com/microsoft/vscode/issues/97087
This commit is contained in:
Connor Peet 2020-09-11 16:22:08 -07:00
parent 122fc9a1b8
commit cda3fbe7f8
No known key found for this signature in database
GPG key ID: CF8FD2EA0DBC61BD
3 changed files with 221 additions and 239 deletions

View file

@ -17,33 +17,6 @@
"watch": "gulp watch-extension:debug-auto-launch" "watch": "gulp watch-extension:debug-auto-launch"
}, },
"contributes": { "contributes": {
"configuration": {
"title": "Node debug",
"properties": {
"debug.node.autoAttach": {
"scope": "window",
"type": "string",
"enum": [
"disabled",
"on",
"off"
],
"enumDescriptions": [
"%debug.node.autoAttach.disabled.description%",
"%debug.node.autoAttach.on.description%",
"%debug.node.autoAttach.off.description%"
],
"description": "%debug.node.autoAttach.description%",
"default": "disabled"
},
"debug.javascript.usePreviewAutoAttach": {
"scope": "window",
"type": "boolean",
"default": true,
"description": "%debug.javascript.usePreviewAutoAttach%"
}
}
},
"commands": [ "commands": [
{ {
"command": "extension.node-debug.toggleAutoAttach", "command": "extension.node-debug.toggleAutoAttach",
@ -57,5 +30,11 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^12.11.7" "@types/node": "^12.11.7"
},
"prettier": {
"printWidth": 100,
"trailingComma": "all",
"singleQuote": true,
"arrowParens": "avoid"
} }
} }

View file

@ -1,12 +1,5 @@
{ {
"displayName": "Node Debug Auto-attach", "displayName": "Node Debug Auto-attach",
"description": "Helper for auto-attach feature when node-debug extensions are not active.", "description": "Helper for auto-attach feature when node-debug extensions are not active.",
"debug.node.autoAttach.description": "Automatically attach node debugger when node.js was launched in debug mode from integrated terminal.",
"debug.javascript.usePreviewAutoAttach": "Whether to use the preview debugger's version of auto attach.",
"debug.node.autoAttach.disabled.description": "Auto attach is disabled and not shown in status bar.",
"debug.node.autoAttach.on.description": "Auto attach is active.",
"debug.node.autoAttach.off.description": "Auto attach is inactive.",
"toggle.auto.attach": "Toggle Auto Attach" "toggle.auto.attach": "Toggle Auto Attach"
} }

View file

@ -8,121 +8,152 @@ import * as vscode from 'vscode';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
const ON_TEXT = localize('status.text.auto.attach.on', 'Auto Attach: On'); const TEXT_ALWAYS = localize('status.text.auto.attach.always', 'Auto Attach: Always');
const OFF_TEXT = localize('status.text.auto.attach.off', 'Auto Attach: Off'); const TEXT_SMART = localize('status.text.auto.attach.smart', 'Auto Attach: Smart');
const TEXT_WITH_FLAG = localize('status.text.auto.attach.withFlag', 'Auto Attach: With Flag');
const TEXT_STATE_DESCRIPTION = {
[State.Disabled]: localize(
'debug.javascript.autoAttach.disabled.description',
'Auto attach is disabled and not shown in status bar',
),
[State.Always]: localize(
'debug.javascript.autoAttach.always.description',
'Auto attach to every Node.js process launched in the terminal',
),
[State.Smart]: localize(
'debug.javascript.autoAttach.smart.description',
"Auto attach when running scripts that aren't in a node_modules folder",
),
[State.OnlyWithFlag]: localize(
'debug.javascript.autoAttach.onlyWithFlag.description',
'Only auto attach when the `--inspect` flag is given',
),
};
const TOGGLE_COMMAND = 'extension.node-debug.toggleAutoAttach'; const TOGGLE_COMMAND = 'extension.node-debug.toggleAutoAttach';
const JS_DEBUG_SETTINGS = 'debug.javascript'; const STORAGE_IPC = 'jsDebugIpcState';
const JS_DEBUG_USEPREVIEWAA = 'usePreviewAutoAttach';
const JS_DEBUG_IPC_KEY = 'jsDebugIpcState';
const JS_DEBUG_REFRESH_SETTINGS = ['autoAttachSmartPattern', 'autoAttachFilter']; // settings that, when changed, should cause us to refresh js-debug vars
const NODE_DEBUG_SETTINGS = 'debug.node';
const AUTO_ATTACH_SETTING = 'autoAttach';
const LAST_STATE_STORAGE_KEY = 'lastState';
const SETTING_SECTION = 'debug.javascript';
const SETTING_STATE = 'autoAttachFilter';
type AUTO_ATTACH_VALUES = 'disabled' | 'on' | 'off'; /**
* settings that, when changed, should cause us to refresh the state vars
*/
const SETTINGS_CAUSE_REFRESH = new Set(
['autoAttachSmartPattern', SETTING_STATE].map(s => `${SETTING_SECTION}.${s}`),
);
const enum State { const enum State {
Disabled, Disabled = 'disabled',
Off, OnlyWithFlag = 'onlyWithFlag',
OnWithJsDebug, Smart = 'smart',
OnWithNodeDebug, Always = 'always',
} }
// on activation this feature is always disabled... let currentState: Promise<{ context: vscode.ExtensionContext; state: State | null }>;
let currentState: Promise<{ context: vscode.ExtensionContext, state: State; transitionData: unknown }>;
let statusItem: vscode.StatusBarItem | undefined; // and there is no status bar item let statusItem: vscode.StatusBarItem | undefined; // and there is no status bar item
let server: Promise<Server | undefined> | undefined; // auto attach server
export function activate(context: vscode.ExtensionContext): void { export function activate(context: vscode.ExtensionContext): void {
const previousState = context.workspaceState.get<State>(LAST_STATE_STORAGE_KEY, State.Disabled); currentState = Promise.resolve({ context, state: null });
currentState = Promise.resolve(transitions[previousState].onActivate?.(context, readCurrentState()))
.then(() => ({ context, state: State.Disabled, transitionData: null }));
context.subscriptions.push(vscode.commands.registerCommand(TOGGLE_COMMAND, toggleAutoAttachSetting));
// settings that can result in the "state" being changed--on/off/disable or useV3 toggles
const effectualConfigurationSettings = [
`${NODE_DEBUG_SETTINGS}.${AUTO_ATTACH_SETTING}`,
`${JS_DEBUG_SETTINGS}.${JS_DEBUG_USEPREVIEWAA}`,
];
const refreshConfigurationSettings = JS_DEBUG_REFRESH_SETTINGS.map(s => `${JS_DEBUG_SETTINGS}.${s}`);
context.subscriptions.push( context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration((e) => { vscode.commands.registerCommand(TOGGLE_COMMAND, toggleAutoAttachSetting),
if (effectualConfigurationSettings.some(setting => e.affectsConfiguration(setting))) {
updateAutoAttach();
} else if (refreshConfigurationSettings.some(setting => e.affectsConfiguration(setting))) {
currentState = currentState.then(async s => {
if (s.state !== State.OnWithJsDebug) {
return s;
}
await transitions[State.OnWithJsDebug].exit?.(context, s.transitionData);
await clearJsDebugAttachState(context);
const transitionData = await transitions[State.OnWithJsDebug].enter?.(context);
return { context, state: State.OnWithJsDebug, transitionData };
});
}
})
); );
updateAutoAttach(); context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(e => {
// Whenever a setting is changed, disable auto attach, and re-enable
// it (if necessary) to refresh variables.
if (
e.affectsConfiguration(`${SETTING_SECTION}.${SETTING_STATE}`) ||
[...SETTINGS_CAUSE_REFRESH].some(setting => e.affectsConfiguration(setting))
) {
updateAutoAttach(State.Disabled);
updateAutoAttach(readCurrentState());
}
}),
);
updateAutoAttach(readCurrentState());
} }
export async function deactivate(): Promise<void> { export async function deactivate(): Promise<void> {
const { context, state, transitionData } = await currentState; await destroyAttachServer();
await transitions[state].exit?.(context, transitionData);
} }
function toggleAutoAttachSetting() { type StatePickItem =
const conf = vscode.workspace.getConfiguration(NODE_DEBUG_SETTINGS); | (vscode.QuickPickItem & { state: State })
if (conf) { | (vscode.QuickPickItem & { scope: vscode.ConfigurationTarget })
let value = <AUTO_ATTACH_VALUES>conf.get(AUTO_ATTACH_SETTING); | (vscode.QuickPickItem & { type: 'separator' });
if (value === 'on') {
value = 'off';
} else {
value = 'on';
}
const info = conf.inspect(AUTO_ATTACH_SETTING); function getDefaultScope(info: ReturnType<vscode.WorkspaceConfiguration['inspect']>) {
let target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global; if (!info) {
if (info) { return vscode.ConfigurationTarget.Global;
if (info.workspaceFolderValue) { } else if (info.workspaceFolderValue) {
target = vscode.ConfigurationTarget.WorkspaceFolder; return vscode.ConfigurationTarget.WorkspaceFolder;
} else if (info.workspaceValue) { } else if (info.workspaceValue) {
target = vscode.ConfigurationTarget.Workspace; return vscode.ConfigurationTarget.Workspace;
} else if (info.globalValue) { } else if (info.globalValue) {
target = vscode.ConfigurationTarget.Global; return vscode.ConfigurationTarget.Global;
} else if (info.defaultValue) {
// setting not yet used: store setting in workspace
if (vscode.workspace.workspaceFolders) {
target = vscode.ConfigurationTarget.Workspace;
}
}
}
conf.update(AUTO_ATTACH_SETTING, value, target);
} }
return vscode.ConfigurationTarget.Global;
} }
function autoAttachWithJsDebug() { async function toggleAutoAttachSetting(scope?: vscode.ConfigurationTarget): Promise<void> {
const jsDebugConfig = vscode.workspace.getConfiguration(JS_DEBUG_SETTINGS); const section = vscode.workspace.getConfiguration(SETTING_SECTION);
return jsDebugConfig.get(JS_DEBUG_USEPREVIEWAA, true); scope = scope || getDefaultScope(section.inspect(SETTING_STATE));
const stateItems = [State.Always, State.Smart, State.OnlyWithFlag, State.Disabled].map(state => ({
state,
label: state.slice(0, 1).toUpperCase() + state.slice(1),
description: TEXT_STATE_DESCRIPTION[state],
alwaysShow: true,
}));
const scopeItem =
scope === vscode.ConfigurationTarget.Global
? {
label: localize('scope.workspace', 'Toggle in this workspace $(arrow-right)'),
scope: vscode.ConfigurationTarget.Workspace,
}
: {
label: localize('scope.global', 'Toggle for this machine $(arrow-right)'),
scope: vscode.ConfigurationTarget.Global,
};
const quickPick = vscode.window.createQuickPick<StatePickItem>();
// todo: have a separator here, see https://github.com/microsoft/vscode/issues/74967
quickPick.items = [...stateItems, scopeItem];
quickPick.show();
const current = readCurrentState();
quickPick.activeItems = stateItems.filter(i => i.state === current);
const result = await new Promise<StatePickItem | undefined>(resolve => {
quickPick.onDidAccept(() => resolve(quickPick.selectedItems[0]));
quickPick.onDidHide(() => resolve());
});
quickPick.dispose();
if (!result) {
return;
}
if ('scope' in result) {
return await toggleAutoAttachSetting(result.scope);
}
if ('state' in result) {
section.update(SETTING_STATE, result.state, scope);
}
} }
function readCurrentState(): State { function readCurrentState(): State {
const nodeConfig = vscode.workspace.getConfiguration(NODE_DEBUG_SETTINGS); const section = vscode.workspace.getConfiguration(SETTING_SECTION);
const autoAttachState = <AUTO_ATTACH_VALUES>nodeConfig.get(AUTO_ATTACH_SETTING); return section.get<State>(SETTING_STATE) ?? State.Disabled;
switch (autoAttachState) {
case 'off':
return State.Off;
case 'on':
return autoAttachWithJsDebug() ? State.OnWithJsDebug : State.OnWithNodeDebug;
case 'disabled':
default:
return State.Disabled;
}
} }
/** /**
@ -134,7 +165,7 @@ function ensureStatusBarExists(context: vscode.ExtensionContext) {
statusItem.command = TOGGLE_COMMAND; statusItem.command = TOGGLE_COMMAND;
statusItem.tooltip = localize( statusItem.tooltip = localize(
'status.tooltip.auto.attach', 'status.tooltip.auto.attach',
'Automatically attach to node.js processes in debug mode' 'Automatically attach to node.js processes in debug mode',
); );
statusItem.show(); statusItem.show();
context.subscriptions.push(statusItem); context.subscriptions.push(statusItem);
@ -146,8 +177,63 @@ function ensureStatusBarExists(context: vscode.ExtensionContext) {
} }
async function clearJsDebugAttachState(context: vscode.ExtensionContext) { async function clearJsDebugAttachState(context: vscode.ExtensionContext) {
await context.workspaceState.update(JS_DEBUG_IPC_KEY, undefined); await context.workspaceState.update(STORAGE_IPC, undefined);
await vscode.commands.executeCommand('extension.js-debug.clearAutoAttachVariables'); await vscode.commands.executeCommand('extension.js-debug.clearAutoAttachVariables');
await destroyAttachServer();
}
/**
* Turns auto attach on, and returns the server auto attach is listening on
* if it's successful.
*/
async function createAttachServer(context: vscode.ExtensionContext) {
const ipcAddress = await getIpcAddress(context);
if (!ipcAddress) {
return undefined;
}
server = new Promise<Server>((resolve, reject) => {
const s = createServer(socket => {
let data: Buffer[] = [];
socket.on('data', async chunk => {
if (chunk[chunk.length - 1] !== 0) {
// terminated with NUL byte
data.push(chunk);
return;
}
data.push(chunk.slice(0, -1));
try {
await vscode.commands.executeCommand(
'extension.js-debug.autoAttachToProcess',
JSON.parse(Buffer.concat(data).toString()),
);
socket.write(Buffer.from([0]));
} catch (err) {
socket.write(Buffer.from([1]));
console.error(err);
}
});
})
.on('error', reject)
.listen(ipcAddress, () => resolve(s));
}).catch(err => {
console.error(err);
return undefined;
});
return await server;
}
/**
* Destroys the auto-attach server, if it's running.
*/
async function destroyAttachServer() {
const instance = await server;
if (instance) {
await new Promise(r => instance.close(r));
}
} }
interface CachedIpcState { interface CachedIpcState {
@ -156,124 +242,46 @@ interface CachedIpcState {
settingsValue: string; settingsValue: string;
} }
interface StateTransition<StateData> {
onActivate?(context: vscode.ExtensionContext, currentState: State): Promise<void>;
exit?(context: vscode.ExtensionContext, stateData: StateData): Promise<void> | void;
enter?(context: vscode.ExtensionContext): Promise<StateData> | StateData;
}
const makeTransition = <T>(tsn: StateTransition<T>) => tsn; // helper to apply generic type
/** /**
* Map of logic that happens when auto attach states are entered and exited. * 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. * All state transitions are queued and run in order; promises are awaited.
*/ */
const transitions: { [S in State]: StateTransition<unknown> } = { const transitions: { [S in State]: (context: vscode.ExtensionContext) => Promise<void> } = {
[State.Disabled]: makeTransition({ async [State.Disabled](context) {
async enter(context) { await clearJsDebugAttachState(context);
statusItem?.hide(); statusItem?.hide();
await clearJsDebugAttachState(context); },
},
}),
[State.Off]: makeTransition({ async [State.OnlyWithFlag](context) {
enter(context) { await createAttachServer(context);
const statusItem = ensureStatusBarExists(context); const statusItem = ensureStatusBarExists(context);
statusItem.text = OFF_TEXT; statusItem.text = TEXT_WITH_FLAG;
}, },
}),
[State.OnWithNodeDebug]: makeTransition({ async [State.Smart](context) {
async enter(context) { await createAttachServer(context);
const statusItem = ensureStatusBarExists(context); const statusItem = ensureStatusBarExists(context);
const vscode_pid = process.env['VSCODE_PID']; statusItem.text = TEXT_SMART;
const rootPid = vscode_pid ? parseInt(vscode_pid) : 0; },
await vscode.commands.executeCommand('extension.node-debug.startAutoAttach', rootPid);
statusItem.text = ON_TEXT;
},
async exit() { async [State.Always](context) {
await vscode.commands.executeCommand('extension.node-debug.stopAutoAttach'); await createAttachServer(context);
}, const statusItem = ensureStatusBarExists(context);
}), statusItem.text = TEXT_ALWAYS;
},
[State.OnWithJsDebug]: makeTransition<Server | null>({
async enter(context) {
const ipcAddress = await getIpcAddress(context);
if (!ipcAddress) {
return null;
}
const server = await new Promise<Server>((resolve, reject) => {
const s = createServer((socket) => {
let data: Buffer[] = [];
socket.on('data', async (chunk) => {
if (chunk[chunk.length - 1] !== 0) { // terminated with NUL byte
data.push(chunk);
return;
}
data.push(chunk.slice(0, -1));
try {
await vscode.commands.executeCommand(
'extension.js-debug.autoAttachToProcess',
JSON.parse(Buffer.concat(data).toString())
);
socket.write(Buffer.from([0]));
} catch (err) {
socket.write(Buffer.from([1]));
console.error(err);
}
});
})
.on('error', reject)
.listen(ipcAddress, () => resolve(s));
}).catch(console.error);
const statusItem = ensureStatusBarExists(context);
statusItem.text = ON_TEXT;
return server || null;
},
async exit(context, 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.
if (server) {
await new Promise((resolve) => server.close(resolve));
}
// but if they toggled auto attach use js-debug off, go ahead and do so
if (!autoAttachWithJsDebug()) {
await clearJsDebugAttachState(context);
}
},
async onActivate(context, currentState) {
if (currentState === State.OnWithNodeDebug || currentState === State.Disabled) {
await clearJsDebugAttachState(context);
}
}
}),
}; };
/** /**
* Updates the auto attach feature based on the user or workspace setting * Updates the auto attach feature based on the user or workspace setting
*/ */
function updateAutoAttach() { function updateAutoAttach(newState: State) {
const newState = readCurrentState(); currentState = currentState.then(async ({ context, state: oldState }) => {
currentState = currentState.then(async ({ context, state: oldState, transitionData }) => {
if (newState === oldState) { if (newState === oldState) {
return { context, state: oldState, transitionData }; return { context, state: oldState };
} }
await transitions[oldState].exit?.(context, transitionData); await transitions[newState](context);
const newData = await transitions[newState].enter?.(context); return { context, state: newState };
await context.workspaceState.update(LAST_STATE_STORAGE_KEY, newState);
return { context, state: newState, transitionData: newData };
}); });
} }
@ -285,41 +293,43 @@ async function getIpcAddress(context: vscode.ExtensionContext) {
// Iff the `cachedData` is present, the js-debug registered environment // Iff the `cachedData` is present, the js-debug registered environment
// variables for this workspace--cachedData is set after successfully // variables for this workspace--cachedData is set after successfully
// invoking the attachment command. // invoking the attachment command.
const cachedIpc = context.workspaceState.get<CachedIpcState>(JS_DEBUG_IPC_KEY); const cachedIpc = context.workspaceState.get<CachedIpcState>(STORAGE_IPC);
// We invalidate the IPC data if the js-debug path changes, since that // We invalidate the IPC data if the js-debug path changes, since that
// indicates the extension was updated or reinstalled and the // indicates the extension was updated or reinstalled and the
// environment variables will have been lost. // environment variables will have been lost.
// todo: make a way in the API to read environment data directly without activating js-debug? // 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 const jsDebugPath =
|| vscode.extensions.getExtension('ms-vscode.js-debug')?.extensionPath; vscode.extensions.getExtension('ms-vscode.js-debug-nightly')?.extensionPath ||
vscode.extensions.getExtension('ms-vscode.js-debug')?.extensionPath;
const settingsValue = getJsDebugSettingKey(); const settingsValue = getJsDebugSettingKey();
if (cachedIpc && cachedIpc.jsDebugPath === jsDebugPath && cachedIpc.settingsValue === settingsValue) { if (cachedIpc?.jsDebugPath === jsDebugPath && cachedIpc?.settingsValue === settingsValue) {
return cachedIpc.ipcAddress; return cachedIpc.ipcAddress;
} }
const result = await vscode.commands.executeCommand<{ ipcAddress: string; }>( const result = await vscode.commands.executeCommand<{ ipcAddress: string }>(
'extension.js-debug.setAutoAttachVariables', 'extension.js-debug.setAutoAttachVariables',
cachedIpc?.ipcAddress cachedIpc?.ipcAddress,
); );
if (!result) { if (!result) {
return; return;
} }
const ipcAddress = result.ipcAddress; const ipcAddress = result.ipcAddress;
await context.workspaceState.update( await context.workspaceState.update(STORAGE_IPC, {
JS_DEBUG_IPC_KEY, ipcAddress,
{ ipcAddress, jsDebugPath, settingsValue } as CachedIpcState, jsDebugPath,
); settingsValue,
} as CachedIpcState);
return ipcAddress; return ipcAddress;
} }
function getJsDebugSettingKey() { function getJsDebugSettingKey() {
let o: { [key: string]: unknown } = {}; let o: { [key: string]: unknown } = {};
const config = vscode.workspace.getConfiguration(JS_DEBUG_SETTINGS); const config = vscode.workspace.getConfiguration(SETTING_SECTION);
for (const setting of JS_DEBUG_REFRESH_SETTINGS) { for (const setting of SETTINGS_CAUSE_REFRESH) {
o[setting] = config.get(setting); o[setting] = config.get(setting);
} }