未验证 提交 923189c7 编写于 作者: C Connor Peet

Merge branch 'connor4312/auto-attach-js-debug'

......@@ -17,33 +17,6 @@
"watch": "gulp watch-extension:debug-auto-launch"
},
"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": [
{
"command": "extension.node-debug.toggleAutoAttach",
......@@ -57,5 +30,11 @@
},
"devDependencies": {
"@types/node": "^12.11.7"
},
"prettier": {
"printWidth": 100,
"trailingComma": "all",
"singleQuote": true,
"arrowParens": "avoid"
}
}
{
"displayName": "Node Debug Auto-attach",
"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"
}
......@@ -8,121 +8,163 @@ import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
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 TEXT_ALWAYS = localize('status.text.auto.attach.always', 'Auto Attach: Always');
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_LABEL = {
[State.Disabled]: localize('debug.javascript.autoAttach.disabled.label', 'Disabled'),
[State.Always]: localize('debug.javascript.autoAttach.always.label', 'Always'),
[State.Smart]: localize('debug.javascript.autoAttach.smart.label', 'Smart'),
[State.OnlyWithFlag]: localize(
'debug.javascript.autoAttach.onlyWithFlag.label',
'Only 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 TEXT_TOGGLE_WORKSPACE = localize('scope.workspace', 'Toggle auto attach in this workspace');
const TEXT_TOGGLE_GLOBAL = localize('scope.global', 'Toggle auto attach on this machine');
const TOGGLE_COMMAND = 'extension.node-debug.toggleAutoAttach';
const JS_DEBUG_SETTINGS = 'debug.javascript';
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 STORAGE_IPC = 'jsDebugIpcState';
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 {
Disabled,
Off,
OnWithJsDebug,
OnWithNodeDebug,
Disabled = 'disabled',
OnlyWithFlag = 'onlyWithFlag',
Smart = 'smart',
Always = 'always',
}
// on activation this feature is always disabled...
let currentState: Promise<{ context: vscode.ExtensionContext, state: State; transitionData: unknown }>;
let currentState: Promise<{ context: vscode.ExtensionContext; state: State | null }>;
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 {
const previousState = context.workspaceState.get<State>(LAST_STATE_STORAGE_KEY, State.Disabled);
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}`);
currentState = Promise.resolve({ context, state: null });
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration((e) => {
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;
}
vscode.commands.registerCommand(TOGGLE_COMMAND, toggleAutoAttachSetting),
);
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 };
});
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();
updateAutoAttach(readCurrentState());
}
export async function deactivate(): Promise<void> {
const { context, state, transitionData } = await currentState;
await transitions[state].exit?.(context, transitionData);
await destroyAttachServer();
}
function toggleAutoAttachSetting() {
const conf = vscode.workspace.getConfiguration(NODE_DEBUG_SETTINGS);
if (conf) {
let value = <AUTO_ATTACH_VALUES>conf.get(AUTO_ATTACH_SETTING);
if (value === 'on') {
value = 'off';
} else {
value = 'on';
}
const info = conf.inspect(AUTO_ATTACH_SETTING);
let target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global;
if (info) {
if (info.workspaceFolderValue) {
target = vscode.ConfigurationTarget.WorkspaceFolder;
function getDefaultScope(info: ReturnType<vscode.WorkspaceConfiguration['inspect']>) {
if (!info) {
return vscode.ConfigurationTarget.Global;
} else if (info.workspaceFolderValue) {
return vscode.ConfigurationTarget.WorkspaceFolder;
} else if (info.workspaceValue) {
target = vscode.ConfigurationTarget.Workspace;
return vscode.ConfigurationTarget.Workspace;
} else if (info.globalValue) {
target = vscode.ConfigurationTarget.Global;
} else if (info.defaultValue) {
// setting not yet used: store setting in workspace
if (vscode.workspace.workspaceFolders) {
target = vscode.ConfigurationTarget.Workspace;
}
return vscode.ConfigurationTarget.Global;
}
return vscode.ConfigurationTarget.Global;
}
type PickResult = { state: State } | { scope: vscode.ConfigurationTarget } | undefined;
async function toggleAutoAttachSetting(scope?: vscode.ConfigurationTarget): Promise<void> {
const section = vscode.workspace.getConfiguration(SETTING_SECTION);
scope = scope || getDefaultScope(section.inspect(SETTING_STATE));
const isGlobalScope = scope === vscode.ConfigurationTarget.Global;
const quickPick = vscode.window.createQuickPick<vscode.QuickPickItem & { state: State }>();
const current = readCurrentState();
quickPick.items = [State.Always, State.Smart, State.OnlyWithFlag, State.Disabled].map(state => ({
state,
label: TEXT_STATE_LABEL[state],
description: TEXT_STATE_DESCRIPTION[state],
alwaysShow: true,
}));
quickPick.activeItems = quickPick.items.filter(i => i.state === current);
quickPick.title = isGlobalScope ? TEXT_TOGGLE_GLOBAL : TEXT_TOGGLE_WORKSPACE;
quickPick.buttons = [
{
iconPath: new vscode.ThemeIcon(isGlobalScope ? 'folder' : 'globe'),
tooltip: isGlobalScope ? TEXT_TOGGLE_WORKSPACE : TEXT_TOGGLE_GLOBAL,
},
];
quickPick.show();
const result = await new Promise<PickResult>(resolve => {
quickPick.onDidAccept(() => resolve(quickPick.selectedItems[0]));
quickPick.onDidHide(() => resolve());
quickPick.onDidTriggerButton(() => {
resolve({
scope: isGlobalScope
? vscode.ConfigurationTarget.Workspace
: vscode.ConfigurationTarget.Global,
});
});
});
quickPick.dispose();
if (!result) {
return;
}
conf.update(AUTO_ATTACH_SETTING, value, target);
if ('scope' in result) {
return await toggleAutoAttachSetting(result.scope);
}
}
function autoAttachWithJsDebug() {
const jsDebugConfig = vscode.workspace.getConfiguration(JS_DEBUG_SETTINGS);
return jsDebugConfig.get(JS_DEBUG_USEPREVIEWAA, true);
if ('state' in result) {
section.update(SETTING_STATE, result.state, scope);
}
}
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':
return autoAttachWithJsDebug() ? State.OnWithJsDebug : State.OnWithNodeDebug;
case 'disabled':
default:
return State.Disabled;
}
const section = vscode.workspace.getConfiguration(SETTING_SECTION);
return section.get<State>(SETTING_STATE) ?? State.Disabled;
}
/**
......@@ -134,7 +176,7 @@ function ensureStatusBarExists(context: vscode.ExtensionContext) {
statusItem.command = TOGGLE_COMMAND;
statusItem.tooltip = localize(
'status.tooltip.auto.attach',
'Automatically attach to node.js processes in debug mode'
'Automatically attach to node.js processes in debug mode',
);
statusItem.show();
context.subscriptions.push(statusItem);
......@@ -146,69 +188,27 @@ function ensureStatusBarExists(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 destroyAttachServer();
}
interface CachedIpcState {
ipcAddress: string;
jsDebugPath: 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.
* All state transitions are queued and run in order; promises are awaited.
* Turns auto attach on, and returns the server auto attach is listening on
* if it's successful.
*/
const transitions: { [S in State]: StateTransition<unknown> } = {
[State.Disabled]: makeTransition({
async enter(context) {
statusItem?.hide();
await clearJsDebugAttachState(context);
},
}),
[State.Off]: makeTransition({
enter(context) {
const statusItem = ensureStatusBarExists(context);
statusItem.text = OFF_TEXT;
},
}),
[State.OnWithNodeDebug]: makeTransition({
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]: makeTransition<Server | null>({
async enter(context) {
async function createAttachServer(context: vscode.ExtensionContext) {
const ipcAddress = await getIpcAddress(context);
if (!ipcAddress) {
return null;
return undefined;
}
const server = await new Promise<Server>((resolve, reject) => {
const s = createServer((socket) => {
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
socket.on('data', async chunk => {
if (chunk[chunk.length - 1] !== 0) {
// terminated with NUL byte
data.push(chunk);
return;
}
......@@ -218,7 +218,7 @@ const transitions: { [S in State]: StateTransition<unknown> } = {
try {
await vscode.commands.executeCommand(
'extension.js-debug.autoAttachToProcess',
JSON.parse(Buffer.concat(data).toString())
JSON.parse(Buffer.concat(data).toString()),
);
socket.write(Buffer.from([0]));
} catch (err) {
......@@ -229,51 +229,70 @@ const transitions: { [S in State]: StateTransition<unknown> } = {
})
.on('error', reject)
.listen(ipcAddress, () => resolve(s));
}).catch(console.error);
}).catch(err => {
console.error(err);
return undefined;
});
const statusItem = ensureStatusBarExists(context);
statusItem.text = ON_TEXT;
return server || null;
},
return await server;
}
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));
/**
* 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));
}
}
// but if they toggled auto attach use js-debug off, go ahead and do so
if (!autoAttachWithJsDebug()) {
interface CachedIpcState {
ipcAddress: string;
jsDebugPath: string;
settingsValue: string;
}
/**
* 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]: (context: vscode.ExtensionContext) => Promise<void> } = {
async [State.Disabled](context) {
await clearJsDebugAttachState(context);
}
statusItem?.hide();
},
async onActivate(context, currentState) {
if (currentState === State.OnWithNodeDebug || currentState === State.Disabled) {
await clearJsDebugAttachState(context);
}
}
}),
async [State.OnlyWithFlag](context) {
await createAttachServer(context);
const statusItem = ensureStatusBarExists(context);
statusItem.text = TEXT_WITH_FLAG;
},
async [State.Smart](context) {
await createAttachServer(context);
const statusItem = ensureStatusBarExists(context);
statusItem.text = TEXT_SMART;
},
async [State.Always](context) {
await createAttachServer(context);
const statusItem = ensureStatusBarExists(context);
statusItem.text = TEXT_ALWAYS;
},
};
/**
* Updates the auto attach feature based on the user or workspace setting
*/
function updateAutoAttach() {
const newState = readCurrentState();
currentState = currentState.then(async ({ context, state: oldState, transitionData }) => {
function updateAutoAttach(newState: State) {
currentState = currentState.then(async ({ context, state: oldState }) => {
if (newState === oldState) {
return { context, state: oldState, transitionData };
return { context, state: oldState };
}
await transitions[oldState].exit?.(context, transitionData);
const newData = await transitions[newState].enter?.(context);
await context.workspaceState.update(LAST_STATE_STORAGE_KEY, newState);
return { context, state: newState, transitionData: newData };
await transitions[newState](context);
return { context, state: newState };
});
}
......@@ -285,41 +304,43 @@ 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);
const cachedIpc = context.workspaceState.get<CachedIpcState>(STORAGE_IPC);
// 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;
const jsDebugPath =
vscode.extensions.getExtension('ms-vscode.js-debug-nightly')?.extensionPath ||
vscode.extensions.getExtension('ms-vscode.js-debug')?.extensionPath;
const settingsValue = getJsDebugSettingKey();
if (cachedIpc && cachedIpc.jsDebugPath === jsDebugPath && cachedIpc.settingsValue === settingsValue) {
if (cachedIpc?.jsDebugPath === jsDebugPath && cachedIpc?.settingsValue === settingsValue) {
return cachedIpc.ipcAddress;
}
const result = await vscode.commands.executeCommand<{ ipcAddress: string; }>(
const result = await vscode.commands.executeCommand<{ ipcAddress: string }>(
'extension.js-debug.setAutoAttachVariables',
cachedIpc?.ipcAddress
cachedIpc?.ipcAddress,
);
if (!result) {
return;
}
const ipcAddress = result.ipcAddress;
await context.workspaceState.update(
JS_DEBUG_IPC_KEY,
{ ipcAddress, jsDebugPath, settingsValue } as CachedIpcState,
);
await context.workspaceState.update(STORAGE_IPC, {
ipcAddress,
jsDebugPath,
settingsValue,
} as CachedIpcState);
return ipcAddress;
}
function getJsDebugSettingKey() {
let o: { [key: string]: unknown } = {};
const config = vscode.workspace.getConfiguration(JS_DEBUG_SETTINGS);
for (const setting of JS_DEBUG_REFRESH_SETTINGS) {
const config = vscode.workspace.getConfiguration(SETTING_SECTION);
for (const setting of SETTINGS_CAUSE_REFRESH) {
o[setting] = config.get(setting);
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册