Skip to content

Commit 66744e3

Browse files
authored
debug: enable js-debug to auto attach (microsoft#95807)
* debug: enable js-debug to auto attach This modifies the debug-auto-launch extension to trigger js-debug as outlined in microsoft#88599 (comment) 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 microsoft#88599. * fix typo * clear js-debug environment variables when disabling auto attach
1 parent 044bf17 commit 66744e3

1 file changed

Lines changed: 187 additions & 66 deletions

File tree

extensions/debug-auto-launch/src/extension.ts

Lines changed: 187 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,61 @@
55

66
import * as vscode from 'vscode';
77
import * as nls from 'vscode-nls';
8+
import { createServer, Server } from 'net';
89

910
const localize = nls.loadMessageBundle();
10-
const ON_TEXT = localize('status.text.auto.attach.on', "Auto Attach: On");
11-
const OFF_TEXT = localize('status.text.auto.attach.off', "Auto Attach: Off");
11+
const ON_TEXT = localize('status.text.auto.attach.on', 'Auto Attach: On');
12+
const OFF_TEXT = localize('status.text.auto.attach.off', 'Auto Attach: Off');
1213

1314
const TOGGLE_COMMAND = 'extension.node-debug.toggleAutoAttach';
14-
const DEBUG_SETTINGS = 'debug.node';
15+
const JS_DEBUG_SETTINGS = 'debug.javascript';
16+
const JS_DEBUG_USEPREVIEW = 'usePreview';
17+
const JS_DEBUG_IPC_KEY = 'jsDebugIpcState';
18+
const NODE_DEBUG_SETTINGS = 'debug.node';
19+
const NODE_DEBUG_USEV3 = 'useV3';
1520
const AUTO_ATTACH_SETTING = 'autoAttach';
1621

1722
type AUTO_ATTACH_VALUES = 'disabled' | 'on' | 'off';
1823

19-
let currentState: AUTO_ATTACH_VALUES = 'disabled'; // on activation this feature is always disabled and
20-
let statusItem: vscode.StatusBarItem | undefined; // there is no status bar item
21-
let autoAttachStarted = false;
24+
const enum State {
25+
Disabled,
26+
Off,
27+
OnWithJsDebug,
28+
OnWithNodeDebug,
29+
}
2230

23-
export function activate(context: vscode.ExtensionContext): void {
31+
// on activation this feature is always disabled...
32+
let currentState = Promise.resolve({ state: State.Disabled, transitionData: null as unknown });
33+
let statusItem: vscode.StatusBarItem | undefined; // and there is no status bar item
2434

35+
export function activate(context: vscode.ExtensionContext): void {
2536
context.subscriptions.push(vscode.commands.registerCommand(TOGGLE_COMMAND, toggleAutoAttachSetting));
2637

27-
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => {
28-
if (e.affectsConfiguration(DEBUG_SETTINGS + '.' + AUTO_ATTACH_SETTING)) {
29-
updateAutoAttach(context);
30-
}
31-
}));
38+
// settings that can result in the "state" being changed--on/off/disable or useV3 toggles
39+
const effectualConfigurationSettings = [
40+
`${NODE_DEBUG_SETTINGS}.${AUTO_ATTACH_SETTING}`,
41+
`${NODE_DEBUG_SETTINGS}.${NODE_DEBUG_USEV3}`,
42+
`${JS_DEBUG_SETTINGS}.${JS_DEBUG_USEPREVIEW}`,
43+
];
44+
45+
context.subscriptions.push(
46+
vscode.workspace.onDidChangeConfiguration((e) => {
47+
if (effectualConfigurationSettings.some(setting => e.affectsConfiguration(setting))) {
48+
updateAutoAttach(context);
49+
}
50+
})
51+
);
3252

3353
updateAutoAttach(context);
3454
}
3555

