Path: blob/main/src/vs/workbench/api/node/extHostExtensionService.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 * as performance from '../../../base/common/performance.js';6import type * as vscode from 'vscode';7import { createApiFactoryAndRegisterActors } from '../common/extHost.api.impl.js';8import { INodeModuleFactory, RequireInterceptor } from '../common/extHostRequireInterceptor.js';9import { ExtensionActivationTimesBuilder } from '../common/extHostExtensionActivator.js';10import { connectProxyResolver } from './proxyResolver.js';11import { AbstractExtHostExtensionService } from '../common/extHostExtensionService.js';12import { ExtHostDownloadService } from './extHostDownloadService.js';13import { URI } from '../../../base/common/uri.js';14import { Schemas } from '../../../base/common/network.js';15import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';16import { ExtensionRuntime } from '../common/extHostTypes.js';17import { CLIServer } from './extHostCLIServer.js';18import { realpathSync } from '../../../base/node/pfs.js';19import { ExtHostConsoleForwarder } from './extHostConsoleForwarder.js';20import { ExtHostDiskFileSystemProvider } from './extHostDiskFileSystemProvider.js';21import nodeModule from 'node:module';22import { assertType } from '../../../base/common/types.js';23import { generateUuid } from '../../../base/common/uuid.js';24import { BidirectionalMap } from '../../../base/common/map.js';25import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';2627const require = nodeModule.createRequire(import.meta.url);2829class NodeModuleRequireInterceptor extends RequireInterceptor {3031protected _installInterceptor(): void {32const that = this;33const node_module = require('module');34const originalLoad = node_module._load;35node_module._load = function load(request: string, parent: { filename: string }, isMain: boolean) {36request = applyAlternatives(request);37if (!that._factories.has(request)) {38return originalLoad.apply(this, arguments);39}40return that._factories.get(request)!.load(41request,42URI.file(realpathSync(parent.filename)),43request => originalLoad.apply(this, [request, parent, isMain])44);45};4647const originalLookup = node_module._resolveLookupPaths;48node_module._resolveLookupPaths = (request: string, parent: unknown) => {49return originalLookup.call(this, applyAlternatives(request), parent);50};5152const originalResolveFilename = node_module._resolveFilename;53node_module._resolveFilename = function resolveFilename(request: string, parent: unknown, isMain: boolean, options?: { paths?: string[] }) {54if (request === 'vsda' && Array.isArray(options?.paths) && options.paths.length === 0) {55// ESM: ever since we moved to ESM, `require.main` will be `undefined` for extensions56// Some extensions have been using `require.resolve('vsda', { paths: require.main.paths })`57// to find the `vsda` module in our app root. To be backwards compatible with this pattern,58// we help by filling in the `paths` array with the node modules paths of the current module.59options.paths = node_module._nodeModulePaths(import.meta.dirname);60}61return originalResolveFilename.call(this, request, parent, isMain, options);62};6364const applyAlternatives = (request: string) => {65for (const alternativeModuleName of that._alternatives) {66const alternative = alternativeModuleName(request);67if (alternative) {68request = alternative;69break;70}71}72return request;73};74}75}7677class NodeModuleESMInterceptor extends RequireInterceptor {7879private static _createDataUri(scriptContent: string): string {80return `data:text/javascript;base64,${Buffer.from(scriptContent).toString('base64')}`;81}8283// This string is a script that runs in the loader thread of NodeJS.84private static _loaderScript = `85let lookup;86export const initialize = async (context) => {87let requestIds = 0;88const { port } = context;89const pendingRequests = new Map();90port.onmessage = (event) => {91const { id, url } = event.data;92pendingRequests.get(id)?.(url);93};94lookup = url => {95// debugger;96const myId = requestIds++;97return new Promise((resolve) => {98pendingRequests.set(myId, resolve);99port.postMessage({ id: myId, url, });100});101};102};103export const resolve = async (specifier, context, nextResolve) => {104if (specifier !== 'vscode' || !context.parentURL) {105return nextResolve(specifier, context);106}107const otherUrl = await lookup(context.parentURL);108return {109url: otherUrl,110shortCircuit: true,111};112};`;113114private static _vscodeImportFnName = `_VSCODE_IMPORT_VSCODE_API`;115116private readonly _store = new DisposableStore();117118dispose(): void {119this._store.dispose();120}121122protected override _installInterceptor(): void {123124type Message = { id: string; url: string };125126const apiInstances = new BidirectionalMap<typeof vscode, string>();127const apiImportDataUrl = new Map<string, string>();128129// define a global function that can be used to get API instances given a random key130Object.defineProperty(globalThis, NodeModuleESMInterceptor._vscodeImportFnName, {131enumerable: false,132configurable: false,133writable: false,134value: (key: string) => {135return apiInstances.getKey(key);136}137});138139const { port1, port2 } = new MessageChannel();140141let apiModuleFactory: INodeModuleFactory | undefined;142143// this is a workaround for the fact that the layer checker does not understand144// that onmessage is NodeJS API here145const port1LayerCheckerWorkaround: any = port1;146147port1LayerCheckerWorkaround.onmessage = (e: { data: Message }) => {148149// Get the vscode-module factory - which is the same logic that's also used by150// the CommonJS require interceptor151if (!apiModuleFactory) {152apiModuleFactory = this._factories.get('vscode');153assertType(apiModuleFactory);154}155156const { id, url } = e.data;157const uri = URI.parse(url);158159// Get or create the API instance. The interface is per extension and extensions are160// looked up by the uri (e.data.url) and path containment.161const apiInstance = apiModuleFactory.load('_not_used', uri, () => { throw new Error('CANNOT LOAD MODULE from here.'); });162let key = apiInstances.get(apiInstance);163if (!key) {164key = generateUuid();165apiInstances.set(apiInstance, key);166}167168// Create and cache a data-url which is the import script for the API instance169let scriptDataUrlSrc = apiImportDataUrl.get(key);170if (!scriptDataUrlSrc) {171const jsCode = `const _vscodeInstance = globalThis.${NodeModuleESMInterceptor._vscodeImportFnName}('${key}');\n\n${Object.keys(apiInstance).map((name => `export const ${name} = _vscodeInstance['${name}'];`)).join('\n')}`;172scriptDataUrlSrc = NodeModuleESMInterceptor._createDataUri(jsCode);173apiImportDataUrl.set(key, scriptDataUrlSrc);174}175176port1.postMessage({177id,178url: scriptDataUrlSrc179});180};181182nodeModule.register(NodeModuleESMInterceptor._createDataUri(NodeModuleESMInterceptor._loaderScript), {183parentURL: import.meta.url,184data: { port: port2 },185transferList: [port2],186});187188this._store.add(toDisposable(() => {189port1.close();190port2.close();191}));192}193}194195export class ExtHostExtensionService extends AbstractExtHostExtensionService {196197readonly extensionRuntime = ExtensionRuntime.Node;198199protected async _beforeAlmostReadyToRunExtensions(): Promise<void> {200// make sure console.log calls make it to the render201this._instaService.createInstance(ExtHostConsoleForwarder);202203// initialize API and register actors204const extensionApiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors);205206// Register Download command207this._instaService.createInstance(ExtHostDownloadService);208209// Register CLI Server for ipc210if (this._initData.remote.isRemote && this._initData.remote.authority) {211const cliServer = this._instaService.createInstance(CLIServer);212process.env['VSCODE_IPC_HOOK_CLI'] = cliServer.ipcHandlePath;213}214215// Register local file system shortcut216this._instaService.createInstance(ExtHostDiskFileSystemProvider);217218// Module loading tricks219await this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry })220.install();221222// ESM loading tricks223await this._store.add(this._instaService.createInstance(NodeModuleESMInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry }))224.install();225226performance.mark('code/extHost/didInitAPI');227228// Do this when extension service exists, but extensions are not being activated yet.229const configProvider = await this._extHostConfiguration.getConfigProvider();230await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._logService, this._mainThreadTelemetryProxy, this._initData, this._store);231performance.mark('code/extHost/didInitProxyResolver');232}233234protected _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined {235return extensionDescription.main;236}237238private async _doLoadModule<T>(extension: IExtensionDescription | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder, mode: 'esm' | 'cjs'): Promise<T> {239if (module.scheme !== Schemas.file) {240throw new Error(`Cannot load URI: '${module}', must be of file-scheme`);241}242let r: T | null = null;243activationTimesBuilder.codeLoadingStart();244this._logService.trace(`ExtensionService#loadModule [${mode}] -> ${module.toString(true)}`);245this._logService.flush();246const extensionId = extension?.identifier.value;247if (extension) {248await this._extHostLocalizationService.initializeLocalizedMessages(extension);249}250try {251if (extensionId) {252performance.mark(`code/extHost/willLoadExtensionCode/${extensionId}`);253}254if (mode === 'esm') {255r = <T>await import(module.toString(true));256} else {257r = <T>require(module.fsPath);258}259} finally {260if (extensionId) {261performance.mark(`code/extHost/didLoadExtensionCode/${extensionId}`);262}263activationTimesBuilder.codeLoadingStop();264}265return r;266}267268protected async _loadCommonJSModule<T>(extension: IExtensionDescription | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {269return this._doLoadModule<T>(extension, module, activationTimesBuilder, 'cjs');270}271272protected async _loadESMModule<T>(extension: IExtensionDescription | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {273return this._doLoadModule<T>(extension, module, activationTimesBuilder, 'esm');274}275276public async $setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void> {277if (!this._initData.remote.isRemote) {278return;279}280281for (const key in env) {282const value = env[key];283if (value === null) {284delete process.env[key];285} else {286process.env[key] = value;287}288}289}290}291292293