Path: blob/main/src/vs/workbench/api/node/extensionHostProcess.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import minimist from 'minimist';6import * as nativeWatchdog from 'native-watchdog';7import * as net from 'net';8import { ProcessTimeRunOnceScheduler } from '../../../base/common/async.js';9import { VSBuffer } from '../../../base/common/buffer.js';10import { PendingMigrationError, isCancellationError, isSigPipeError, onUnexpectedError, onUnexpectedExternalError } from '../../../base/common/errors.js';11import { Event } from '../../../base/common/event.js';12import * as performance from '../../../base/common/performance.js';13import { IURITransformer } from '../../../base/common/uriIpc.js';14import { Promises } from '../../../base/node/pfs.js';15import { IMessagePassingProtocol } from '../../../base/parts/ipc/common/ipc.js';16import { BufferedEmitter, PersistentProtocol, ProtocolConstants } from '../../../base/parts/ipc/common/ipc.net.js';17import { NodeSocket, WebSocketNodeSocket } from '../../../base/parts/ipc/node/ipc.net.js';18import type { MessagePortMain, MessageEvent as UtilityMessageEvent } from '../../../base/parts/sandbox/node/electronTypes.js';19import { boolean } from '../../../editor/common/config/editorOptions.js';20import product from '../../../platform/product/common/product.js';21import { ExtensionHostMain, IExitFn } from '../common/extensionHostMain.js';22import { IHostUtils } from '../common/extHostExtensionService.js';23import { createURITransformer } from './uriTransformer.js';24import { ExtHostConnectionType, readExtHostConnection } from '../../services/extensions/common/extensionHostEnv.js';25import { ExtensionHostExitCode, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, IExtHostSocketMessage, IExtensionHostInitData, MessageType, createMessageOfType, isMessageOfType } from '../../services/extensions/common/extensionHostProtocol.js';26import { IDisposable } from '../../../base/common/lifecycle.js';27import '../common/extHost.common.services.js';28import './extHost.node.services.js';29import { createRequire } from 'node:module';30const require = createRequire(import.meta.url);3132interface ParsedExtHostArgs {33transformURIs?: boolean;34skipWorkspaceStorageLock?: boolean;35supportGlobalNavigator?: boolean; // enable global navigator object in nodejs36useHostProxy?: 'true' | 'false'; // use a string, as undefined is also a valid value37}3839// silence experimental warnings when in development40if (process.env.VSCODE_DEV) {41const warningListeners = process.listeners('warning');42process.removeAllListeners('warning');43process.on('warning', (warning: any) => {44if (warning.code === 'ExperimentalWarning' || warning.name === 'ExperimentalWarning') {45return;46}4748warningListeners[0](warning);49});50}5152// workaround for https://github.com/microsoft/vscode/issues/8549053// remove --inspect-port=0 after start so that it doesn't trigger LSP debugging54(function removeInspectPort() {55for (let i = 0; i < process.execArgv.length; i++) {56if (process.execArgv[i] === '--inspect-port=0') {57process.execArgv.splice(i, 1);58i--;59}60}61})();6263const args = minimist(process.argv.slice(2), {64boolean: [65'transformURIs',66'skipWorkspaceStorageLock',67'supportGlobalNavigator',68],69string: [70'useHostProxy' // 'true' | 'false' | undefined71]72}) as ParsedExtHostArgs;7374// With Electron 2.x and node.js 8.x the "natives" module75// can cause a native crash (see https://github.com/nodejs/node/issues/19891 and76// https://github.com/electron/electron/issues/10905). To prevent this from77// happening we essentially blocklist this module from getting loaded in any78// extension by patching the node require() function.79(function () {80const Module = require('module');81const originalLoad = Module._load;8283Module._load = function (request: string) {84if (request === 'natives') {85throw new Error('Either the extension or an NPM dependency is using the [unsupported "natives" node module](https://go.microsoft.com/fwlink/?linkid=871887).');86}8788return originalLoad.apply(this, arguments);89};90})();9192// custom process.exit logic...93const nativeExit: IExitFn = process.exit.bind(process);94const nativeOn = process.on.bind(process);95function patchProcess(allowExit: boolean) {96process.exit = function (code?: number) {97if (allowExit) {98nativeExit(code);99} else {100const err = new Error('An extension called process.exit() and this was prevented.');101console.warn(err.stack);102}103} as (code?: number) => never;104105// override Electron's process.crash() method106(process as any /* bypass layer checker */).crash = function () {107const err = new Error('An extension called process.crash() and this was prevented.');108console.warn(err.stack);109};110111// Set ELECTRON_RUN_AS_NODE environment variable for extensions that use112// child_process.spawn with process.execPath and expect to run as node process113// on the desktop.114// Refs https://github.com/microsoft/vscode/issues/151012#issuecomment-1156593228115process.env['ELECTRON_RUN_AS_NODE'] = '1';116117process.on = <any>function (event: string, listener: (...args: any[]) => void) {118if (event === 'uncaughtException') {119const actualListener = listener;120listener = function (...args: any[]) {121try {122return actualListener.apply(undefined, args);123} catch {124// DO NOT HANDLE NOR PRINT the error here because this can and will lead to125// more errors which will cause error handling to be reentrant and eventually126// overflowing the stack. Do not be sad, we do handle and annotate uncaught127// errors properly in 'extensionHostMain'128}129};130}131nativeOn(event, listener);132};133134}135136// NodeJS since v21 defines navigator as a global object. This will likely surprise many extensions and potentially break them137// because `navigator` has historically often been used to check if running in a browser (vs running inside NodeJS)138if (!args.supportGlobalNavigator) {139Object.defineProperty(globalThis, 'navigator', {140get: () => {141onUnexpectedExternalError(new PendingMigrationError('navigator is now a global in nodejs, please see https://aka.ms/vscode-extensions/navigator for additional info on this error.'));142return undefined;143}144});145}146147148interface IRendererConnection {149protocol: IMessagePassingProtocol;150initData: IExtensionHostInitData;151}152153// This calls exit directly in case the initialization is not finished and we need to exit154// Otherwise, if initialization completed we go to extensionHostMain.terminate()155let onTerminate = function (reason: string) {156nativeExit();157};158159function _createExtHostProtocol(): Promise<IMessagePassingProtocol> {160const extHostConnection = readExtHostConnection(process.env);161162if (extHostConnection.type === ExtHostConnectionType.MessagePort) {163164return new Promise<IMessagePassingProtocol>((resolve, reject) => {165166const withPorts = (ports: MessagePortMain[]) => {167const port = ports[0];168const onMessage = new BufferedEmitter<VSBuffer>();169port.on('message', (e) => onMessage.fire(VSBuffer.wrap(e.data)));170port.on('close', () => {171onTerminate('renderer closed the MessagePort');172});173port.start();174175resolve({176onMessage: onMessage.event,177send: message => port.postMessage(message.buffer)178});179};180181(process as unknown as { parentPort: { on: (event: 'message', listener: (messageEvent: UtilityMessageEvent) => void) => void } }).parentPort.on('message', (e: UtilityMessageEvent) => withPorts(e.ports));182});183184} else if (extHostConnection.type === ExtHostConnectionType.Socket) {185186return new Promise<PersistentProtocol>((resolve, reject) => {187188let protocol: PersistentProtocol | null = null;189190const timer = setTimeout(() => {191onTerminate('VSCODE_EXTHOST_IPC_SOCKET timeout');192}, 60000);193194const reconnectionGraceTime = ProtocolConstants.ReconnectionGraceTime;195const reconnectionShortGraceTime = ProtocolConstants.ReconnectionShortGraceTime;196const disconnectRunner1 = new ProcessTimeRunOnceScheduler(() => onTerminate('renderer disconnected for too long (1)'), reconnectionGraceTime);197const disconnectRunner2 = new ProcessTimeRunOnceScheduler(() => onTerminate('renderer disconnected for too long (2)'), reconnectionShortGraceTime);198199process.on('message', (msg: IExtHostSocketMessage | IExtHostReduceGraceTimeMessage, handle: net.Socket) => {200if (msg && msg.type === 'VSCODE_EXTHOST_IPC_SOCKET') {201// Disable Nagle's algorithm. We also do this on the server process,202// but nodejs doesn't document if this option is transferred with the socket203handle.setNoDelay(true);204205const initialDataChunk = VSBuffer.wrap(Buffer.from(msg.initialDataChunk, 'base64'));206let socket: NodeSocket | WebSocketNodeSocket;207if (msg.skipWebSocketFrames) {208socket = new NodeSocket(handle, 'extHost-socket');209} else {210const inflateBytes = VSBuffer.wrap(Buffer.from(msg.inflateBytes, 'base64'));211socket = new WebSocketNodeSocket(new NodeSocket(handle, 'extHost-socket'), msg.permessageDeflate, inflateBytes, false);212}213if (protocol) {214// reconnection case215disconnectRunner1.cancel();216disconnectRunner2.cancel();217protocol.beginAcceptReconnection(socket, initialDataChunk);218protocol.endAcceptReconnection();219protocol.sendResume();220} else {221clearTimeout(timer);222protocol = new PersistentProtocol({ socket, initialChunk: initialDataChunk });223protocol.sendResume();224protocol.onDidDispose(() => onTerminate('renderer disconnected'));225resolve(protocol);226227// Wait for rich client to reconnect228protocol.onSocketClose(() => {229// The socket has closed, let's give the renderer a certain amount of time to reconnect230disconnectRunner1.schedule();231});232}233}234if (msg && msg.type === 'VSCODE_EXTHOST_IPC_REDUCE_GRACE_TIME') {235if (disconnectRunner2.isScheduled()) {236// we are disconnected and already running the short reconnection timer237return;238}239if (disconnectRunner1.isScheduled()) {240// we are disconnected and running the long reconnection timer241disconnectRunner2.schedule();242}243}244});245246// Now that we have managed to install a message listener, ask the other side to send us the socket247const req: IExtHostReadyMessage = { type: 'VSCODE_EXTHOST_IPC_READY' };248process.send?.(req);249});250251} else {252253const pipeName = extHostConnection.pipeName;254255return new Promise<PersistentProtocol>((resolve, reject) => {256257const socket = net.createConnection(pipeName, () => {258socket.removeListener('error', reject);259const protocol = new PersistentProtocol({ socket: new NodeSocket(socket, 'extHost-renderer') });260protocol.sendResume();261resolve(protocol);262});263socket.once('error', reject);264265socket.on('close', () => {266onTerminate('renderer closed the socket');267});268});269}270}271272async function createExtHostProtocol(): Promise<IMessagePassingProtocol> {273274const protocol = await _createExtHostProtocol();275276return new class implements IMessagePassingProtocol {277278private readonly _onMessage = new BufferedEmitter<VSBuffer>();279readonly onMessage: Event<VSBuffer> = this._onMessage.event;280281private _terminating: boolean;282private _protocolListener: IDisposable;283284constructor() {285this._terminating = false;286this._protocolListener = protocol.onMessage((msg) => {287if (isMessageOfType(msg, MessageType.Terminate)) {288this._terminating = true;289this._protocolListener.dispose();290onTerminate('received terminate message from renderer');291} else {292this._onMessage.fire(msg);293}294});295}296297send(msg: any): void {298if (!this._terminating) {299protocol.send(msg);300}301}302303async drain(): Promise<void> {304if (protocol.drain) {305return protocol.drain();306}307}308};309}310311function connectToRenderer(protocol: IMessagePassingProtocol): Promise<IRendererConnection> {312return new Promise<IRendererConnection>((c) => {313314// Listen init data message315const first = protocol.onMessage(raw => {316first.dispose();317318const initData = <IExtensionHostInitData>JSON.parse(raw.toString());319320const rendererCommit = initData.commit;321const myCommit = product.commit;322323if (rendererCommit && myCommit) {324// Running in the built version where commits are defined325if (rendererCommit !== myCommit) {326nativeExit(ExtensionHostExitCode.VersionMismatch);327}328}329330if (initData.parentPid) {331// Kill oneself if one's parent dies. Much drama.332let epermErrors = 0;333setInterval(function () {334try {335process.kill(initData.parentPid, 0); // throws an exception if the main process doesn't exist anymore.336epermErrors = 0;337} catch (e) {338if (e && e.code === 'EPERM') {339// Even if the parent process is still alive,340// some antivirus software can lead to an EPERM error to be thrown here.341// Let's terminate only if we get 3 consecutive EPERM errors.342epermErrors++;343if (epermErrors >= 3) {344onTerminate(`parent process ${initData.parentPid} does not exist anymore (3 x EPERM): ${e.message} (code: ${e.code}) (errno: ${e.errno})`);345}346} else {347onTerminate(`parent process ${initData.parentPid} does not exist anymore: ${e.message} (code: ${e.code}) (errno: ${e.errno})`);348}349}350}, 1000);351352// In certain cases, the event loop can become busy and never yield353// e.g. while-true or process.nextTick endless loops354// So also use the native node module to do it from a separate thread355let watchdog: typeof nativeWatchdog;356try {357watchdog = require('native-watchdog');358watchdog.start(initData.parentPid);359} catch (err) {360// no problem...361onUnexpectedError(err);362}363}364365// Tell the outside that we are initialized366protocol.send(createMessageOfType(MessageType.Initialized));367368c({ protocol, initData });369});370371// Tell the outside that we are ready to receive messages372protocol.send(createMessageOfType(MessageType.Ready));373});374}375376async function startExtensionHostProcess(): Promise<void> {377378// Print a console message when rejection isn't handled within N seconds. For details:379// see https://nodejs.org/api/process.html#process_event_unhandledrejection380// and https://nodejs.org/api/process.html#process_event_rejectionhandled381const unhandledPromises: Promise<any>[] = [];382process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {383unhandledPromises.push(promise);384setTimeout(() => {385const idx = unhandledPromises.indexOf(promise);386if (idx >= 0) {387promise.catch(e => {388unhandledPromises.splice(idx, 1);389if (!isCancellationError(e)) {390console.warn(`rejected promise not handled within 1 second: ${e}`);391if (e && e.stack) {392console.warn(`stack trace: ${e.stack}`);393}394if (reason) {395onUnexpectedError(reason);396}397}398});399}400}, 1000);401});402403process.on('rejectionHandled', (promise: Promise<any>) => {404const idx = unhandledPromises.indexOf(promise);405if (idx >= 0) {406unhandledPromises.splice(idx, 1);407}408});409410// Print a console message when an exception isn't handled.411process.on('uncaughtException', function (err: Error) {412if (!isSigPipeError(err)) {413onUnexpectedError(err);414}415});416417performance.mark(`code/extHost/willConnectToRenderer`);418const protocol = await createExtHostProtocol();419performance.mark(`code/extHost/didConnectToRenderer`);420const renderer = await connectToRenderer(protocol);421performance.mark(`code/extHost/didWaitForInitData`);422const { initData } = renderer;423// setup things424patchProcess(!!initData.environment.extensionTestsLocationURI); // to support other test frameworks like Jasmin that use process.exit (https://github.com/microsoft/vscode/issues/37708)425initData.environment.useHostProxy = args.useHostProxy !== undefined ? args.useHostProxy !== 'false' : undefined;426initData.environment.skipWorkspaceStorageLock = boolean(args.skipWorkspaceStorageLock, false);427428// host abstraction429const hostUtils = new class NodeHost implements IHostUtils {430declare readonly _serviceBrand: undefined;431public readonly pid = process.pid;432exit(code: number) { nativeExit(code); }433fsExists(path: string) { return Promises.exists(path); }434fsRealpath(path: string) { return Promises.realpath(path); }435};436437// Attempt to load uri transformer438let uriTransformer: IURITransformer | null = null;439if (initData.remote.authority && args.transformURIs) {440uriTransformer = createURITransformer(initData.remote.authority);441}442443const extensionHostMain = new ExtensionHostMain(444renderer.protocol,445initData,446hostUtils,447uriTransformer448);449450// rewrite onTerminate-function to be a proper shutdown451onTerminate = (reason: string) => extensionHostMain.terminate(reason);452}453454startExtensionHostProcess().catch((err) => console.log(err));455456457