diff --git a/resources/serverless/code-web.js b/resources/serverless/code-web.js index 87117a7699f..5223aebe92b 100644 --- a/resources/serverless/code-web.js +++ b/resources/serverless/code-web.js @@ -254,7 +254,9 @@ async function handleBuiltInExtension(req, res, parsedUrl) { if (!filePath) { return serveError(req, res, 400, `Bad request.`); } - return serveFile(req, res, filePath); + return serveFile(req, res, filePath, { + 'Access-Control-Allow-Origin': '*' + }); } /** diff --git a/src/vs/base/worker/defaultWorkerFactory.ts b/src/vs/base/worker/defaultWorkerFactory.ts index 45f3b41a457..0876b7aa056 100644 --- a/src/vs/base/worker/defaultWorkerFactory.ts +++ b/src/vs/base/worker/defaultWorkerFactory.ts @@ -28,11 +28,11 @@ function getWorker(workerId: string, label: string): Worker | Promise { } // ESM-comment-begin -export function getWorkerBootstrapUrl(scriptPath: string, label: string): string { - if (/^(http:)|(https:)|(file:)/.test(scriptPath)) { +export function getWorkerBootstrapUrl(scriptPath: string, label: string, forceDataUri: boolean = false): string { + if (forceDataUri || /^(http:)|(https:)|(file:)/.test(scriptPath)) { const currentUrl = String(window.location); const currentOrigin = currentUrl.substr(0, currentUrl.length - window.location.hash.length - window.location.search.length - window.location.pathname.length); - if (scriptPath.substring(0, currentOrigin.length) !== currentOrigin) { + if (forceDataUri || scriptPath.substring(0, currentOrigin.length) !== currentOrigin) { // this is the cross-origin case // i.e. the webpage is running at a different origin than where the scripts are loaded from const myPath = 'vs/base/worker/defaultWorkerFactory.js'; diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index c7240616746..34639e18b6f 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -5,6 +5,7 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; +import * as platform from 'vs/base/common/platform'; import { originalFSPath, joinPath } from 'vs/base/common/resources'; import { Barrier, timeout } from 'vs/base/common/async'; import { dispose, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; @@ -383,7 +384,14 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme subscriptions: [], get extensionUri() { return extensionDescription.extensionLocation; }, get extensionPath() { return extensionDescription.extensionLocation.fsPath; }, - asAbsolutePath(relativePath: string) { return path.join(extensionDescription.extensionLocation.fsPath, relativePath); }, + asAbsolutePath(relativePath: string) { + if (platform.isWeb) { + // web worker + return URI.joinPath(extensionDescription.extensionLocation, relativePath).toString(); + } else { + return path.join(extensionDescription.extensionLocation.fsPath, relativePath); + } + }, get storagePath() { return that._storagePath.workspaceValue(extensionDescription)?.fsPath; }, get globalStoragePath() { return that._storagePath.globalValue(extensionDescription).fsPath; }, get logPath() { return path.join(that._initData.logsLocation.fsPath, extensionDescription.identifier.value); }, diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index 031ed230b86..1d32d261533 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -5,7 +5,7 @@ import { getWorkerBootstrapUrl } from 'vs/base/worker/defaultWorkerFactory'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { toDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { VSBuffer } from 'vs/base/common/buffer'; import { createMessageOfType, MessageType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; @@ -16,6 +16,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import * as platform from 'vs/base/common/platform'; +import * as dom from 'vs/base/browser/dom'; import { URI } from 'vs/base/common/uri'; import { IExtensionHost, ExtensionHostLogFileName, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -24,6 +25,10 @@ import { joinPath } from 'vs/base/common/resources'; import { Registry } from 'vs/platform/registry/common/platform'; import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; import { localize } from 'vs/nls'; +import { generateUuid } from 'vs/base/common/uuid'; +import { canceled, onUnexpectedError } from 'vs/base/common/errors'; + +const WRAP_IN_IFRAME = true; export interface IWebWorkerExtensionHostInitData { readonly autoStart: boolean; @@ -34,17 +39,17 @@ export interface IWebWorkerExtensionHostDataProvider { getInitData(): Promise; } -export class WebWorkerExtensionHost implements IExtensionHost { +export class WebWorkerExtensionHost extends Disposable implements IExtensionHost { public readonly kind = ExtensionHostKind.LocalWebWorker; public readonly remoteAuthority = null; - private _toDispose = new DisposableStore(); - private _isTerminating: boolean = false; - private _protocol?: IMessagePassingProtocol; + private readonly _onDidExit = this._register(new Emitter<[number, string | null]>()); + public readonly onExit: Event<[number, string | null]> = this._onDidExit.event; - private readonly _onDidExit = new Emitter<[number, string | null]>(); - readonly onExit: Event<[number, string | null]> = this._onDidExit.event; + private _isTerminating: boolean; + private _protocolPromise: Promise | null; + private _protocol: IMessagePassingProtocol | null; private readonly _extensionHostLogsLocation: URI; private readonly _extensionHostLogFile: URI; @@ -58,76 +63,218 @@ export class WebWorkerExtensionHost implements IExtensionHost { @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, @IProductService private readonly _productService: IProductService, ) { + super(); + this._isTerminating = false; + this._protocolPromise = null; + this._protocol = null; this._extensionHostLogsLocation = URI.file(this._environmentService.logsPath).with({ scheme: this._environmentService.logFile.scheme }); this._extensionHostLogFile = joinPath(this._extensionHostLogsLocation, `${ExtensionHostLogFileName}.log`); } - async start(): Promise { - - if (!this._protocol) { - - const emitter = new Emitter(); - - const url = getWorkerBootstrapUrl(require.toUrl('../worker/extensionHostWorkerMain.js'), 'WorkerExtensionHost'); - const worker = new Worker(url, { name: 'WorkerExtensionHost' }); - - worker.onmessage = (event) => { - const { data } = event; - if (!(data instanceof ArrayBuffer)) { - console.warn('UNKNOWN data received', data); - this._onDidExit.fire([77, 'UNKNOWN data received']); - return; - } - - emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength))); - }; - - worker.onerror = (event) => { - console.error(event.message, event.error); - this._onDidExit.fire([81, event.message || event.error]); - }; - - // keep for cleanup - this._toDispose.add(emitter); - this._toDispose.add(toDisposable(() => worker.terminate())); - - const protocol: IMessagePassingProtocol = { - onMessage: emitter.event, - send: vsbuf => { - const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength); - worker.postMessage(data, [data]); - } - }; - - // extension host handshake happens below - // (1) <== wait for: Ready - // (2) ==> send: init data - // (3) <== wait for: Initialized - - await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Ready))); - protocol.send(VSBuffer.fromString(JSON.stringify(await this._createExtHostInitData()))); - await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Initialized))); - - // Register log channel for web worker exthost log - Registry.as(Extensions.OutputChannels).registerChannel({ id: 'webWorkerExtHostLog', label: localize('name', "Worker Extension Host"), file: this._extensionHostLogFile, log: true }); - - this._protocol = protocol; + public async start(): Promise { + if (!this._protocolPromise) { + if (WRAP_IN_IFRAME) { + this._protocolPromise = this._startInsideIframe(); + } else { + this._protocolPromise = this._startOutsideIframe(); + } + this._protocolPromise.then(protocol => this._protocol = protocol); } - return this._protocol; - + return this._protocolPromise; } - dispose(): void { - if (!this._protocol) { - this._toDispose.dispose(); + private _startInsideIframe(): Promise { + const emitter = this._register(new Emitter()); + + const iframe = document.createElement('iframe'); + iframe.setAttribute('class', 'web-worker-ext-host-iframe'); + iframe.setAttribute('sandbox', 'allow-scripts'); + iframe.style.display = 'none'; + + const nonce = generateUuid(); + const vscodeWebWorkerExtHostId = generateUuid(); + const workerUrl = require.toUrl('../worker/extensionHostWorkerMain.js'); + + const js = ` +(function() { + const workerUrl = "${getWorkerBootstrapUrl(workerUrl, 'WorkerExtensionHost', true)}"; + const worker = new Worker(workerUrl, { name: 'WorkerExtensionHost' }); + const vscodeWebWorkerExtHostId = '${vscodeWebWorkerExtHostId}'; + + worker.onmessage = (event) => { + const { data } = event; + if (!(data instanceof ArrayBuffer)) { + console.warn('Unknown data received', data); + window.parent.postMessage({ + vscodeWebWorkerExtHostId, + error: { + name: 'Error', + message: 'Unknown data received', + stack: [] + } + }, '*'); return; } + window.parent.postMessage({ + vscodeWebWorkerExtHostId, + data: data + }, '*', [data]); + }; + + worker.onerror = (event) => { + console.error(event.message, event.error); + window.parent.postMessage({ + vscodeWebWorkerExtHostId, + error: { + name: event.error.name, + message: event.error.message, + stack: event.error.stack + } + }, '*'); + }; + + window.addEventListener('message', function(event) { + if (event.source !== window.parent) { + return; + } + if (event.data.vscodeWebWorkerExtHostId !== vscodeWebWorkerExtHostId) { + return; + } + worker.postMessage(event.data.data, [event.data.data]); + }, false); +})(); +`; + let sourcesOrigin = location.origin; + if (/^(http:)|(https:)|(file:)/.test(workerUrl)) { + sourcesOrigin = new URL(workerUrl).origin; + } + + const html = ` + + + + + + + +`; + const iframeContent = `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; + iframe.setAttribute('src', iframeContent); + + this._register(dom.addDisposableListener(window, 'message', (event) => { + if (event.source !== iframe.contentWindow) { + return; + } + if (event.data.vscodeWebWorkerExtHostId !== vscodeWebWorkerExtHostId) { + return; + } + if (event.data.error) { + const { name, message, stack } = event.data.error; + const err = new Error(); + err.message = message; + err.name = name; + err.stack = stack; + onUnexpectedError(err); + this._onDidExit.fire([18, err.message]); + return; + } + const { data } = event.data; + if (!(data instanceof ArrayBuffer)) { + console.warn('UNKNOWN data received', data); + this._onDidExit.fire([77, 'UNKNOWN data received']); + return; + } + emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength))); + })); + + const protocol: IMessagePassingProtocol = { + onMessage: emitter.event, + send: vsbuf => { + const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength); + iframe.contentWindow!.postMessage({ + vscodeWebWorkerExtHostId, + data: data + }, '*', [data]); + } + }; + + document.body.appendChild(iframe); + this._register(toDisposable(() => iframe.remove())); + + return this._performHandshake(protocol); + } + + private _startOutsideIframe(): Promise { + const emitter = new Emitter(); + + const url = getWorkerBootstrapUrl(require.toUrl('../worker/extensionHostWorkerMain.js'), 'WorkerExtensionHost'); + const worker = new Worker(url, { name: 'WorkerExtensionHost' }); + + worker.onmessage = (event) => { + const { data } = event; + if (!(data instanceof ArrayBuffer)) { + console.warn('UNKNOWN data received', data); + this._onDidExit.fire([77, 'UNKNOWN data received']); + return; + } + + emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength))); + }; + + worker.onerror = (event) => { + console.error(event.message, event.error); + this._onDidExit.fire([81, event.message || event.error]); + }; + + // keep for cleanup + this._register(emitter); + this._register(toDisposable(() => worker.terminate())); + + const protocol: IMessagePassingProtocol = { + onMessage: emitter.event, + send: vsbuf => { + const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength); + worker.postMessage(data, [data]); + } + }; + + return this._performHandshake(protocol); + } + + private async _performHandshake(protocol: IMessagePassingProtocol): Promise { + // extension host handshake happens below + // (1) <== wait for: Ready + // (2) ==> send: init data + // (3) <== wait for: Initialized + + await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Ready))); + if (this._isTerminating) { + throw canceled(); + } + protocol.send(VSBuffer.fromString(JSON.stringify(await this._createExtHostInitData()))); + if (this._isTerminating) { + throw canceled(); + } + await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Initialized))); + if (this._isTerminating) { + throw canceled(); + } + + // Register log channel for web worker exthost log + Registry.as(Extensions.OutputChannels).registerChannel({ id: 'webWorkerExtHostLog', label: localize('name', "Worker Extension Host"), file: this._extensionHostLogFile, log: true }); + + return protocol; + } + + public dispose(): void { if (this._isTerminating) { return; } this._isTerminating = true; - this._protocol.send(createMessageOfType(MessageType.Terminate)); - setTimeout(() => this._toDispose.dispose(), 10 * 1000); + if (this._protocol) { + this._protocol.send(createMessageOfType(MessageType.Terminate)); + } + super.dispose(); } getInspectPort(): number | undefined { diff --git a/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts b/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts index e2d68b86b98..2041723074a 100644 --- a/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts +++ b/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts @@ -10,6 +10,7 @@ import { isMessageOfType, MessageType, createMessageOfType } from 'vs/workbench/ import { IInitData } from 'vs/workbench/api/common/extHost.protocol'; import { ExtensionHostMain } from 'vs/workbench/services/extensions/common/extensionHostMain'; import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService'; +import * as path from 'vs/base/common/path'; import 'vs/workbench/api/common/extHost.common.services'; import 'vs/workbench/api/worker/extHost.worker.services'; @@ -35,6 +36,17 @@ self.postMessage = () => console.trace(`'postMessage' has been blocked`); const nativeAddEventLister = addEventListener.bind(self); self.addEventLister = () => console.trace(`'addEventListener' has been blocked`); +if (location.protocol === 'data:') { + // make sure new Worker(...) always uses data: + const _Worker = Worker; + Worker = function (stringUrl: string | URL, options?: WorkerOptions) { + const js = `importScripts('${stringUrl}');`; + options = options || {}; + options.name = options.name || path.basename(stringUrl.toString()); + return new _Worker(`data:text/javascript;charset=utf-8,${encodeURIComponent(js)}`, options); + }; +} + //#endregion --- const hostUtil = new class implements IHostUtils {