Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/node/extensionHostProcess.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import minimist from 'minimist';
7
import * as nativeWatchdog from 'native-watchdog';
8
import * as net from 'net';
9
import { ProcessTimeRunOnceScheduler } from '../../../base/common/async.js';
10
import { VSBuffer } from '../../../base/common/buffer.js';
11
import { PendingMigrationError, isCancellationError, isSigPipeError, onUnexpectedError, onUnexpectedExternalError } from '../../../base/common/errors.js';
12
import { Event } from '../../../base/common/event.js';
13
import * as performance from '../../../base/common/performance.js';
14
import { IURITransformer } from '../../../base/common/uriIpc.js';
15
import { Promises } from '../../../base/node/pfs.js';
16
import { IMessagePassingProtocol } from '../../../base/parts/ipc/common/ipc.js';
17
import { BufferedEmitter, PersistentProtocol, ProtocolConstants } from '../../../base/parts/ipc/common/ipc.net.js';
18
import { NodeSocket, WebSocketNodeSocket } from '../../../base/parts/ipc/node/ipc.net.js';
19
import type { MessagePortMain, MessageEvent as UtilityMessageEvent } from '../../../base/parts/sandbox/node/electronTypes.js';
20
import { boolean } from '../../../editor/common/config/editorOptions.js';
21
import product from '../../../platform/product/common/product.js';
22
import { ExtensionHostMain, IExitFn } from '../common/extensionHostMain.js';
23
import { IHostUtils } from '../common/extHostExtensionService.js';
24
import { createURITransformer } from './uriTransformer.js';
25
import { ExtHostConnectionType, readExtHostConnection } from '../../services/extensions/common/extensionHostEnv.js';
26
import { ExtensionHostExitCode, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, IExtHostSocketMessage, IExtensionHostInitData, MessageType, createMessageOfType, isMessageOfType } from '../../services/extensions/common/extensionHostProtocol.js';
27
import { IDisposable } from '../../../base/common/lifecycle.js';
28
import '../common/extHost.common.services.js';
29
import './extHost.node.services.js';
30
import { createRequire } from 'node:module';
31
const require = createRequire(import.meta.url);
32
33
interface ParsedExtHostArgs {
34
transformURIs?: boolean;
35
skipWorkspaceStorageLock?: boolean;
36
supportGlobalNavigator?: boolean; // enable global navigator object in nodejs
37
useHostProxy?: 'true' | 'false'; // use a string, as undefined is also a valid value
38
}
39
40
// silence experimental warnings when in development
41
if (process.env.VSCODE_DEV) {
42
const warningListeners = process.listeners('warning');
43
process.removeAllListeners('warning');
44
process.on('warning', (warning: any) => {
45
if (warning.code === 'ExperimentalWarning' || warning.name === 'ExperimentalWarning') {
46
return;
47
}
48
49
warningListeners[0](warning);
50
});
51
}
52
53
// workaround for https://github.com/microsoft/vscode/issues/85490
54
// remove --inspect-port=0 after start so that it doesn't trigger LSP debugging
55
(function removeInspectPort() {
56
for (let i = 0; i < process.execArgv.length; i++) {
57
if (process.execArgv[i] === '--inspect-port=0') {
58
process.execArgv.splice(i, 1);
59
i--;
60
}
61
}
62
})();
63
64
const args = minimist(process.argv.slice(2), {
65
boolean: [
66
'transformURIs',
67
'skipWorkspaceStorageLock',
68
'supportGlobalNavigator',
69
],
70
string: [
71
'useHostProxy' // 'true' | 'false' | undefined
72
]
73
}) as ParsedExtHostArgs;
74
75
// With Electron 2.x and node.js 8.x the "natives" module
76
// can cause a native crash (see https://github.com/nodejs/node/issues/19891 and
77
// https://github.com/electron/electron/issues/10905). To prevent this from
78
// happening we essentially blocklist this module from getting loaded in any
79
// extension by patching the node require() function.
80
(function () {
81
const Module = require('module');
82
const originalLoad = Module._load;
83
84
Module._load = function (request: string) {
85
if (request === 'natives') {
86
throw new Error('Either the extension or an NPM dependency is using the [unsupported "natives" node module](https://go.microsoft.com/fwlink/?linkid=871887).');
87
}
88
89
return originalLoad.apply(this, arguments);
90
};
91
})();
92
93
// custom process.exit logic...
94
const nativeExit: IExitFn = process.exit.bind(process);
95
const nativeOn = process.on.bind(process);
96
function patchProcess(allowExit: boolean) {
97
process.exit = function (code?: number) {
98
if (allowExit) {
99
nativeExit(code);
100
} else {
101
const err = new Error('An extension called process.exit() and this was prevented.');
102
console.warn(err.stack);
103
}
104
} as (code?: number) => never;
105
106
// override Electron's process.crash() method
107
(process as any /* bypass layer checker */).crash = function () {
108
const err = new Error('An extension called process.crash() and this was prevented.');
109
console.warn(err.stack);
110
};
111
112
// Set ELECTRON_RUN_AS_NODE environment variable for extensions that use
113
// child_process.spawn with process.execPath and expect to run as node process
114
// on the desktop.
115
// Refs https://github.com/microsoft/vscode/issues/151012#issuecomment-1156593228
116
process.env['ELECTRON_RUN_AS_NODE'] = '1';
117
118
process.on = <any>function (event: string, listener: (...args: any[]) => void) {
119
if (event === 'uncaughtException') {
120
const actualListener = listener;
121
listener = function (...args: any[]) {
122
try {
123
return actualListener.apply(undefined, args);
124
} catch {
125
// DO NOT HANDLE NOR PRINT the error here because this can and will lead to
126
// more errors which will cause error handling to be reentrant and eventually
127
// overflowing the stack. Do not be sad, we do handle and annotate uncaught
128
// errors properly in 'extensionHostMain'
129
}
130
};
131
}
132
nativeOn(event, listener);
133
};
134
135
}
136
137
// NodeJS since v21 defines navigator as a global object. This will likely surprise many extensions and potentially break them
138
// because `navigator` has historically often been used to check if running in a browser (vs running inside NodeJS)
139
if (!args.supportGlobalNavigator) {
140
Object.defineProperty(globalThis, 'navigator', {
141
get: () => {
142
onUnexpectedExternalError(new PendingMigrationError('navigator is now a global in nodejs, please see https://aka.ms/vscode-extensions/navigator for additional info on this error.'));
143
return undefined;
144
}
145
});
146
}
147
148
149
interface IRendererConnection {
150
protocol: IMessagePassingProtocol;
151
initData: IExtensionHostInitData;
152
}
153
154
// This calls exit directly in case the initialization is not finished and we need to exit
155
// Otherwise, if initialization completed we go to extensionHostMain.terminate()
156
let onTerminate = function (reason: string) {
157
nativeExit();
158
};
159
160
function _createExtHostProtocol(): Promise<IMessagePassingProtocol> {
161
const extHostConnection = readExtHostConnection(process.env);
162
163
if (extHostConnection.type === ExtHostConnectionType.MessagePort) {
164
165
return new Promise<IMessagePassingProtocol>((resolve, reject) => {
166
167
const withPorts = (ports: MessagePortMain[]) => {
168
const port = ports[0];
169
const onMessage = new BufferedEmitter<VSBuffer>();
170
port.on('message', (e) => onMessage.fire(VSBuffer.wrap(e.data)));
171
port.on('close', () => {
172
onTerminate('renderer closed the MessagePort');
173
});
174
port.start();
175
176
resolve({
177
onMessage: onMessage.event,
178
send: message => port.postMessage(message.buffer)
179
});
180
};
181
182
(process as unknown as { parentPort: { on: (event: 'message', listener: (messageEvent: UtilityMessageEvent) => void) => void } }).parentPort.on('message', (e: UtilityMessageEvent) => withPorts(e.ports));
183
});
184
185
} else if (extHostConnection.type === ExtHostConnectionType.Socket) {
186
187
return new Promise<PersistentProtocol>((resolve, reject) => {
188
189
let protocol: PersistentProtocol | null = null;
190
191
const timer = setTimeout(() => {
192
onTerminate('VSCODE_EXTHOST_IPC_SOCKET timeout');
193
}, 60000);
194
195
const reconnectionGraceTime = ProtocolConstants.ReconnectionGraceTime;
196
const reconnectionShortGraceTime = ProtocolConstants.ReconnectionShortGraceTime;
197
const disconnectRunner1 = new ProcessTimeRunOnceScheduler(() => onTerminate('renderer disconnected for too long (1)'), reconnectionGraceTime);
198
const disconnectRunner2 = new ProcessTimeRunOnceScheduler(() => onTerminate('renderer disconnected for too long (2)'), reconnectionShortGraceTime);
199
200
process.on('message', (msg: IExtHostSocketMessage | IExtHostReduceGraceTimeMessage, handle: net.Socket) => {
201
if (msg && msg.type === 'VSCODE_EXTHOST_IPC_SOCKET') {
202
// Disable Nagle's algorithm. We also do this on the server process,
203
// but nodejs doesn't document if this option is transferred with the socket
204
handle.setNoDelay(true);
205
206
const initialDataChunk = VSBuffer.wrap(Buffer.from(msg.initialDataChunk, 'base64'));
207
let socket: NodeSocket | WebSocketNodeSocket;
208
if (msg.skipWebSocketFrames) {
209
socket = new NodeSocket(handle, 'extHost-socket');
210
} else {
211
const inflateBytes = VSBuffer.wrap(Buffer.from(msg.inflateBytes, 'base64'));
212
socket = new WebSocketNodeSocket(new NodeSocket(handle, 'extHost-socket'), msg.permessageDeflate, inflateBytes, false);
213
}
214
if (protocol) {
215
// reconnection case
216
disconnectRunner1.cancel();
217
disconnectRunner2.cancel();
218
protocol.beginAcceptReconnection(socket, initialDataChunk);
219
protocol.endAcceptReconnection();
220
protocol.sendResume();
221
} else {
222
clearTimeout(timer);
223
protocol = new PersistentProtocol({ socket, initialChunk: initialDataChunk });
224
protocol.sendResume();
225
protocol.onDidDispose(() => onTerminate('renderer disconnected'));
226
resolve(protocol);
227
228
// Wait for rich client to reconnect
229
protocol.onSocketClose(() => {
230
// The socket has closed, let's give the renderer a certain amount of time to reconnect
231
disconnectRunner1.schedule();
232
});
233
}
234
}
235
if (msg && msg.type === 'VSCODE_EXTHOST_IPC_REDUCE_GRACE_TIME') {
236
if (disconnectRunner2.isScheduled()) {
237
// we are disconnected and already running the short reconnection timer
238
return;
239
}
240
if (disconnectRunner1.isScheduled()) {
241
// we are disconnected and running the long reconnection timer
242
disconnectRunner2.schedule();
243
}
244
}
245
});
246
247
// Now that we have managed to install a message listener, ask the other side to send us the socket
248
const req: IExtHostReadyMessage = { type: 'VSCODE_EXTHOST_IPC_READY' };
249
process.send?.(req);
250
});
251
252
} else {
253
254
const pipeName = extHostConnection.pipeName;
255
256
return new Promise<PersistentProtocol>((resolve, reject) => {
257
258
const socket = net.createConnection(pipeName, () => {
259
socket.removeListener('error', reject);
260
const protocol = new PersistentProtocol({ socket: new NodeSocket(socket, 'extHost-renderer') });
261
protocol.sendResume();
262
resolve(protocol);
263
});
264
socket.once('error', reject);
265
266
socket.on('close', () => {
267
onTerminate('renderer closed the socket');
268
});
269
});
270
}
271
}
272
273
async function createExtHostProtocol(): Promise<IMessagePassingProtocol> {
274
275
const protocol = await _createExtHostProtocol();
276
277
return new class implements IMessagePassingProtocol {
278
279
private readonly _onMessage = new BufferedEmitter<VSBuffer>();
280
readonly onMessage: Event<VSBuffer> = this._onMessage.event;
281
282
private _terminating: boolean;
283
private _protocolListener: IDisposable;
284
285
constructor() {
286
this._terminating = false;
287
this._protocolListener = protocol.onMessage((msg) => {
288
if (isMessageOfType(msg, MessageType.Terminate)) {
289
this._terminating = true;
290
this._protocolListener.dispose();
291
onTerminate('received terminate message from renderer');
292
} else {
293
this._onMessage.fire(msg);
294
}
295
});
296
}
297
298
send(msg: any): void {
299
if (!this._terminating) {
300
protocol.send(msg);
301
}
302
}
303
304
async drain(): Promise<void> {
305
if (protocol.drain) {
306
return protocol.drain();
307
}
308
}
309
};
310
}
311
312
function connectToRenderer(protocol: IMessagePassingProtocol): Promise<IRendererConnection> {
313
return new Promise<IRendererConnection>((c) => {
314
315
// Listen init data message
316
const first = protocol.onMessage(raw => {
317
first.dispose();
318
319
const initData = <IExtensionHostInitData>JSON.parse(raw.toString());
320
321
const rendererCommit = initData.commit;
322
const myCommit = product.commit;
323
324
if (rendererCommit && myCommit) {
325
// Running in the built version where commits are defined
326
if (rendererCommit !== myCommit) {
327
nativeExit(ExtensionHostExitCode.VersionMismatch);
328
}
329
}
330
331
if (initData.parentPid) {
332
// Kill oneself if one's parent dies. Much drama.
333
let epermErrors = 0;
334
setInterval(function () {
335
try {
336
process.kill(initData.parentPid, 0); // throws an exception if the main process doesn't exist anymore.
337
epermErrors = 0;
338
} catch (e) {
339
if (e && e.code === 'EPERM') {
340
// Even if the parent process is still alive,
341
// some antivirus software can lead to an EPERM error to be thrown here.
342
// Let's terminate only if we get 3 consecutive EPERM errors.
343
epermErrors++;
344
if (epermErrors >= 3) {
345
onTerminate(`parent process ${initData.parentPid} does not exist anymore (3 x EPERM): ${e.message} (code: ${e.code}) (errno: ${e.errno})`);
346
}
347
} else {
348
onTerminate(`parent process ${initData.parentPid} does not exist anymore: ${e.message} (code: ${e.code}) (errno: ${e.errno})`);
349
}
350
}
351
}, 1000);
352
353
// In certain cases, the event loop can become busy and never yield
354
// e.g. while-true or process.nextTick endless loops
355
// So also use the native node module to do it from a separate thread
356
let watchdog: typeof nativeWatchdog;
357
try {
358
watchdog = require('native-watchdog');
359
watchdog.start(initData.parentPid);
360
} catch (err) {
361
// no problem...
362
onUnexpectedError(err);
363
}
364
}
365
366
// Tell the outside that we are initialized
367
protocol.send(createMessageOfType(MessageType.Initialized));
368
369
c({ protocol, initData });
370
});
371
372
// Tell the outside that we are ready to receive messages
373
protocol.send(createMessageOfType(MessageType.Ready));
374
});
375
}
376
377
async function startExtensionHostProcess(): Promise<void> {
378
379
// Print a console message when rejection isn't handled within N seconds. For details:
380
// see https://nodejs.org/api/process.html#process_event_unhandledrejection
381
// and https://nodejs.org/api/process.html#process_event_rejectionhandled
382
const unhandledPromises: Promise<any>[] = [];
383
process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
384
unhandledPromises.push(promise);
385
setTimeout(() => {
386
const idx = unhandledPromises.indexOf(promise);
387
if (idx >= 0) {
388
promise.catch(e => {
389
unhandledPromises.splice(idx, 1);
390
if (!isCancellationError(e)) {
391
console.warn(`rejected promise not handled within 1 second: ${e}`);
392
if (e && e.stack) {
393
console.warn(`stack trace: ${e.stack}`);
394
}
395
if (reason) {
396
onUnexpectedError(reason);
397
}
398
}
399
});
400
}
401
}, 1000);
402
});
403
404
process.on('rejectionHandled', (promise: Promise<any>) => {
405
const idx = unhandledPromises.indexOf(promise);
406
if (idx >= 0) {
407
unhandledPromises.splice(idx, 1);
408
}
409
});
410
411
// Print a console message when an exception isn't handled.
412
process.on('uncaughtException', function (err: Error) {
413
if (!isSigPipeError(err)) {
414
onUnexpectedError(err);
415
}
416
});
417
418
performance.mark(`code/extHost/willConnectToRenderer`);
419
const protocol = await createExtHostProtocol();
420
performance.mark(`code/extHost/didConnectToRenderer`);
421
const renderer = await connectToRenderer(protocol);
422
performance.mark(`code/extHost/didWaitForInitData`);
423
const { initData } = renderer;
424
// setup things
425
patchProcess(!!initData.environment.extensionTestsLocationURI); // to support other test frameworks like Jasmin that use process.exit (https://github.com/microsoft/vscode/issues/37708)
426
initData.environment.useHostProxy = args.useHostProxy !== undefined ? args.useHostProxy !== 'false' : undefined;
427
initData.environment.skipWorkspaceStorageLock = boolean(args.skipWorkspaceStorageLock, false);
428
429
// host abstraction
430
const hostUtils = new class NodeHost implements IHostUtils {
431
declare readonly _serviceBrand: undefined;
432
public readonly pid = process.pid;
433
exit(code: number) { nativeExit(code); }
434
fsExists(path: string) { return Promises.exists(path); }
435
fsRealpath(path: string) { return Promises.realpath(path); }
436
};
437
438
// Attempt to load uri transformer
439
let uriTransformer: IURITransformer | null = null;
440
if (initData.remote.authority && args.transformURIs) {
441
uriTransformer = createURITransformer(initData.remote.authority);
442
}
443
444
const extensionHostMain = new ExtensionHostMain(
445
renderer.protocol,
446
initData,
447
hostUtils,
448
uriTransformer
449
);
450
451
// rewrite onTerminate-function to be a proper shutdown
452
onTerminate = (reason: string) => extensionHostMain.terminate(reason);
453
}
454
455
startExtensionHostProcess().catch((err) => console.log(err));
456
457