mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-26 18:06:43 +01:00
328 lines
11 KiB
TypeScript
328 lines
11 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { createServer, Server } from 'net';
|
|
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 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';
|
|
|
|
|
|
type AUTO_ATTACH_VALUES = 'disabled' | 'on' | 'off';
|
|
|
|
const enum State {
|
|
Disabled,
|
|
Off,
|
|
OnWithJsDebug,
|
|
OnWithNodeDebug,
|
|
}
|
|
|
|
// on activation this feature is always disabled...
|
|
let currentState: Promise<{ context: vscode.ExtensionContext, state: State; transitionData: unknown }>;
|
|
let statusItem: vscode.StatusBarItem | undefined; // and there is no status bar item
|
|
|
|
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}`);
|
|
|
|
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;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
export async function deactivate(): Promise<void> {
|
|
const { context, state, transitionData } = await currentState;
|
|
await transitions[state].exit?.(context, transitionData);
|
|
}
|
|
|
|
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;
|
|
} else if (info.workspaceValue) {
|
|
target = 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;
|
|
}
|
|
}
|
|
}
|
|
conf.update(AUTO_ATTACH_SETTING, value, target);
|
|
}
|
|
}
|
|
|
|
function autoAttachWithJsDebug() {
|
|
const jsDebugConfig = vscode.workspace.getConfiguration(JS_DEBUG_SETTINGS);
|
|
return jsDebugConfig.get(JS_DEBUG_USEPREVIEWAA, true);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
async function clearJsDebugAttachState(context: vscode.ExtensionContext) {
|
|
await context.workspaceState.update(JS_DEBUG_IPC_KEY, undefined);
|
|
await vscode.commands.executeCommand('extension.js-debug.clearAutoAttachVariables');
|
|
}
|
|
|
|
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.
|
|
*/
|
|
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) {
|
|
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
|
|
*/
|
|
function updateAutoAttach() {
|
|
const newState = readCurrentState();
|
|
|
|
currentState = currentState.then(async ({ context, state: oldState, transitionData }) => {
|
|
if (newState === oldState) {
|
|
return { context, state: oldState, transitionData };
|
|
}
|
|
|
|
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 };
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
|
|
const settingsValue = getJsDebugSettingKey();
|
|
if (cachedIpc && cachedIpc.jsDebugPath === jsDebugPath && cachedIpc.settingsValue === settingsValue) {
|
|
return cachedIpc.ipcAddress;
|
|
}
|
|
|
|
const result = await vscode.commands.executeCommand<{ ipcAddress: string; }>(
|
|
'extension.js-debug.setAutoAttachVariables',
|
|
cachedIpc?.ipcAddress
|
|
);
|
|
if (!result) {
|
|
return;
|
|
}
|
|
|
|
const ipcAddress = result.ipcAddress;
|
|
await context.workspaceState.update(
|
|
JS_DEBUG_IPC_KEY,
|
|
{ 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) {
|
|
o[setting] = config.get(setting);
|
|
}
|
|
|
|
return JSON.stringify(o);
|
|
}
|