Path: blob/main/src/vs/workbench/api/worker/extensionHostWorker.ts
5252 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 { IMessagePassingProtocol } from '../../../base/parts/ipc/common/ipc.js';6import { VSBuffer } from '../../../base/common/buffer.js';7import { Emitter } from '../../../base/common/event.js';8import { isMessageOfType, MessageType, createMessageOfType, IExtensionHostInitData } from '../../services/extensions/common/extensionHostProtocol.js';9import { ExtensionHostMain } from '../common/extensionHostMain.js';10import { IHostUtils } from '../common/extHostExtensionService.js';11import { NestedWorker } from '../../services/extensions/worker/polyfillNestedWorker.js';12import * as path from '../../../base/common/path.js';13import * as performance from '../../../base/common/performance.js';1415import '../common/extHost.common.services.js';16import './extHost.worker.services.js';17import { FileAccess } from '../../../base/common/network.js';18import { URI } from '../../../base/common/uri.js';1920//#region --- Define, capture, and override some globals2122declare function postMessage(data: any, transferables?: Transferable[]): void;23declare const name: string; // https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope/name24declare type _Fetch = typeof fetch;2526declare namespace self {27let close: any;28let postMessage: any;29let addEventListener: any;30let removeEventListener: any;31let dispatchEvent: any;32let indexedDB: { open: any;[k: string]: any };33let caches: { open: any;[k: string]: any };34let importScripts: any;35let fetch: _Fetch;36let XMLHttpRequest: any;37}3839const nativeClose = self.close.bind(self);40self.close = () => console.trace(`'close' has been blocked`);4142const nativePostMessage = postMessage.bind(self);43self.postMessage = () => console.trace(`'postMessage' has been blocked`);4445function shouldTransformUri(uri: string): boolean {46// In principle, we could convert any URI, but we have concerns47// that parsing https URIs might end up decoding escape characters48// and result in an unintended transformation49return /^(file|vscode-remote):/i.test(uri);50}5152const nativeFetch = fetch.bind(self);53function patchFetching(asBrowserUri: (uri: URI) => Promise<URI>) {54self.fetch = async function (input, init) {55if (input instanceof Request) {56// Request object - massage not supported57return nativeFetch(input, init);58}59if (shouldTransformUri(String(input))) {60input = (await asBrowserUri(URI.parse(String(input)))).toString(true);61}62return nativeFetch(input, init);63};6465self.XMLHttpRequest = class extends XMLHttpRequest {66override open(method: string, url: string | URL, async?: boolean, username?: string | null, password?: string | null): void {67(async () => {68if (shouldTransformUri(url.toString())) {69url = (await asBrowserUri(URI.parse(url.toString()))).toString(true);70}71super.open(method, url, async ?? true, username, password);72})();73}74};75}7677self.importScripts = () => { throw new Error(`'importScripts' has been blocked`); };7879// const nativeAddEventListener = addEventListener.bind(self);80self.addEventListener = () => console.trace(`'addEventListener' has been blocked`);8182// eslint-disable-next-line local/code-no-any-casts83(<any>self)['AMDLoader'] = undefined;84// eslint-disable-next-line local/code-no-any-casts85(<any>self)['NLSLoaderPlugin'] = undefined;86// eslint-disable-next-line local/code-no-any-casts87(<any>self)['define'] = undefined;88// eslint-disable-next-line local/code-no-any-casts89(<any>self)['require'] = undefined;90// eslint-disable-next-line local/code-no-any-casts91(<any>self)['webkitRequestFileSystem'] = undefined;92// eslint-disable-next-line local/code-no-any-casts93(<any>self)['webkitRequestFileSystemSync'] = undefined;94// eslint-disable-next-line local/code-no-any-casts95(<any>self)['webkitResolveLocalFileSystemSyncURL'] = undefined;96// eslint-disable-next-line local/code-no-any-casts97(<any>self)['webkitResolveLocalFileSystemURL'] = undefined;9899// eslint-disable-next-line local/code-no-any-casts100if ((<any>self).Worker) {101102// make sure new Worker(...) always uses blob: (to maintain current origin)103// eslint-disable-next-line local/code-no-any-casts104const _Worker = (<any>self).Worker;105// eslint-disable-next-line local/code-no-any-casts106Worker = <any>function (stringUrl: string | URL, options?: WorkerOptions) {107if (/^file:/i.test(stringUrl.toString())) {108stringUrl = FileAccess.uriToBrowserUri(URI.parse(stringUrl.toString())).toString(true);109} else if (/^vscode-remote:/i.test(stringUrl.toString())) {110// Supporting transformation of vscode-remote URIs requires an async call to the main thread,111// but we cannot do this call from within the embedded Worker, and the only way out would be112// to use templating instead of a function in the web api (`resourceUriProvider`)113throw new Error(`Creating workers from remote extensions is currently not supported.`);114}115116// IMPORTANT: bootstrapFn is stringified and injected as worker blob-url. Because of that it CANNOT117// have dependencies on other functions or variables. Only constant values are supported. Due to118// that logic of FileAccess.asBrowserUri had to be copied, see `asWorkerBrowserUrl` (below).119const bootstrapFnSource = (function bootstrapFn(workerUrl: string) {120function asWorkerBrowserUrl(url: string | URL | TrustedScriptURL): any {121if (typeof url === 'string' || url instanceof URL) {122return String(url).replace(/^file:\/\//i, 'vscode-file://vscode-app');123}124return url;125}126127const nativeFetch = fetch.bind(self);128self.fetch = function (input, init) {129if (input instanceof Request) {130// Request object - massage not supported131return nativeFetch(input, init);132}133return nativeFetch(asWorkerBrowserUrl(input), init);134};135self.XMLHttpRequest = class extends XMLHttpRequest {136override open(method: string, url: string | URL, async?: boolean, username?: string | null, password?: string | null): void {137return super.open(method, asWorkerBrowserUrl(url), async ?? true, username, password);138}139};140const nativeImportScripts = importScripts.bind(self);141self.importScripts = (...urls: string[]) => {142nativeImportScripts(...urls.map(asWorkerBrowserUrl));143};144145nativeImportScripts(workerUrl);146}).toString();147148const js = `(${bootstrapFnSource}('${stringUrl}'))`;149options = options || {};150options.name = `${name} -> ${options.name || path.basename(stringUrl.toString())}`;151const blob = new Blob([js], { type: 'application/javascript' });152const blobUrl = URL.createObjectURL(blob);153return new _Worker(blobUrl, options);154};155156} else {157// eslint-disable-next-line local/code-no-any-casts158(<any>self).Worker = class extends NestedWorker {159constructor(stringOrUrl: string | URL, options?: WorkerOptions) {160super(nativePostMessage, stringOrUrl, { name: path.basename(stringOrUrl.toString()), ...options });161}162};163}164165//#endregion ---166167const hostUtil = new class implements IHostUtils {168declare readonly _serviceBrand: undefined;169public readonly pid = undefined;170exit(_code?: number | undefined): void {171nativeClose();172}173};174175176class ExtensionWorker {177178// protocol179readonly protocol: IMessagePassingProtocol;180181constructor() {182183const channel = new MessageChannel();184const emitter = new Emitter<VSBuffer>();185let terminating = false;186187// send over port2, keep port1188nativePostMessage(channel.port2, [channel.port2]);189190channel.port1.onmessage = event => {191const { data } = event;192if (!(data instanceof ArrayBuffer)) {193console.warn('UNKNOWN data received', data);194return;195}196197const msg = VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength));198if (isMessageOfType(msg, MessageType.Terminate)) {199// handle terminate-message right here200terminating = true;201onTerminate('received terminate message from renderer');202return;203}204205// emit non-terminate messages to the outside206emitter.fire(msg);207};208209this.protocol = {210onMessage: emitter.event,211send: vsbuf => {212if (!terminating) {213const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength);214channel.port1.postMessage(data, [data]);215}216}217};218}219}220221interface IRendererConnection {222protocol: IMessagePassingProtocol;223initData: IExtensionHostInitData;224}225function connectToRenderer(protocol: IMessagePassingProtocol): Promise<IRendererConnection> {226return new Promise<IRendererConnection>(resolve => {227const once = protocol.onMessage(raw => {228once.dispose();229const initData = <IExtensionHostInitData>JSON.parse(raw.toString());230protocol.send(createMessageOfType(MessageType.Initialized));231resolve({ protocol, initData });232});233protocol.send(createMessageOfType(MessageType.Ready));234});235}236237let onTerminate = (reason: string) => nativeClose();238239interface IInitMessage {240readonly type: 'vscode.init';241readonly data: ReadonlyMap<string, MessagePort>;242}243244function isInitMessage(a: any): a is IInitMessage {245return !!a && typeof a === 'object' && a.type === 'vscode.init' && a.data instanceof Map;246}247248export function create(): { onmessage: (message: any) => void } {249performance.mark(`code/extHost/willConnectToRenderer`);250const res = new ExtensionWorker();251252return {253onmessage(message: any) {254if (!isInitMessage(message)) {255return; // silently ignore foreign messages256}257258connectToRenderer(res.protocol).then(data => {259performance.mark(`code/extHost/didWaitForInitData`);260const extHostMain = new ExtensionHostMain(261data.protocol,262data.initData,263hostUtil,264null,265message.data266);267268patchFetching(uri => extHostMain.asBrowserUri(uri));269270onTerminate = (reason: string) => extHostMain.terminate(reason);271});272}273};274}275276277