55
66import { getWorkerBootstrapUrl } from 'vs/base/worker/defaultWorkerFactory' ;
77import { Emitter , Event } from 'vs/base/common/event' ;
8- import { DisposableStore , toDisposable } from 'vs/base/common/lifecycle' ;
8+ import { toDisposable , Disposable } from 'vs/base/common/lifecycle' ;
99import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc' ;
1010import { VSBuffer } from 'vs/base/common/buffer' ;
1111import { createMessageOfType , MessageType , isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol' ;
@@ -16,6 +16,7 @@ import { ILabelService } from 'vs/platform/label/common/label';
1616import { ILogService } from 'vs/platform/log/common/log' ;
1717import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' ;
1818import * as platform from 'vs/base/common/platform' ;
19+ import * as dom from 'vs/base/browser/dom' ;
1920import { URI } from 'vs/base/common/uri' ;
2021import { IExtensionHost , ExtensionHostLogFileName , ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions' ;
2122import { IProductService } from 'vs/platform/product/common/productService' ;
@@ -24,6 +25,10 @@ import { joinPath } from 'vs/base/common/resources';
2425import { Registry } from 'vs/platform/registry/common/platform' ;
2526import { IOutputChannelRegistry , Extensions } from 'vs/workbench/services/output/common/output' ;
2627import { localize } from 'vs/nls' ;
28+ import { generateUuid } from 'vs/base/common/uuid' ;
29+ import { canceled , onUnexpectedError } from 'vs/base/common/errors' ;
30+
31+ const WRAP_IN_IFRAME = true ;
2732
2833export interface IWebWorkerExtensionHostInitData {
2934 readonly autoStart : boolean ;
@@ -34,17 +39,17 @@ export interface IWebWorkerExtensionHostDataProvider {
3439 getInitData ( ) : Promise < IWebWorkerExtensionHostInitData > ;
3540}
3641
37- export class WebWorkerExtensionHost implements IExtensionHost {
42+ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost {
3843
3944 public readonly kind = ExtensionHostKind . LocalWebWorker ;
4045 public readonly remoteAuthority = null ;
4146
42- private _toDispose = new DisposableStore ( ) ;
43- private _isTerminating : boolean = false ;
44- private _protocol ?: IMessagePassingProtocol ;
47+ private readonly _onDidExit = this . _register ( new Emitter < [ number , string | null ] > ( ) ) ;
48+ public readonly onExit : Event < [ number , string | null ] > = this . _onDidExit . event ;
4549
46- private readonly _onDidExit = new Emitter < [ number , string | null ] > ( ) ;
47- readonly onExit : Event < [ number , string | null ] > = this . _onDidExit . event ;
50+ private _isTerminating : boolean ;
51+ private _protocolPromise : Promise < IMessagePassingProtocol > | null ;
52+ private _protocol : IMessagePassingProtocol | null ;
4853
4954 private readonly _extensionHostLogsLocation : URI ;
5055 private readonly _extensionHostLogFile : URI ;
@@ -58,76 +63,218 @@ export class WebWorkerExtensionHost implements IExtensionHost {
5863 @IWorkbenchEnvironmentService private readonly _environmentService : IWorkbenchEnvironmentService ,
5964 @IProductService private readonly _productService : IProductService ,
6065 ) {
66+ super ( ) ;
67+ this . _isTerminating = false ;
68+ this . _protocolPromise = null ;
69+ this . _protocol = null ;
6170 this . _extensionHostLogsLocation = URI . file ( this . _environmentService . logsPath ) . with ( { scheme : this . _environmentService . logFile . scheme } ) ;
6271 this . _extensionHostLogFile = joinPath ( this . _extensionHostLogsLocation , `${ ExtensionHostLogFileName } .log` ) ;
6372 }
6473
65- async start ( ) : Promise < IMessagePassingProtocol > {
74+ public async start ( ) : Promise < IMessagePassingProtocol > {
75+ if ( ! this . _protocolPromise ) {
76+ if ( WRAP_IN_IFRAME ) {
77+ this . _protocolPromise = this . _startInsideIframe ( ) ;
78+ } else {
79+ this . _protocolPromise = this . _startOutsideIframe ( ) ;
80+ }
81+ this . _protocolPromise . then ( protocol => this . _protocol = protocol ) ;
82+ }
83+ return this . _protocolPromise ;
84+ }
6685
67- if ( ! this . _protocol ) {
86+ private _startInsideIframe ( ) : Promise < IMessagePassingProtocol > {
87+ const emitter = this . _register ( new Emitter < VSBuffer > ( ) ) ;
6888
69- const emitter = new Emitter < VSBuffer > ( ) ;
89+ const iframe = document . createElement ( 'iframe' ) ;
90+ iframe . setAttribute ( 'class' , 'web-worker-ext-host-iframe' ) ;
91+ iframe . setAttribute ( 'sandbox' , 'allow-scripts' ) ;
92+ iframe . style . display = 'none' ;
7093
71- const url = getWorkerBootstrapUrl ( require . toUrl ( '../worker/extensionHostWorkerMain.js' ) , 'WorkerExtensionHost' ) ;
72- const worker = new Worker ( url , { name : 'WorkerExtensionHost' } ) ;
94+ const nonce = generateUuid ( ) ;
95+ const vscodeWebWorkerExtHostId = generateUuid ( ) ;
96+ const workerUrl = require . toUrl ( '../worker/extensionHostWorkerMain.js' ) ;
7397
74- worker . onmessage = ( event ) => {
75- const { data } = event ;
76- if ( ! ( data instanceof ArrayBuffer ) ) {
77- console . warn ( 'UNKNOWN data received' , data ) ;
78- this . _onDidExit . fire ( [ 77 , 'UNKNOWN data received' ] ) ;
79- return ;
98+ const js = `
99+ (function() {
100+ const workerUrl = "${ getWorkerBootstrapUrl ( workerUrl , 'WorkerExtensionHost' , true ) } ";
101+ const worker = new Worker(workerUrl, { name: 'WorkerExtensionHost' });
102+ const vscodeWebWorkerExtHostId = '${ vscodeWebWorkerExtHostId } ';
103+
104+ worker.onmessage = (event) => {
105+ const { data } = event;
106+ if (!(data instanceof ArrayBuffer)) {
107+ console.warn('Unknown data received', data);
108+ window.parent.postMessage({
109+ vscodeWebWorkerExtHostId,
110+ error: {
111+ name: 'Error',
112+ message: 'Unknown data received',
113+ stack: []
80114 }
115+ }, '*');
116+ return;
117+ }
118+ window.parent.postMessage({
119+ vscodeWebWorkerExtHostId,
120+ data: data
121+ }, '*', [data]);
122+ };
123+
124+ worker.onerror = (event) => {
125+ console.error(event.message, event.error);
126+ window.parent.postMessage({
127+ vscodeWebWorkerExtHostId,
128+ error: {
129+ name: event.error.name,
130+ message: event.error.message,
131+ stack: event.error.stack
132+ }
133+ }, '*');
134+ };
135+
136+ window.addEventListener('message', function(event) {
137+ if (event.source !== window.parent) {
138+ return;
139+ }
140+ if (event.data.vscodeWebWorkerExtHostId !== vscodeWebWorkerExtHostId) {
141+ return;
142+ }
143+ worker.postMessage(event.data.data, [event.data.data]);
144+ }, false);
145+ })();
146+ ` ;
147+ let sourcesOrigin = location . origin ;
148+ if ( / ^ ( h t t p : ) | ( h t t p s : ) | ( f i l e : ) / . test ( workerUrl ) ) {
149+ sourcesOrigin = new URL ( workerUrl ) . origin ;
150+ }
81151
82- emitter . fire ( VSBuffer . wrap ( new Uint8Array ( data , 0 , data . byteLength ) ) ) ;
83- } ;
152+ const html = `<!DOCTYPE html>
153+ <html>
154+ <head>
155+ <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" />
156+ </head>
157+ <body>
158+ <script nonce="${ nonce } ">${ js } </script>
159+ </body>
160+ </html>` ;
161+ const iframeContent = `data:text/html;charset=utf-8,${ encodeURIComponent ( html ) } ` ;
162+ iframe . setAttribute ( 'src' , iframeContent ) ;
84163
85- worker . onerror = ( event ) => {
86- console . error ( event . message , event . error ) ;
87- this . _onDidExit . fire ( [ 81 , event . message || event . error ] ) ;
88- } ;
164+ this . _register ( dom . addDisposableListener ( window , 'message' , ( event ) => {
165+ if ( event . source !== iframe . contentWindow ) {
166+ return ;
167+ }
168+ if ( event . data . vscodeWebWorkerExtHostId !== vscodeWebWorkerExtHostId ) {
169+ return ;
170+ }
171+ if ( event . data . error ) {
172+ const { name, message, stack } = event . data . error ;
173+ const err = new Error ( ) ;
174+ err . message = message ;
175+ err . name = name ;
176+ err . stack = stack ;
177+ onUnexpectedError ( err ) ;
178+ this . _onDidExit . fire ( [ 18 , err . message ] ) ;
179+ return ;
180+ }
181+ const { data } = event . data ;
182+ if ( ! ( data instanceof ArrayBuffer ) ) {
183+ console . warn ( 'UNKNOWN data received' , data ) ;
184+ this . _onDidExit . fire ( [ 77 , 'UNKNOWN data received' ] ) ;
185+ return ;
186+ }
187+ emitter . fire ( VSBuffer . wrap ( new Uint8Array ( data , 0 , data . byteLength ) ) ) ;
188+ } ) ) ;
89189
90- // keep for cleanup
91- this . _toDispose . add ( emitter ) ;
92- this . _toDispose . add ( toDisposable ( ( ) => worker . terminate ( ) ) ) ;
190+ const protocol : IMessagePassingProtocol = {
191+ onMessage : emitter . event ,
192+ send : vsbuf => {
193+ const data = vsbuf . buffer . buffer . slice ( vsbuf . buffer . byteOffset , vsbuf . buffer . byteOffset + vsbuf . buffer . byteLength ) ;
194+ iframe . contentWindow ! . postMessage ( {
195+ vscodeWebWorkerExtHostId,
196+ data : data
197+ } , '*' , [ data ] ) ;
198+ }
199+ } ;
93200
94- const protocol : IMessagePassingProtocol = {
95- onMessage : emitter . event ,
96- send : vsbuf => {
97- const data = vsbuf . buffer . buffer . slice ( vsbuf . buffer . byteOffset , vsbuf . buffer . byteOffset + vsbuf . buffer . byteLength ) ;
98- worker . postMessage ( data , [ data ] ) ;
99- }
100- } ;
201+ document . body . appendChild ( iframe ) ;
202+ this . _register ( toDisposable ( ( ) => iframe . remove ( ) ) ) ;
203+
204+ return this . _performHandshake ( protocol ) ;
205+ }
101206
102- // extension host handshake happens below
103- // (1) <== wait for: Ready
104- // (2) ==> send: init data
105- // (3) <== wait for: Initialized
207+ private _startOutsideIframe ( ) : Promise < IMessagePassingProtocol > {
208+ const emitter = new Emitter < VSBuffer > ( ) ;
106209
107- await Event . toPromise ( Event . filter ( protocol . onMessage , msg => isMessageOfType ( msg , MessageType . Ready ) ) ) ;
108- protocol . send ( VSBuffer . fromString ( JSON . stringify ( await this . _createExtHostInitData ( ) ) ) ) ;
109- await Event . toPromise ( Event . filter ( protocol . onMessage , msg => isMessageOfType ( msg , MessageType . Initialized ) ) ) ;
210+ const url = getWorkerBootstrapUrl ( require . toUrl ( '../worker/extensionHostWorkerMain.js' ) , 'WorkerExtensionHost' ) ;
211+ const worker = new Worker ( url , { name : 'WorkerExtensionHost' } ) ;
110212
111- // Register log channel for web worker exthost log
112- Registry . as < IOutputChannelRegistry > ( Extensions . OutputChannels ) . registerChannel ( { id : 'webWorkerExtHostLog' , label : localize ( 'name' , "Worker Extension Host" ) , file : this . _extensionHostLogFile , log : true } ) ;
213+ worker . onmessage = ( event ) => {
214+ const { data } = event ;
215+ if ( ! ( data instanceof ArrayBuffer ) ) {
216+ console . warn ( 'UNKNOWN data received' , data ) ;
217+ this . _onDidExit . fire ( [ 77 , 'UNKNOWN data received' ] ) ;
218+ return ;
219+ }
113220
114- this . _protocol = protocol ;
115- }
116- return this . _protocol ;
221+ emitter . fire ( VSBuffer . wrap ( new Uint8Array ( data , 0 , data . byteLength ) ) ) ;
222+ } ;
223+
224+ worker . onerror = ( event ) => {
225+ console . error ( event . message , event . error ) ;
226+ this . _onDidExit . fire ( [ 81 , event . message || event . error ] ) ;
227+ } ;
117228
229+ // keep for cleanup
230+ this . _register ( emitter ) ;
231+ this . _register ( toDisposable ( ( ) => worker . terminate ( ) ) ) ;
232+
233+ const protocol : IMessagePassingProtocol = {
234+ onMessage : emitter . event ,
235+ send : vsbuf => {
236+ const data = vsbuf . buffer . buffer . slice ( vsbuf . buffer . byteOffset , vsbuf . buffer . byteOffset + vsbuf . buffer . byteLength ) ;
237+ worker . postMessage ( data , [ data ] ) ;
238+ }
239+ } ;
240+
241+ return this . _performHandshake ( protocol ) ;
118242 }
119243
120- dispose ( ) : void {
121- if ( ! this . _protocol ) {
122- this . _toDispose . dispose ( ) ;
123- return ;
244+ private async _performHandshake ( protocol : IMessagePassingProtocol ) : Promise < IMessagePassingProtocol > {
245+ // extension host handshake happens below
246+ // (1) <== wait for: Ready
247+ // (2) ==> send: init data
248+ // (3) <== wait for: Initialized
249+
250+ await Event . toPromise ( Event . filter ( protocol . onMessage , msg => isMessageOfType ( msg , MessageType . Ready ) ) ) ;
251+ if ( this . _isTerminating ) {
252+ throw canceled ( ) ;
124253 }
254+ protocol . send ( VSBuffer . fromString ( JSON . stringify ( await this . _createExtHostInitData ( ) ) ) ) ;
255+ if ( this . _isTerminating ) {
256+ throw canceled ( ) ;
257+ }
258+ await Event . toPromise ( Event . filter ( protocol . onMessage , msg => isMessageOfType ( msg , MessageType . Initialized ) ) ) ;
259+ if ( this . _isTerminating ) {
260+ throw canceled ( ) ;
261+ }
262+
263+ // Register log channel for web worker exthost log
264+ Registry . as < IOutputChannelRegistry > ( Extensions . OutputChannels ) . registerChannel ( { id : 'webWorkerExtHostLog' , label : localize ( 'name' , "Worker Extension Host" ) , file : this . _extensionHostLogFile , log : true } ) ;
265+
266+ return protocol ;
267+ }
268+
269+ public dispose ( ) : void {
125270 if ( this . _isTerminating ) {
126271 return ;
127272 }
128273 this . _isTerminating = true ;
129- this . _protocol . send ( createMessageOfType ( MessageType . Terminate ) ) ;
130- setTimeout ( ( ) => this . _toDispose . dispose ( ) , 10 * 1000 ) ;
274+ if ( this . _protocol ) {
275+ this . _protocol . send ( createMessageOfType ( MessageType . Terminate ) ) ;
276+ }
277+ super . dispose ( ) ;
131278 }
132279
133280 getInspectPort ( ) : number | undefined {
0 commit comments