|
5 | 5 |
|
6 | 6 | import * as vscode from 'vscode'; |
7 | 7 | import * as nls from 'vscode-nls'; |
| 8 | +import { createServer, Server } from 'net'; |
8 | 9 |
|
9 | 10 | 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'); |
12 | 13 |
|
13 | 14 | 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'; |
15 | 20 | const AUTO_ATTACH_SETTING = 'autoAttach'; |
16 | 21 |
|
17 | 22 | type AUTO_ATTACH_VALUES = 'disabled' | 'on' | 'off'; |
18 | 23 |
|
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 | +} |
22 | 30 |
|
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 |
24 | 34 |
|
| 35 | +export function activate(context: vscode.ExtensionContext): void { |
25 | 36 | context.subscriptions.push(vscode.commands.registerCommand(TOGGLE_COMMAND, toggleAutoAttachSetting)); |
26 | 37 |
|
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 | + ); |
32 | 52 |
|
33 | 53 | updateAutoAttach(context); |
34 | 54 | } |
35 | 55 |
|
36 | | -export function deactivate(): void { |
| 56 | +export async function deactivate(): Promise<void> { |
| 57 | + const { state, transitionData } = await currentState; |
| 58 | + await transitions[state].exit?.(transitionData); |
37 | 59 | } |
38 | 60 |
|
39 | | - |
40 | 61 | function toggleAutoAttachSetting() { |
41 | | - |
42 | | - const conf = vscode.workspace.getConfiguration(DEBUG_SETTINGS); |
| 62 | + const conf = vscode.workspace.getConfiguration(NODE_DEBUG_SETTINGS); |
43 | 63 | if (conf) { |
44 | 64 | let value = <AUTO_ATTACH_VALUES>conf.get(AUTO_ATTACH_SETTING); |
45 | 65 | if (value === 'on') { |
@@ -68,65 +88,166 @@ function toggleAutoAttachSetting() { |
68 | 88 | } |
69 | 89 | } |
70 | 90 |
|
| 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 | + |
71 | 107 | /** |
72 | | - * Updates the auto attach feature based on the user or workspace setting |
| 108 | + * Makes sure the status bar exists and is visible. |
73 | 109 | */ |
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 | + } |
77 | 123 |
|
78 | | - if (newState !== currentState) { |
| 124 | + return statusItem; |
| 125 | +} |
79 | 126 |
|
80 | | - if (newState === 'disabled') { |
| 127 | +interface CachedIpcState { |
| 128 | + ipcAddress: string; |
| 129 | + jsDebugPath: string; |
| 130 | +} |
81 | 131 |
|
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 | +} |
93 | 136 |
|
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'); |
105 | 150 | } |
| 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 | +}; |
106 | 206 |
|
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(); |
117 | 212 |
|
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 }; |
130 | 216 | } |
| 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; |
131 | 244 | } |
| 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; |
132 | 253 | } |
0 commit comments