|
|
|
@@ -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<IWebWorkerExtensionHostInitData>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<IMessagePassingProtocol> | 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<IMessagePassingProtocol> {
|
|
|
|
|
|
|
|
|
|
if (!this._protocol) {
|
|
|
|
|
|
|
|
|
|
const emitter = new Emitter<VSBuffer>();
|
|
|
|
|
|
|
|
|
|
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<IOutputChannelRegistry>(Extensions.OutputChannels).registerChannel({ id: 'webWorkerExtHostLog', label: localize('name', "Worker Extension Host"), file: this._extensionHostLogFile, log: true });
|
|
|
|
|
|
|
|
|
|
this._protocol = protocol;
|
|
|
|
|
public async start(): Promise<IMessagePassingProtocol> {
|
|
|
|
|
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<IMessagePassingProtocol> {
|
|
|
|
|
const emitter = this._register(new Emitter<VSBuffer>());
|
|
|
|
|
|
|
|
|
|
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 = `<!DOCTYPE html>
|
|
|
|
|
<html>
|
|
|
|
|
<head>
|
|
|
|
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${nonce}' 'unsafe-eval' ${sourcesOrigin} https://*.gallerycdn.vsassets.io; worker-src data:; connect-src ${sourcesOrigin} https://*.gallerycdn.vsassets.io" />
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<script nonce="${nonce}">${js}</script>
|
|
|
|
|
</body>
|
|
|
|
|
</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<IMessagePassingProtocol> {
|
|
|
|
|
const emitter = new Emitter<VSBuffer>();
|
|
|
|
|
|
|
|
|
|
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<IMessagePassingProtocol> {
|
|
|
|
|
// 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<IOutputChannelRegistry>(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 {
|
|
|
|
|