Path: blob/main/src/vs/workbench/api/node/extensionHostProcess.ts
5221 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 '@vscode/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 '../../../base/common/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' || warning.name === 'DeprecationWarning') {45console.debug(warning);46return;47}4849warningListeners[0](warning);50});51}5253// workaround for https://github.com/microsoft/vscode/issues/8549054// remove --inspect-port=0 after start so that it doesn't trigger LSP debugging55(function removeInspectPort() {56for (let i = 0; i < process.execArgv.length; i++) {57if (process.execArgv[i] === '--inspect-port=0') {58process.execArgv.splice(i, 1);59i--;60}61}62})();6364const args = minimist(process.argv.slice(2), {65boolean: [66'transformURIs',67'skipWorkspaceStorageLock',68'supportGlobalNavigator',69],70string: [71'useHostProxy' // 'true' | 'false' | undefined72]73}) as ParsedExtHostArgs;7475// With Electron 2.x and node.js 8.x the "natives" module76// can cause a native crash (see https://github.com/nodejs/node/issues/19891 and77// https://github.com/electron/electron/issues/10905). To prevent this from78// happening we essentially blocklist this module from getting loaded in any79// extension by patching the node require() function.80(function () {81const Module = require('module');82const originalLoad = Module._load;8384Module._load = function (request: string) {85if (request === 'natives') {86throw new Error('Either the extension or an NPM dependency is using the [unsupported "natives" node module](https://go.microsoft.com/fwlink/?linkid=871887).');87}8889return originalLoad.apply(this, arguments);90};91})();9293// custom process.exit logic...94const nativeExit: IExitFn = process.exit.bind(process);95const nativeOn = process.on.bind(process);96function patchProcess(allowExit: boolean) {97process.exit = function (code?: number) {98if (allowExit) {99nativeExit(code);100} else {101const err = new Error('An extension called process.exit() and this was prevented.');102console.warn(err.stack);103}104} as (code?: number) => never;105106// override Electron's process.crash() method107// eslint-disable-next-line local/code-no-any-casts108(process as any /* bypass layer checker */).crash = function () {109const err = new Error('An extension called process.crash() and this was prevented.');110console.warn(err.stack);111};112113// Set ELECTRON_RUN_AS_NODE environment variable for extensions that use114// child_process.spawn with process.execPath and expect to run as node process115// on the desktop.116// Refs https://github.com/microsoft/vscode/issues/151012#issuecomment-1156593228117process.env['ELECTRON_RUN_AS_NODE'] = '1';118119// eslint-disable-next-line local/code-no-any-casts120process.on = <any>function (event: string, listener: (...args: any[]) => void) {121if (event === 'uncaughtException') {122const actualListener = listener;123listener = function (...args: unknown[]) {124try {125return actualListener.apply(undefined, args);126} catch {127// DO NOT HANDLE NOR PRINT the error here because this can and will lead to128// more errors which will cause error handling to be reentrant and eventually129// overflowing the stack. Do not be sad, we do handle and annotate uncaught130// errors properly in 'extensionHostMain'131}132};133}134nativeOn(event, listener);135};136137}138139// NodeJS since v21 defines navigator as a global object. This will likely surprise many extensions and potentially break them140// because `navigator` has historically often been used to check if running in a browser (vs running inside NodeJS)141if (!args.supportGlobalNavigator) {142Object.defineProperty(globalThis, 'navigator', {143get: () => {144onUnexpectedExternalError(new PendingMigrationError('navigator is now a global in nodejs, please see https://aka.ms/vscode-extensions/navigator for additional info on this error.'));145return undefined;146}147});148}149150151interface IRendererConnection {152protocol: IMessagePassingProtocol;153initData: IExtensionHostInitData;154}155156// This calls exit directly in case the initialization is not finished and we need to exit157// Otherwise, if initialization completed we go to extensionHostMain.terminate()158let onTerminate = function (reason: string) {159nativeExit();160};161162function readReconnectionValue(envKey: string, fallback: number): number {163const raw = process.env[envKey];164if (typeof raw !== 'string' || raw.trim().length === 0) {165console.log(`[reconnection-grace-time] Extension host: env var ${envKey} not set, using default: ${fallback}ms (${Math.floor(fallback / 1000)}s)`);166return fallback;167}168const parsed = Number(raw);169if (!isFinite(parsed) || parsed < 0) {170console.log(`[reconnection-grace-time] Extension host: env var ${envKey} invalid value '${raw}', using default: ${fallback}ms (${Math.floor(fallback / 1000)}s)`);171return fallback;172}173const millis = Math.floor(parsed);174const result = millis > Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : millis;175console.log(`[reconnection-grace-time] Extension host: read ${envKey}=${raw}ms (${Math.floor(result / 1000)}s)`);176return result;177}178179function _createExtHostProtocol(): Promise<IMessagePassingProtocol> {180const extHostConnection = readExtHostConnection(process.env);181182if (extHostConnection.type === ExtHostConnectionType.MessagePort) {183184return new Promise<IMessagePassingProtocol>((resolve, reject) => {185186const withPorts = (ports: MessagePortMain[]) => {187const port = ports[0];188const onMessage = new BufferedEmitter<VSBuffer>();189port.on('message', (e) => onMessage.fire(VSBuffer.wrap(e.data as Uint8Array)));190port.on('close', () => {191onTerminate('renderer closed the MessagePort');192});193port.start();194195resolve({196onMessage: onMessage.event,197send: message => port.postMessage(message.buffer)198});199};200201(process as unknown as { parentPort: { on: (event: 'message', listener: (messageEvent: UtilityMessageEvent) => void) => void } }).parentPort.on('message', (e: UtilityMessageEvent) => withPorts(e.ports));202});203204} else if (extHostConnection.type === ExtHostConnectionType.Socket) {205206return new Promise<PersistentProtocol>((resolve, reject) => {207208let protocol: PersistentProtocol | null = null;209210const timer = setTimeout(() => {211onTerminate('VSCODE_EXTHOST_IPC_SOCKET timeout');212}, 60000);213214const reconnectionGraceTime = readReconnectionValue('VSCODE_RECONNECTION_GRACE_TIME', ProtocolConstants.ReconnectionGraceTime);215const reconnectionShortGraceTime = reconnectionGraceTime > 0 ? Math.min(ProtocolConstants.ReconnectionShortGraceTime, reconnectionGraceTime) : 0;216const disconnectRunner1 = new ProcessTimeRunOnceScheduler(() => onTerminate('renderer disconnected for too long (1)'), reconnectionGraceTime);217const disconnectRunner2 = new ProcessTimeRunOnceScheduler(() => onTerminate('renderer disconnected for too long (2)'), reconnectionShortGraceTime);218219process.on('message', (msg: IExtHostSocketMessage | IExtHostReduceGraceTimeMessage, handle: net.Socket) => {220if (msg && msg.type === 'VSCODE_EXTHOST_IPC_SOCKET') {221// Disable Nagle's algorithm. We also do this on the server process,222// but nodejs doesn't document if this option is transferred with the socket223handle.setNoDelay(true);224225const initialDataChunk = VSBuffer.wrap(Buffer.from(msg.initialDataChunk, 'base64'));226let socket: NodeSocket | WebSocketNodeSocket;227if (msg.skipWebSocketFrames) {228socket = new NodeSocket(handle, 'extHost-socket');229} else {230const inflateBytes = VSBuffer.wrap(Buffer.from(msg.inflateBytes, 'base64'));231socket = new WebSocketNodeSocket(new NodeSocket(handle, 'extHost-socket'), msg.permessageDeflate, inflateBytes, false);232}233if (protocol) {234// reconnection case235disconnectRunner1.cancel();236disconnectRunner2.cancel();237protocol.beginAcceptReconnection(socket, initialDataChunk);238protocol.endAcceptReconnection();239protocol.sendResume();240} else {241clearTimeout(timer);242protocol = new PersistentProtocol({ socket, initialChunk: initialDataChunk });243protocol.sendResume();244protocol.onDidDispose(() => onTerminate('renderer disconnected'));245resolve(protocol);246247// Wait for rich client to reconnect248protocol.onSocketClose(() => {249// The socket has closed, let's give the renderer a certain amount of time to reconnect250disconnectRunner1.schedule();251});252}253}254if (msg && msg.type === 'VSCODE_EXTHOST_IPC_REDUCE_GRACE_TIME') {255if (disconnectRunner2.isScheduled()) {256// we are disconnected and already running the short reconnection timer257return;258}259if (disconnectRunner1.isScheduled()) {260// we are disconnected and running the long reconnection timer261disconnectRunner2.schedule();262}263}264});265266// Now that we have managed to install a message listener, ask the other side to send us the socket267const req: IExtHostReadyMessage = { type: 'VSCODE_EXTHOST_IPC_READY' };268process.send?.(req);269});270271} else {272273const pipeName = extHostConnection.pipeName;274275return new Promise<PersistentProtocol>((resolve, reject) => {276277const socket = net.createConnection(pipeName, () => {278socket.removeListener('error', reject);279const protocol = new PersistentProtocol({ socket: new NodeSocket(socket, 'extHost-renderer') });280protocol.sendResume();281resolve(protocol);282});283socket.once('error', reject);284285socket.on('close', () => {286onTerminate('renderer closed the socket');287});288});289}290}291292async function createExtHostProtocol(): Promise<IMessagePassingProtocol> {293294const protocol = await _createExtHostProtocol();295296return new class implements IMessagePassingProtocol {297298private readonly _onMessage = new BufferedEmitter<VSBuffer>();299readonly onMessage: Event<VSBuffer> = this._onMessage.event;300301private _terminating: boolean;302private _protocolListener: IDisposable;303304constructor() {305this._terminating = false;306this._protocolListener = protocol.onMessage((msg) => {307if (isMessageOfType(msg, MessageType.Terminate)) {308this._terminating = true;309this._protocolListener.dispose();310onTerminate('received terminate message from renderer');311} else {312this._onMessage.fire(msg);313}314});315}316317send(msg: any): void {318if (!this._terminating) {319protocol.send(msg);320}321}322323async drain(): Promise<void> {324if (protocol.drain) {325return protocol.drain();326}327}328};329}330331function connectToRenderer(protocol: IMessagePassingProtocol): Promise<IRendererConnection> {332return new Promise<IRendererConnection>((c) => {333334// Listen init data message335const first = protocol.onMessage(raw => {336first.dispose();337338const initData = <IExtensionHostInitData>JSON.parse(raw.toString());339340const rendererCommit = initData.commit;341const myCommit = product.commit;342343if (rendererCommit && myCommit) {344// Running in the built version where commits are defined345if (rendererCommit !== myCommit) {346nativeExit(ExtensionHostExitCode.VersionMismatch);347}348}349350if (initData.parentPid) {351// Kill oneself if one's parent dies. Much drama.352let epermErrors = 0;353setInterval(function () {354try {355process.kill(initData.parentPid, 0); // throws an exception if the main process doesn't exist anymore.356epermErrors = 0;357} catch (e) {358if (e && e.code === 'EPERM') {359// Even if the parent process is still alive,360// some antivirus software can lead to an EPERM error to be thrown here.361// Let's terminate only if we get 3 consecutive EPERM errors.362epermErrors++;363if (epermErrors >= 3) {364onTerminate(`parent process ${initData.parentPid} does not exist anymore (3 x EPERM): ${e.message} (code: ${e.code}) (errno: ${e.errno})`);365}366} else {367onTerminate(`parent process ${initData.parentPid} does not exist anymore: ${e.message} (code: ${e.code}) (errno: ${e.errno})`);368}369}370}, 1000);371372// In certain cases, the event loop can become busy and never yield373// e.g. while-true or process.nextTick endless loops374// So also use the native node module to do it from a separate thread375let watchdog: typeof nativeWatchdog;376try {377watchdog = require('@vscode/native-watchdog');378watchdog.start(initData.parentPid);379} catch (err) {380// no problem...381onUnexpectedError(err);382}383}384385// Tell the outside that we are initialized386protocol.send(createMessageOfType(MessageType.Initialized));387388c({ protocol, initData });389});390391// Tell the outside that we are ready to receive messages392protocol.send(createMessageOfType(MessageType.Ready));393});394}395396async function startExtensionHostProcess(): Promise<void> {397398// Print a console message when rejection isn't handled within N seconds. For details:399// see https://nodejs.org/api/process.html#process_event_unhandledrejection400// and https://nodejs.org/api/process.html#process_event_rejectionhandled401const unhandledPromises: Promise<any>[] = [];402process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {403unhandledPromises.push(promise);404setTimeout(() => {405const idx = unhandledPromises.indexOf(promise);406if (idx >= 0) {407promise.catch(e => {408unhandledPromises.splice(idx, 1);409if (!isCancellationError(e)) {410console.warn(`rejected promise not handled within 1 second: ${e}`);411if (e && e.stack) {412console.warn(`stack trace: ${e.stack}`);413}414if (reason) {415onUnexpectedError(reason);416}417}418});419}420}, 1000);421});422423process.on('rejectionHandled', (promise: Promise<any>) => {424const idx = unhandledPromises.indexOf(promise);425if (idx >= 0) {426unhandledPromises.splice(idx, 1);427}428});429430// Print a console message when an exception isn't handled.431process.on('uncaughtException', function (err: Error) {432if (!isSigPipeError(err)) {433onUnexpectedError(err);434}435});436437performance.mark(`code/extHost/willConnectToRenderer`);438const protocol = await createExtHostProtocol();439performance.mark(`code/extHost/didConnectToRenderer`);440const renderer = await connectToRenderer(protocol);441performance.mark(`code/extHost/didWaitForInitData`);442const { initData } = renderer;443// setup things444patchProcess(!!initData.environment.extensionTestsLocationURI); // to support other test frameworks like Jasmin that use process.exit (https://github.com/microsoft/vscode/issues/37708)445initData.environment.useHostProxy = args.useHostProxy !== undefined ? args.useHostProxy !== 'false' : undefined;446initData.environment.skipWorkspaceStorageLock = boolean(args.skipWorkspaceStorageLock, false);447448// host abstraction449const hostUtils = new class NodeHost implements IHostUtils {450declare readonly _serviceBrand: undefined;451public readonly pid = process.pid;452exit(code: number) { nativeExit(code); }453fsExists(path: string) { return Promises.exists(path); }454fsRealpath(path: string) { return Promises.realpath(path); }455};456457// Attempt to load uri transformer458let uriTransformer: IURITransformer | null = null;459if (initData.remote.authority && args.transformURIs) {460uriTransformer = createURITransformer(initData.remote.authority);461}462463const extensionHostMain = new ExtensionHostMain(464renderer.protocol,465initData,466hostUtils,467uriTransformer468);469470// rewrite onTerminate-function to be a proper shutdown471onTerminate = (reason: string) => extensionHostMain.terminate(reason);472}473474startExtensionHostProcess().catch((err) => console.log(err));475476477