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