Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
Comment thread
connor4312 marked this conversation as resolved.

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> } = {
Comment thread
connor4312 marked this conversation as resolved.
[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;
}