Path: blob/main/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.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 dom from '../../../../base/browser/dom.js';6import { parentOriginHash } from '../../../../base/browser/iframe.js';7import { mainWindow } from '../../../../base/browser/window.js';8import { Barrier } from '../../../../base/common/async.js';9import { VSBuffer } from '../../../../base/common/buffer.js';10import { canceled, onUnexpectedError } from '../../../../base/common/errors.js';11import { Emitter, Event } from '../../../../base/common/event.js';12import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js';13import { AppResourcePath, COI, FileAccess } from '../../../../base/common/network.js';14import * as platform from '../../../../base/common/platform.js';15import { joinPath } from '../../../../base/common/resources.js';16import { URI } from '../../../../base/common/uri.js';17import { generateUuid } from '../../../../base/common/uuid.js';18import { IMessagePassingProtocol } from '../../../../base/parts/ipc/common/ipc.js';19import { getNLSLanguage, getNLSMessages } from '../../../../nls.js';20import { ILabelService } from '../../../../platform/label/common/label.js';21import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js';22import { ILogService, ILoggerService } from '../../../../platform/log/common/log.js';23import { IProductService } from '../../../../platform/product/common/productService.js';24import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';25import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';26import { isLoggingOnly } from '../../../../platform/telemetry/common/telemetryUtils.js';27import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';28import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';29import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js';30import { ExtensionHostExitCode, IExtensionHostInitData, MessageType, UIKind, createMessageOfType, isMessageOfType } from '../common/extensionHostProtocol.js';31import { LocalWebWorkerRunningLocation } from '../common/extensionRunningLocation.js';32import { ExtensionHostExtensions, ExtensionHostStartup, IExtensionHost } from '../common/extensions.js';3334export interface IWebWorkerExtensionHostInitData {35readonly extensions: ExtensionHostExtensions;36}3738export interface IWebWorkerExtensionHostDataProvider {39getInitData(): Promise<IWebWorkerExtensionHostInitData>;40}4142export class WebWorkerExtensionHost extends Disposable implements IExtensionHost {4344public readonly pid = null;45public readonly remoteAuthority = null;46public extensions: ExtensionHostExtensions | null = null;4748private readonly _onDidExit = this._register(new Emitter<[number, string | null]>());49public readonly onExit: Event<[number, string | null]> = this._onDidExit.event;5051private _isTerminating: boolean;52private _protocolPromise: Promise<IMessagePassingProtocol> | null;53private _protocol: IMessagePassingProtocol | null;5455private readonly _extensionHostLogsLocation: URI;5657constructor(58public readonly runningLocation: LocalWebWorkerRunningLocation,59public readonly startup: ExtensionHostStartup,60private readonly _initDataProvider: IWebWorkerExtensionHostDataProvider,61@ITelemetryService private readonly _telemetryService: ITelemetryService,62@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,63@ILabelService private readonly _labelService: ILabelService,64@ILogService private readonly _logService: ILogService,65@ILoggerService private readonly _loggerService: ILoggerService,66@IBrowserWorkbenchEnvironmentService private readonly _environmentService: IBrowserWorkbenchEnvironmentService,67@IUserDataProfilesService private readonly _userDataProfilesService: IUserDataProfilesService,68@IProductService private readonly _productService: IProductService,69@ILayoutService private readonly _layoutService: ILayoutService,70@IStorageService private readonly _storageService: IStorageService,71) {72super();73this._isTerminating = false;74this._protocolPromise = null;75this._protocol = null;76this._extensionHostLogsLocation = joinPath(this._environmentService.extHostLogsPath, 'webWorker');77}7879private async _getWebWorkerExtensionHostIframeSrc(): Promise<string> {80const suffixSearchParams = new URLSearchParams();81if (this._environmentService.debugExtensionHost && this._environmentService.debugRenderer) {82suffixSearchParams.set('debugged', '1');83}84COI.addSearchParam(suffixSearchParams, true, true);8586const suffix = `?${suffixSearchParams.toString()}`;8788const iframeModulePath: AppResourcePath = `vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html`;89if (platform.isWeb) {90const webEndpointUrlTemplate = this._productService.webEndpointUrlTemplate;91const commit = this._productService.commit;92const quality = this._productService.quality;93if (webEndpointUrlTemplate && commit && quality) {94// Try to keep the web worker extension host iframe origin stable by storing it in workspace storage95const key = 'webWorkerExtensionHostIframeStableOriginUUID';96let stableOriginUUID = this._storageService.get(key, StorageScope.WORKSPACE);97if (typeof stableOriginUUID === 'undefined') {98stableOriginUUID = generateUuid();99this._storageService.store(key, stableOriginUUID, StorageScope.WORKSPACE, StorageTarget.MACHINE);100}101const hash = await parentOriginHash(mainWindow.origin, stableOriginUUID);102const baseUrl = (103webEndpointUrlTemplate104.replace('{{uuid}}', `v--${hash}`) // using `v--` as a marker to require `parentOrigin`/`salt` verification105.replace('{{commit}}', commit)106.replace('{{quality}}', quality)107);108109const res = new URL(`${baseUrl}/out/${iframeModulePath}${suffix}`);110res.searchParams.set('parentOrigin', mainWindow.origin);111res.searchParams.set('salt', stableOriginUUID);112return res.toString();113}114115console.warn(`The web worker extension host is started in a same-origin iframe!`);116}117118const relativeExtensionHostIframeSrc = FileAccess.asBrowserUri(iframeModulePath);119return `${relativeExtensionHostIframeSrc.toString(true)}${suffix}`;120}121122public async start(): Promise<IMessagePassingProtocol> {123if (!this._protocolPromise) {124this._protocolPromise = this._startInsideIframe();125this._protocolPromise.then(protocol => this._protocol = protocol);126}127return this._protocolPromise;128}129130private async _startInsideIframe(): Promise<IMessagePassingProtocol> {131const webWorkerExtensionHostIframeSrc = await this._getWebWorkerExtensionHostIframeSrc();132const emitter = this._register(new Emitter<VSBuffer>());133134const iframe = document.createElement('iframe');135iframe.setAttribute('class', 'web-worker-ext-host-iframe');136iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');137iframe.setAttribute('allow', 'usb; serial; hid; cross-origin-isolated;');138iframe.setAttribute('aria-hidden', 'true');139iframe.style.display = 'none';140141const vscodeWebWorkerExtHostId = generateUuid();142iframe.setAttribute('src', `${webWorkerExtensionHostIframeSrc}&vscodeWebWorkerExtHostId=${vscodeWebWorkerExtHostId}`);143144const barrier = new Barrier();145let port!: MessagePort;146let barrierError: Error | null = null;147let barrierHasError = false;148let startTimeout: Timeout | undefined = undefined;149150const rejectBarrier = (exitCode: number, error: Error) => {151barrierError = error;152barrierHasError = true;153onUnexpectedError(barrierError);154clearTimeout(startTimeout);155this._onDidExit.fire([ExtensionHostExitCode.UnexpectedError, barrierError.message]);156barrier.open();157};158159const resolveBarrier = (messagePort: MessagePort) => {160port = messagePort;161clearTimeout(startTimeout);162barrier.open();163};164165startTimeout = setTimeout(() => {166console.warn(`The Web Worker Extension Host did not start in 60s, that might be a problem.`);167}, 60000);168169this._register(dom.addDisposableListener(mainWindow, 'message', (event) => {170if (event.source !== iframe.contentWindow) {171return;172}173if (event.data.vscodeWebWorkerExtHostId !== vscodeWebWorkerExtHostId) {174return;175}176if (event.data.error) {177const { name, message, stack } = event.data.error;178const err = new Error();179err.message = message;180err.name = name;181err.stack = stack;182return rejectBarrier(ExtensionHostExitCode.UnexpectedError, err);183}184if (event.data.type === 'vscode.bootstrap.nls') {185iframe.contentWindow!.postMessage({186type: event.data.type,187data: {188workerUrl: FileAccess.asBrowserUri('vs/workbench/api/worker/extensionHostWorkerMain.js').toString(true),189fileRoot: globalThis._VSCODE_FILE_ROOT,190nls: {191messages: getNLSMessages(),192language: getNLSLanguage()193}194}195}, '*');196return;197}198const { data } = event.data;199if (barrier.isOpen() || !(data instanceof MessagePort)) {200console.warn('UNEXPECTED message', event);201const err = new Error('UNEXPECTED message');202return rejectBarrier(ExtensionHostExitCode.UnexpectedError, err);203}204resolveBarrier(data);205}));206207this._layoutService.mainContainer.appendChild(iframe);208this._register(toDisposable(() => iframe.remove()));209210// await MessagePort and use it to directly communicate211// with the worker extension host212await barrier.wait();213214if (barrierHasError) {215throw barrierError;216}217218// Send over message ports for extension API219const messagePorts = this._environmentService.options?.messagePorts ?? new Map();220iframe.contentWindow!.postMessage({ type: 'vscode.init', data: messagePorts }, '*', [...messagePorts.values()]);221222port.onmessage = (event) => {223const { data } = event;224if (!(data instanceof ArrayBuffer)) {225console.warn('UNKNOWN data received', data);226this._onDidExit.fire([77, 'UNKNOWN data received']);227return;228}229emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength)));230};231232const protocol: IMessagePassingProtocol = {233onMessage: emitter.event,234send: vsbuf => {235const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength);236port.postMessage(data, [data]);237}238};239240return this._performHandshake(protocol);241}242243private async _performHandshake(protocol: IMessagePassingProtocol): Promise<IMessagePassingProtocol> {244// extension host handshake happens below245// (1) <== wait for: Ready246// (2) ==> send: init data247// (3) <== wait for: Initialized248249await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Ready)));250if (this._isTerminating) {251throw canceled();252}253protocol.send(VSBuffer.fromString(JSON.stringify(await this._createExtHostInitData())));254if (this._isTerminating) {255throw canceled();256}257await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Initialized)));258if (this._isTerminating) {259throw canceled();260}261262return protocol;263}264265public override dispose(): void {266if (this._isTerminating) {267return;268}269this._isTerminating = true;270this._protocol?.send(createMessageOfType(MessageType.Terminate));271super.dispose();272}273274getInspectPort(): undefined {275return undefined;276}277278enableInspectPort(): Promise<boolean> {279return Promise.resolve(false);280}281282private async _createExtHostInitData(): Promise<IExtensionHostInitData> {283const initData = await this._initDataProvider.getInitData();284this.extensions = initData.extensions;285const workspace = this._contextService.getWorkspace();286const nlsBaseUrl = this._productService.extensionsGallery?.nlsBaseUrl;287let nlsUrlWithDetails: URI | undefined = undefined;288// Only use the nlsBaseUrl if we are using a language other than the default, English.289if (nlsBaseUrl && this._productService.commit && !platform.Language.isDefaultVariant()) {290nlsUrlWithDetails = URI.joinPath(URI.parse(nlsBaseUrl), this._productService.commit, this._productService.version, platform.Language.value());291}292return {293commit: this._productService.commit,294version: this._productService.version,295quality: this._productService.quality,296date: this._productService.date,297parentPid: 0,298environment: {299isExtensionDevelopmentDebug: this._environmentService.debugRenderer,300appName: this._productService.nameLong,301appHost: this._productService.embedderIdentifier ?? (platform.isWeb ? 'web' : 'desktop'),302appUriScheme: this._productService.urlProtocol,303appLanguage: platform.language,304isExtensionTelemetryLoggingOnly: isLoggingOnly(this._productService, this._environmentService),305extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,306extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI,307globalStorageHome: this._userDataProfilesService.defaultProfile.globalStorageHome,308workspaceStorageHome: this._environmentService.workspaceStorageHome,309extensionLogLevel: this._environmentService.extensionLogLevel310},311workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : {312configuration: workspace.configuration || undefined,313id: workspace.id,314name: this._labelService.getWorkspaceLabel(workspace),315transient: workspace.transient316},317consoleForward: {318includeStack: false,319logNative: this._environmentService.debugRenderer320},321extensions: this.extensions.toSnapshot(),322nlsBaseUrl: nlsUrlWithDetails,323telemetryInfo: {324sessionId: this._telemetryService.sessionId,325machineId: this._telemetryService.machineId,326sqmId: this._telemetryService.sqmId,327devDeviceId: this._telemetryService.devDeviceId,328firstSessionDate: this._telemetryService.firstSessionDate,329msftInternal: this._telemetryService.msftInternal330},331logLevel: this._logService.getLevel(),332loggers: [...this._loggerService.getRegisteredLoggers()],333logsLocation: this._extensionHostLogsLocation,334autoStart: (this.startup === ExtensionHostStartup.EagerAutoStart || this.startup === ExtensionHostStartup.LazyAutoStart),335remote: {336authority: this._environmentService.remoteAuthority,337connectionData: null,338isRemote: false339},340uiKind: platform.isWeb ? UIKind.Web : UIKind.Desktop341};342}343}344345346