36-
export function deactivate(): void {
56+
export async function deactivate(): Promise<void> {
57+
const { state, transitionData } = await currentState;
58+
await transitions[state].exit?.(transitionData);
3759
}
3860

39-
4061
function toggleAutoAttachSetting() {
41-
42-
const conf = vscode.workspace.getConfiguration(DEBUG_SETTINGS);
62+
const conf = vscode.workspace.getConfiguration(NODE_DEBUG_SETTINGS);
4363
if (conf) {
4464
let value = <AUTO_ATTACH_VALUES>conf.get(AUTO_ATTACH_SETTING);
4565
if (value === 'on') {
@@ -68,65 +88,166 @@ function toggleAutoAttachSetting() {
6888
}
6989
}
7090

91+
function readCurrentState(): State {
92+
const nodeConfig = vscode.workspace.getConfiguration(NODE_DEBUG_SETTINGS);
93+
const autoAttachState = <AUTO_ATTACH_VALUES>nodeConfig.get(AUTO_ATTACH_SETTING);
94+
switch (autoAttachState) {
95+
case 'off':
96+
return State.Off;
97+
case 'on':
98+
const jsDebugConfig = vscode.workspace.getConfiguration(JS_DEBUG_SETTINGS);
99+
const useV3 = nodeConfig.get(NODE_DEBUG_USEV3) || jsDebugConfig.get(JS_DEBUG_USEPREVIEW);
100+
return useV3 ? State.OnWithJsDebug : State.OnWithNodeDebug;
101+
case 'disabled':
102+
default:
103+
return State.Disabled;
104+
}
105+
}
106+
71107
/**
72-
* Updates the auto attach feature based on the user or workspace setting
108+
* Makes sure the status bar exists and is visible.
73109
*/
74-
function updateAutoAttach(context: vscode.ExtensionContext) {
75-
76-
const newState = <AUTO_ATTACH_VALUES>vscode.workspace.getConfiguration(DEBUG_SETTINGS).get(AUTO_ATTACH_SETTING);
110+
function ensureStatusBarExists(context: vscode.ExtensionContext) {
111+
if (!statusItem) {
112+
statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
113+
statusItem.command = TOGGLE_COMMAND;
114+
statusItem.tooltip = localize(
115+
'status.tooltip.auto.attach',
116+
'Automatically attach to node.js processes in debug mode'
117+
);
118+
statusItem.show();
119+
context.subscriptions.push(statusItem);
120+
} else {
121+
statusItem.show();
122+
}
77123

78-
if (newState !== currentState) {
124+
return statusItem;
125+
}
79126

80-
if (newState === 'disabled') {
127+
interface CachedIpcState {
128+
ipcAddress: string;
129+
jsDebugPath: string;
130+
}
81131

82-
// turn everything off
83-
if (statusItem) {
84-
statusItem.hide();
85-
statusItem.text = OFF_TEXT;
86-
}
87-
if (autoAttachStarted) {
88-
vscode.commands.executeCommand('extension.node-debug.stopAutoAttach').then(_ => {
89-
currentState = newState;
90-
autoAttachStarted = false;
91-
});
92-
}
132+
interface StateTransition<StateData> {
133+
exit?(stateData: StateData): Promise<void> | void;
134+
enter?(context: vscode.ExtensionContext): Promise<StateData> | StateData;
135+
}
93136

94-
} else { // 'on' or 'off'
95-
96-
// make sure status bar item exists and is visible
97-
if (!statusItem) {
98-
statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
99-
statusItem.command = TOGGLE_COMMAND;
100-
statusItem.tooltip = localize('status.tooltip.auto.attach', "Automatically attach to node.js processes in debug mode");
101-
statusItem.show();
102-
context.subscriptions.push(statusItem);
103-
} else {
104-
statusItem.show();
137+
/**
138+
* Map of logic that happens when auto attach states are entered and exited.
139+
* All state transitions are queued and run in order; promises are awaited.
140+
*/
141+
const transitions: { [S in State]: StateTransition<unknown> } = {
142+
[State.Disabled]: {
143+
async enter(context) {
144+
statusItem?.hide();
145+
146+
// If there was js-debug state set, clear it and clear any environment variables
147+
if (context.workspaceState.get<CachedIpcState>(JS_DEBUG_IPC_KEY)) {
148+
await context.workspaceState.update(JS_DEBUG_IPC_KEY, undefined);
149+
await vscode.commands.executeCommand('extension.js-debug.clearAutoAttachVariables');
105150
}
151+
},
152+
},
153+
154+
[State.Off]: {
155+
enter(context) {
156+
const statusItem = ensureStatusBarExists(context);
157+
statusItem.text = OFF_TEXT;
158+
},
159+
},
160+
161+
[State.OnWithNodeDebug]: {
162+
async enter(context) {
163+
const statusItem = ensureStatusBarExists(context);
164+
const vscode_pid = process.env['VSCODE_PID'];
165+
const rootPid = vscode_pid ? parseInt(vscode_pid) : 0;
166+
await vscode.commands.executeCommand('extension.node-debug.startAutoAttach', rootPid);
167+
statusItem.text = ON_TEXT;
168+
},
169+
170+
async exit() {
171+
await vscode.commands.executeCommand('extension.node-debug.stopAutoAttach');
172+
},
173+
},
174+
175+
[State.OnWithJsDebug]: {
176+
async enter(context) {
177+
const ipcAddress = await getIpcAddress(context);
178+
const server = await new Promise((resolve, reject) => {
179+
const s = createServer((socket) => {
180+
let data: Buffer[] = [];
181+
socket.on('data', (chunk) => data.push(chunk));
182+
socket.on('end', () =>
183+
vscode.commands.executeCommand(
184+
'extension.js-debug.autoAttachToProcess',
185+
JSON.parse(Buffer.concat(data).toString())
186+
)
187+
);
188+
})
189+
.on('error', reject)
190+
.listen(ipcAddress, () => resolve(s));
191+
});
192+
193+
const statusItem = ensureStatusBarExists(context);
194+
statusItem.text = ON_TEXT;
195+
return server;
196+
},
197+
198+
async exit(server: Server) {
199+
// we don't need to clear the environment variables--the bootloader will
200+
// no-op if the debug server is closed. This prevents having to reload
201+
// terminals if users want to turn it back on.
202+
await new Promise((resolve) => server.close(resolve));
203+
},
204+
},
205+
};
106206

107-
if (newState === 'off') {
108-
if (autoAttachStarted) {
109-
vscode.commands.executeCommand('extension.node-debug.stopAutoAttach').then(_ => {
110-
currentState = newState;
111-
if (statusItem) {
112-
statusItem.text = OFF_TEXT;
113-
}
114-
autoAttachStarted = false;
115-
});
116-
}
207+
/**
208+
* Updates the auto attach feature based on the user or workspace setting
209+
*/
210+
function updateAutoAttach(context: vscode.ExtensionContext) {
211+
const newState = readCurrentState();
117212

118-
} else if (newState === 'on') {
119-
120-
const vscode_pid = process.env['VSCODE_PID'];
121-
const rootPid = vscode_pid ? parseInt(vscode_pid) : 0;
122-
vscode.commands.executeCommand('extension.node-debug.startAutoAttach', rootPid).then(_ => {
123-
if (statusItem) {
124-
statusItem.text = ON_TEXT;
125-
}
126-
currentState = newState;
127-
autoAttachStarted = true;
128-
});
129-
}
213+
currentState = currentState.then(async ({ state: oldState, transitionData }) => {
214+
if (newState === oldState) {
215+
return { state: oldState, transitionData };
130216
}
217+
218+
await transitions[oldState].exit?.(transitionData);
219+
const newData = await transitions[newState].enter?.(context);
220+
221+
return { state: newState, transitionData: newData };
222+
});
223+
}
224+
225+
/**
226+
* Gets the IPC address for the server to listen on for js-debug sessions. This
227+
* is cached such that we can reuse the address of previous activations.
228+
*/
229+
async function getIpcAddress(context: vscode.ExtensionContext) {
230+
// Iff the `cachedData` is present, the js-debug registered environment
231+
// variables for this workspace--cachedData is set after successfully
232+
// invoking the attachment command.
233+
const cachedIpc = context.workspaceState.get<CachedIpcState>(JS_DEBUG_IPC_KEY);
234+
235+
// We invalidate the IPC data if the js-debug path changes, since that
236+
// indicates the extension was updated or reinstalled and the
237+
// environment variables will have been lost.
238+
// todo: make a way in the API to read environment data directly without activating js-debug?
239+
const jsDebugPath = vscode.extensions.getExtension('ms-vscode.js-debug-nightly')?.extensionPath
240+
|| vscode.extensions.getExtension('ms-vscode.js-debug')?.extensionPath;
241+
242+
if (cachedIpc && cachedIpc.jsDebugPath === jsDebugPath) {
243+
return cachedIpc.ipcAddress;
131244
}
245+
246+
const result = await vscode.commands.executeCommand<{ ipcAddress: string; }>(
247+
'extension.js-debug.setAutoAttachVariables'
248+
);
249+
250+
const ipcAddress = result!.ipcAddress;
251+
await context.workspaceState.update(JS_DEBUG_IPC_KEY, { ipcAddress, jsDebugPath });
252+
return ipcAddress;
132253
}

0 commit comments

Comments
 (0)