Path: blob/main/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts
5256 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 { WebWorkerDescriptor } from '../../../../platform/webWorker/browser/webWorkerDescriptor.js';29import { IWebWorkerService } from '../../../../platform/webWorker/browser/webWorkerService.js';30import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';31import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js';32import { IDefaultLogLevelsService } from '../../log/common/defaultLogLevels.js';33import { ExtensionHostExitCode, IExtensionHostInitData, MessageType, UIKind, createMessageOfType, isMessageOfType } from '../common/extensionHostProtocol.js';34import { LocalWebWorkerRunningLocation } from '../common/extensionRunningLocation.js';35import { ExtensionHostExtensions, ExtensionHostStartup, IExtensionHost } from '../common/extensions.js';3637export interface IWebWorkerExtensionHostInitData {38readonly extensions: ExtensionHostExtensions;39}4041export interface IWebWorkerExtensionHostDataProvider {42getInitData(): Promise<IWebWorkerExtensionHostInitData>;43}4445export class WebWorkerExtensionHost extends Disposable implements IExtensionHost {4647public readonly pid = null;48public readonly remoteAuthority = null;49public extensions: ExtensionHostExtensions | null = null;5051private readonly _onDidExit = this._register(new Emitter<[number, string | null]>());52public readonly onExit: Event<[number, string | null]> = this._onDidExit.event;5354private _isTerminating: boolean;55private _protocolPromise: Promise<IMessagePassingProtocol> | null;56private _protocol: IMessagePassingProtocol | null;5758private readonly _extensionHostLogsLocation: URI;5960constructor(61public readonly runningLocation: LocalWebWorkerRunningLocation,62public readonly startup: ExtensionHostStartup,63private readonly _initDataProvider: IWebWorkerExtensionHostDataProvider,64@ITelemetryService private readonly _telemetryService: ITelemetryService,65@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,66@ILabelService private readonly _labelService: ILabelService,67@ILogService private readonly _logService: ILogService,68@ILoggerService private readonly _loggerService: ILoggerService,69@IBrowserWorkbenchEnvironmentService private readonly _environmentService: IBrowserWorkbenchEnvironmentService,70@IUserDataProfilesService private readonly _userDataProfilesService: IUserDataProfilesService,71@IProductService private readonly _productService: IProductService,72@ILayoutService private readonly _layoutService: ILayoutService,73@IStorageService private readonly _storageService: IStorageService,74@IWebWorkerService private readonly _webWorkerService: IWebWorkerService,75@IDefaultLogLevelsService private readonly _defaultLogLevelsService: IDefaultLogLevelsService,76) {77super();78this._isTerminating = false;79this._protocolPromise = null;80this._protocol = null;81this._extensionHostLogsLocation = joinPath(this._environmentService.extHostLogsPath, 'webWorker');82}8384private async _getWebWorkerExtensionHostIframeSrc(): Promise<string> {85const suffixSearchParams = new URLSearchParams();86if (this._environmentService.debugExtensionHost && this._environmentService.debugRenderer) {87suffixSearchParams.set('debugged', '1');88}89COI.addSearchParam(suffixSearchParams, true, true);9091const suffix = `?${suffixSearchParams.toString()}`;9293const iframeModulePath: AppResourcePath = `vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html`;94if (platform.isWeb) {95const webEndpointUrlTemplate = this._productService.webEndpointUrlTemplate;96const commit = this._productService.commit;97const quality = this._productService.quality;98if (webEndpointUrlTemplate && commit && quality) {99// Try to keep the web worker extension host iframe origin stable by storing it in workspace storage100const key = 'webWorkerExtensionHostIframeStableOriginUUID';101let stableOriginUUID = this._storageService.get(key, StorageScope.WORKSPACE);102if (typeof stableOriginUUID === 'undefined') {103stableOriginUUID = generateUuid();104this._storageService.store(key, stableOriginUUID, StorageScope.WORKSPACE, StorageTarget.MACHINE);105}106const hash = await parentOriginHash(mainWindow.origin, stableOriginUUID);107const baseUrl = (108webEndpointUrlTemplate109.replace('{{uuid}}', `v--${hash}`) // using `v--` as a marker to require `parentOrigin`/`salt` verification110.replace('{{commit}}', commit)111.replace('{{quality}}', quality)112);113114const res = new URL(`${baseUrl}/out/${iframeModulePath}${suffix}`);115res.searchParams.set('parentOrigin', mainWindow.origin);116res.searchParams.set('salt', stableOriginUUID);117return res.toString();118}119120console.warn(`The web worker extension host is started in a same-origin iframe!`);121}122123const relativeExtensionHostIframeSrc = this._webWorkerService.getWorkerUrl(new WebWorkerDescriptor({124esmModuleLocation: FileAccess.asBrowserUri(iframeModulePath),125esmModuleLocationBundler: new URL(`../worker/webWorkerExtensionHostIframe.html`, import.meta.url),126label: 'webWorkerExtensionHostIframe'127}));128129return `${relativeExtensionHostIframeSrc}${suffix}`;130}131132public async start(): Promise<IMessagePassingProtocol> {133if (!this._protocolPromise) {134this._protocolPromise = this._startInsideIframe();135this._protocolPromise.then(protocol => this._protocol = protocol);136}137return this._protocolPromise;138}139140private async _startInsideIframe(): Promise<IMessagePassingProtocol> {141const webWorkerExtensionHostIframeSrc = await this._getWebWorkerExtensionHostIframeSrc();142const emitter = this._register(new Emitter<VSBuffer>());143144const iframe = document.createElement('iframe');145iframe.setAttribute('class', 'web-worker-ext-host-iframe');146iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');147iframe.setAttribute('allow', 'usb; serial; hid; cross-origin-isolated; local-network-access;');148iframe.setAttribute('aria-hidden', 'true');149iframe.style.display = 'none';150151const vscodeWebWorkerExtHostId = generateUuid();152iframe.setAttribute('src', `${webWorkerExtensionHostIframeSrc}&vscodeWebWorkerExtHostId=${vscodeWebWorkerExtHostId}`);153154const barrier = new Barrier();155let port!: MessagePort;156let barrierError: Error | null = null;157let barrierHasError = false;158let startTimeout: Timeout | undefined = undefined;159160const rejectBarrier = (exitCode: number, error: Error) => {161barrierError = error;162barrierHasError = true;163onUnexpectedError(barrierError);164clearTimeout(startTimeout);165this._onDidExit.fire([ExtensionHostExitCode.UnexpectedError, barrierError.message]);166barrier.open();167};168169const resolveBarrier = (messagePort: MessagePort) => {170port = messagePort;171clearTimeout(startTimeout);172barrier.open();173};174175startTimeout = setTimeout(() => {176console.warn(`The Web Worker Extension Host did not start in 60s, that might be a problem.`);177}, 60000);178179this._register(dom.addDisposableListener(mainWindow, 'message', (event) => {180if (event.source !== iframe.contentWindow) {181return;182}183if (event.data.vscodeWebWorkerExtHostId !== vscodeWebWorkerExtHostId) {184return;185}186if (event.data.error) {187const { name, message, stack } = event.data.error;188const err = new Error();189err.message = message;190err.name = name;191err.stack = stack;192return rejectBarrier(ExtensionHostExitCode.UnexpectedError, err);193}194if (event.data.type === 'vscode.bootstrap.nls') {195iframe.contentWindow!.postMessage({196type: event.data.type,197data: {198workerUrl: this._webWorkerService.getWorkerUrl(extensionHostWorkerMainDescriptor),199fileRoot: globalThis._VSCODE_FILE_ROOT,200nls: {201messages: getNLSMessages(),202language: getNLSLanguage()203}204}205}, '*');206return;207}208const { data } = event.data;209if (barrier.isOpen() || !(data instanceof MessagePort)) {210console.warn('UNEXPECTED message', event);211const err = new Error('UNEXPECTED message');212return rejectBarrier(ExtensionHostExitCode.UnexpectedError, err);213}214resolveBarrier(data);215}));216217this._layoutService.mainContainer.appendChild(iframe);218this._register(toDisposable(() => iframe.remove()));219220// await MessagePort and use it to directly communicate221// with the worker extension host222await barrier.wait();223224if (barrierHasError) {225throw barrierError;226}227228// Send over message ports for extension API229const messagePorts = this._environmentService.options?.messagePorts ?? new Map();230iframe.contentWindow!.postMessage({ type: 'vscode.init', data: messagePorts }, '*', [...messagePorts.values()]);231232port.onmessage = (event) => {233const { data } = event;234if (!(data instanceof ArrayBuffer)) {235console.warn('UNKNOWN data received', data);236this._onDidExit.fire([77, 'UNKNOWN data received']);237return;238}239emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength)));240};241242const protocol: IMessagePassingProtocol = {243onMessage: emitter.event,244send: vsbuf => {245const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength);246port.postMessage(data, [data]);247}248};249250return this._performHandshake(protocol);251}252253private async _performHandshake(protocol: IMessagePassingProtocol): Promise<IMessagePassingProtocol> {254// extension host handshake happens below255// (1) <== wait for: Ready256// (2) ==> send: init data257// (3) <== wait for: Initialized258259await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Ready)));260if (this._isTerminating) {261throw canceled();262}263protocol.send(VSBuffer.fromString(JSON.stringify(await this._createExtHostInitData())));264if (this._isTerminating) {265throw canceled();266}267await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Initialized)));268if (this._isTerminating) {269throw canceled();270}271272return protocol;273}274275public override dispose(): void {276if (this._isTerminating) {277return;278}279this._isTerminating = true;280this._protocol?.send(createMessageOfType(MessageType.Terminate));281super.dispose();282}283284getInspectPort(): undefined {285return undefined;286}287288enableInspectPort(): Promise<boolean> {289return Promise.resolve(false);290}291292private async _createExtHostInitData(): Promise<IExtensionHostInitData> {293const initData = await this._initDataProvider.getInitData();294this.extensions = initData.extensions;295const workspace = this._contextService.getWorkspace();296const nlsBaseUrl = this._productService.extensionsGallery?.nlsBaseUrl;297let nlsUrlWithDetails: URI | undefined = undefined;298// Only use the nlsBaseUrl if we are using a language other than the default, English.299if (nlsBaseUrl && this._productService.commit && !platform.Language.isDefaultVariant()) {300nlsUrlWithDetails = URI.joinPath(URI.parse(nlsBaseUrl), this._productService.commit, this._productService.version, platform.Language.value());301}302return {303commit: this._productService.commit,304version: this._productService.version,305quality: this._productService.quality,306date: this._productService.date,307parentPid: 0,308environment: {309isExtensionDevelopmentDebug: this._environmentService.debugRenderer,310appName: this._productService.nameLong,311appHost: this._productService.embedderIdentifier ?? (platform.isWeb ? 'web' : 'desktop'),312appUriScheme: this._productService.urlProtocol,313appLanguage: platform.language,314isExtensionTelemetryLoggingOnly: isLoggingOnly(this._productService, this._environmentService),315isPortable: false,316extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,317extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI,318globalStorageHome: this._userDataProfilesService.defaultProfile.globalStorageHome,319workspaceStorageHome: this._environmentService.workspaceStorageHome,320extensionLogLevel: this._defaultLogLevelsService.defaultLogLevels.extensions321},322workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : {323configuration: workspace.configuration || undefined,324id: workspace.id,325name: this._labelService.getWorkspaceLabel(workspace),326transient: workspace.transient,327isAgentSessionsWorkspace: workspace.isAgentSessionsWorkspace328},329consoleForward: {330includeStack: false,331logNative: this._environmentService.debugRenderer332},333extensions: this.extensions.toSnapshot(),334nlsBaseUrl: nlsUrlWithDetails,335telemetryInfo: {336sessionId: this._telemetryService.sessionId,337machineId: this._telemetryService.machineId,338sqmId: this._telemetryService.sqmId,339devDeviceId: this._telemetryService.devDeviceId ?? this._telemetryService.machineId,340firstSessionDate: this._telemetryService.firstSessionDate,341msftInternal: this._telemetryService.msftInternal342},343remoteExtensionTips: this._productService.remoteExtensionTips,344virtualWorkspaceExtensionTips: this._productService.virtualWorkspaceExtensionTips,345logLevel: this._logService.getLevel(),346loggers: [...this._loggerService.getRegisteredLoggers()],347logsLocation: this._extensionHostLogsLocation,348autoStart: (this.startup === ExtensionHostStartup.EagerAutoStart || this.startup === ExtensionHostStartup.LazyAutoStart),349remote: {350authority: this._environmentService.remoteAuthority,351connectionData: null,352isRemote: false353},354uiKind: platform.isWeb ? UIKind.Web : UIKind.Desktop355};356}357}358359const extensionHostWorkerMainDescriptor = new WebWorkerDescriptor({360label: 'extensionHostWorkerMain',361esmModuleLocation: () => FileAccess.asBrowserUri('vs/workbench/api/worker/extensionHostWorkerMain.js'),362esmModuleLocationBundler: () => new URL('../../../api/worker/extensionHostWorkerMain.ts?esm', import.meta.url),363});364365366