Path: blob/main/src/vs/workbench/api/worker/extensionHostWorker.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 { 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(<any>self)['AMDLoader'] = undefined;83(<any>self)['NLSLoaderPlugin'] = undefined;84(<any>self)['define'] = undefined;85(<any>self)['require'] = undefined;86(<any>self)['webkitRequestFileSystem'] = undefined;87(<any>self)['webkitRequestFileSystemSync'] = undefined;88(<any>self)['webkitResolveLocalFileSystemSyncURL'] = undefined;89(<any>self)['webkitResolveLocalFileSystemURL'] = undefined;9091if ((<any>self).Worker) {9293// make sure new Worker(...) always uses blob: (to maintain current origin)94const _Worker = (<any>self).Worker;95Worker = <any>function (stringUrl: string | URL, options?: WorkerOptions) {96if (/^file:/i.test(stringUrl.toString())) {97stringUrl = FileAccess.uriToBrowserUri(URI.parse(stringUrl.toString())).toString(true);98} else if (/^vscode-remote:/i.test(stringUrl.toString())) {99// Supporting transformation of vscode-remote URIs requires an async call to the main thread,100// but we cannot do this call from within the embedded Worker, and the only way out would be101// to use templating instead of a function in the web api (`resourceUriProvider`)102throw new Error(`Creating workers from remote extensions is currently not supported.`);103}104105// IMPORTANT: bootstrapFn is stringified and injected as worker blob-url. Because of that it CANNOT106// have dependencies on other functions or variables. Only constant values are supported. Due to107// that logic of FileAccess.asBrowserUri had to be copied, see `asWorkerBrowserUrl` (below).108const bootstrapFnSource = (function bootstrapFn(workerUrl: string) {109function asWorkerBrowserUrl(url: string | URL | TrustedScriptURL): any {110if (typeof url === 'string' || url instanceof URL) {111return String(url).replace(/^file:\/\//i, 'vscode-file://vscode-app');112}113return url;114}115116const nativeFetch = fetch.bind(self);117self.fetch = function (input, init) {118if (input instanceof Request) {119// Request object - massage not supported120return nativeFetch(input, init);121}122return nativeFetch(asWorkerBrowserUrl(input), init);123};124self.XMLHttpRequest = class extends XMLHttpRequest {125override open(method: string, url: string | URL, async?: boolean, username?: string | null, password?: string | null): void {126return super.open(method, asWorkerBrowserUrl(url), async ?? true, username, password);127}128};129const nativeImportScripts = importScripts.bind(self);130self.importScripts = (...urls: string[]) => {131nativeImportScripts(...urls.map(asWorkerBrowserUrl));132};133134nativeImportScripts(workerUrl);135}).toString();136137const js = `(${bootstrapFnSource}('${stringUrl}'))`;138options = options || {};139options.name = `${name} -> ${options.name || path.basename(stringUrl.toString())}`;140const blob = new Blob([js], { type: 'application/javascript' });141const blobUrl = URL.createObjectURL(blob);142return new _Worker(blobUrl, options);143};144145} else {146(<any>self).Worker = class extends NestedWorker {147constructor(stringOrUrl: string | URL, options?: WorkerOptions) {148super(nativePostMessage, stringOrUrl, { name: path.basename(stringOrUrl.toString()), ...options });149}150};151}152153//#endregion ---154155const hostUtil = new class implements IHostUtils {156declare readonly _serviceBrand: undefined;157public readonly pid = undefined;158exit(_code?: number | undefined): void {159nativeClose();160}161};162163164class ExtensionWorker {165166// protocol167readonly protocol: IMessagePassingProtocol;168169constructor() {170171const channel = new MessageChannel();172const emitter = new Emitter<VSBuffer>();173let terminating = false;174175// send over port2, keep port1176nativePostMessage(channel.port2, [channel.port2]);177178channel.port1.onmessage = event => {179const { data } = event;180if (!(data instanceof ArrayBuffer)) {181console.warn('UNKNOWN data received', data);182return;183}184185const msg = VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength));186if (isMessageOfType(msg, MessageType.Terminate)) {187// handle terminate-message right here188terminating = true;189onTerminate('received terminate message from renderer');190return;191}192193// emit non-terminate messages to the outside194emitter.fire(msg);195};196197this.protocol = {198onMessage: emitter.event,199send: vsbuf => {200if (!terminating) {201const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength);202channel.port1.postMessage(data, [data]);203}204}205};206}207}208209interface IRendererConnection {210protocol: IMessagePassingProtocol;211initData: IExtensionHostInitData;212}213function connectToRenderer(protocol: IMessagePassingProtocol): Promise<IRendererConnection> {214return new Promise<IRendererConnection>(resolve => {215const once = protocol.onMessage(raw => {216once.dispose();217const initData = <IExtensionHostInitData>JSON.parse(raw.toString());218protocol.send(createMessageOfType(MessageType.Initialized));219resolve({ protocol, initData });220});221protocol.send(createMessageOfType(MessageType.Ready));222});223}224225let onTerminate = (reason: string) => nativeClose();226227interface IInitMessage {228readonly type: 'vscode.init';229readonly data: ReadonlyMap<string, MessagePort>;230}231232function isInitMessage(a: any): a is IInitMessage {233return !!a && typeof a === 'object' && a.type === 'vscode.init' && a.data instanceof Map;234}235236export function create(): { onmessage: (message: any) => void } {237performance.mark(`code/extHost/willConnectToRenderer`);238const res = new ExtensionWorker();239240return {241onmessage(message: any) {242if (!isInitMessage(message)) {243return; // silently ignore foreign messages244}245246connectToRenderer(res.protocol).then(data => {247performance.mark(`code/extHost/didWaitForInitData`);248const extHostMain = new ExtensionHostMain(249data.protocol,250data.initData,251hostUtil,252null,253message.data254);255256patchFetching(uri => extHostMain.asBrowserUri(uri));257258onTerminate = (reason: string) => extHostMain.terminate(reason);259});260}261};262}263264265