Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../../base/browser/dom.js';
7
import { parentOriginHash } from '../../../../base/browser/iframe.js';
8
import { mainWindow } from '../../../../base/browser/window.js';
9
import { Barrier } from '../../../../base/common/async.js';
10
import { VSBuffer } from '../../../../base/common/buffer.js';
11
import { canceled, onUnexpectedError } from '../../../../base/common/errors.js';
12
import { Emitter, Event } from '../../../../base/common/event.js';
13
import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js';
14
import { AppResourcePath, COI, FileAccess } from '../../../../base/common/network.js';
15
import * as platform from '../../../../base/common/platform.js';
16
import { joinPath } from '../../../../base/common/resources.js';
17
import { URI } from '../../../../base/common/uri.js';
18
import { generateUuid } from '../../../../base/common/uuid.js';
19
import { IMessagePassingProtocol } from '../../../../base/parts/ipc/common/ipc.js';
20
import { getNLSLanguage, getNLSMessages } from '../../../../nls.js';
21
import { ILabelService } from '../../../../platform/label/common/label.js';
22
import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js';
23
import { ILogService, ILoggerService } from '../../../../platform/log/common/log.js';
24
import { IProductService } from '../../../../platform/product/common/productService.js';
25
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
26
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
27
import { isLoggingOnly } from '../../../../platform/telemetry/common/telemetryUtils.js';
28
import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
29
import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
30
import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js';
31
import { ExtensionHostExitCode, IExtensionHostInitData, MessageType, UIKind, createMessageOfType, isMessageOfType } from '../common/extensionHostProtocol.js';
32
import { LocalWebWorkerRunningLocation } from '../common/extensionRunningLocation.js';
33
import { ExtensionHostExtensions, ExtensionHostStartup, IExtensionHost } from '../common/extensions.js';
34
35
export interface IWebWorkerExtensionHostInitData {
36
readonly extensions: ExtensionHostExtensions;
37
}
38
39
export interface IWebWorkerExtensionHostDataProvider {
40
getInitData(): Promise<IWebWorkerExtensionHostInitData>;
41
}
42
43
export class WebWorkerExtensionHost extends Disposable implements IExtensionHost {
44
45
public readonly pid = null;
46
public readonly remoteAuthority = null;
47
public extensions: ExtensionHostExtensions | null = null;
48
49
private readonly _onDidExit = this._register(new Emitter<[number, string | null]>());
50
public readonly onExit: Event<[number, string | null]> = this._onDidExit.event;
51
52
private _isTerminating: boolean;
53
private _protocolPromise: Promise<IMessagePassingProtocol> | null;
54
private _protocol: IMessagePassingProtocol | null;
55
56
private readonly _extensionHostLogsLocation: URI;
57
58
constructor(
59
public readonly runningLocation: LocalWebWorkerRunningLocation,
60
public readonly startup: ExtensionHostStartup,
61
private readonly _initDataProvider: IWebWorkerExtensionHostDataProvider,
62
@ITelemetryService private readonly _telemetryService: ITelemetryService,
63
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
64
@ILabelService private readonly _labelService: ILabelService,
65
@ILogService private readonly _logService: ILogService,
66
@ILoggerService private readonly _loggerService: ILoggerService,
67
@IBrowserWorkbenchEnvironmentService private readonly _environmentService: IBrowserWorkbenchEnvironmentService,
68
@IUserDataProfilesService private readonly _userDataProfilesService: IUserDataProfilesService,
69
@IProductService private readonly _productService: IProductService,
70
@ILayoutService private readonly _layoutService: ILayoutService,
71
@IStorageService private readonly _storageService: IStorageService,
72
) {
73
super();
74
this._isTerminating = false;
75
this._protocolPromise = null;
76
this._protocol = null;
77
this._extensionHostLogsLocation = joinPath(this._environmentService.extHostLogsPath, 'webWorker');
78
}
79
80
private async _getWebWorkerExtensionHostIframeSrc(): Promise<string> {
81
const suffixSearchParams = new URLSearchParams();
82
if (this._environmentService.debugExtensionHost && this._environmentService.debugRenderer) {
83
suffixSearchParams.set('debugged', '1');
84
}
85
COI.addSearchParam(suffixSearchParams, true, true);
86
87
const suffix = `?${suffixSearchParams.toString()}`;
88
89
const iframeModulePath: AppResourcePath = `vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html`;
90
if (platform.isWeb) {
91
const webEndpointUrlTemplate = this._productService.webEndpointUrlTemplate;
92
const commit = this._productService.commit;
93
const quality = this._productService.quality;
94
if (webEndpointUrlTemplate && commit && quality) {
95
// Try to keep the web worker extension host iframe origin stable by storing it in workspace storage
96
const key = 'webWorkerExtensionHostIframeStableOriginUUID';
97
let stableOriginUUID = this._storageService.get(key, StorageScope.WORKSPACE);
98
if (typeof stableOriginUUID === 'undefined') {
99
stableOriginUUID = generateUuid();
100
this._storageService.store(key, stableOriginUUID, StorageScope.WORKSPACE, StorageTarget.MACHINE);
101
}
102
const hash = await parentOriginHash(mainWindow.origin, stableOriginUUID);
103
const baseUrl = (
104
webEndpointUrlTemplate
105
.replace('{{uuid}}', `v--${hash}`) // using `v--` as a marker to require `parentOrigin`/`salt` verification
106
.replace('{{commit}}', commit)
107
.replace('{{quality}}', quality)
108
);
109
110
const res = new URL(`${baseUrl}/out/${iframeModulePath}${suffix}`);
111
res.searchParams.set('parentOrigin', mainWindow.origin);
112
res.searchParams.set('salt', stableOriginUUID);
113
return res.toString();
114
}
115
116
console.warn(`The web worker extension host is started in a same-origin iframe!`);
117
}
118
119
const relativeExtensionHostIframeSrc = FileAccess.asBrowserUri(iframeModulePath);
120
return `${relativeExtensionHostIframeSrc.toString(true)}${suffix}`;
121
}
122
123
public async start(): Promise<IMessagePassingProtocol> {
124
if (!this._protocolPromise) {
125
this._protocolPromise = this._startInsideIframe();
126
this._protocolPromise.then(protocol => this._protocol = protocol);
127
}
128
return this._protocolPromise;
129
}
130
131
private async _startInsideIframe(): Promise<IMessagePassingProtocol> {
132
const webWorkerExtensionHostIframeSrc = await this._getWebWorkerExtensionHostIframeSrc();
133
const emitter = this._register(new Emitter<VSBuffer>());
134
135
const iframe = document.createElement('iframe');
136
iframe.setAttribute('class', 'web-worker-ext-host-iframe');
137
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
138
iframe.setAttribute('allow', 'usb; serial; hid; cross-origin-isolated;');
139
iframe.setAttribute('aria-hidden', 'true');
140
iframe.style.display = 'none';
141
142
const vscodeWebWorkerExtHostId = generateUuid();
143
iframe.setAttribute('src', `${webWorkerExtensionHostIframeSrc}&vscodeWebWorkerExtHostId=${vscodeWebWorkerExtHostId}`);
144
145
const barrier = new Barrier();
146
let port!: MessagePort;
147
let barrierError: Error | null = null;
148
let barrierHasError = false;
149
let startTimeout: Timeout | undefined = undefined;
150
151
const rejectBarrier = (exitCode: number, error: Error) => {
152
barrierError = error;
153
barrierHasError = true;
154
onUnexpectedError(barrierError);
155
clearTimeout(startTimeout);
156
this._onDidExit.fire([ExtensionHostExitCode.UnexpectedError, barrierError.message]);
157
barrier.open();
158
};
159
160
const resolveBarrier = (messagePort: MessagePort) => {
161
port = messagePort;
162
clearTimeout(startTimeout);
163
barrier.open();
164
};
165
166
startTimeout = setTimeout(() => {
167
console.warn(`The Web Worker Extension Host did not start in 60s, that might be a problem.`);
168
}, 60000);
169
170
this._register(dom.addDisposableListener(mainWindow, 'message', (event) => {
171
if (event.source !== iframe.contentWindow) {
172
return;
173
}
174
if (event.data.vscodeWebWorkerExtHostId !== vscodeWebWorkerExtHostId) {
175
return;
176
}
177
if (event.data.error) {
178
const { name, message, stack } = event.data.error;
179
const err = new Error();
180
err.message = message;
181
err.name = name;
182
err.stack = stack;
183
return rejectBarrier(ExtensionHostExitCode.UnexpectedError, err);
184
}
185
if (event.data.type === 'vscode.bootstrap.nls') {
186
iframe.contentWindow!.postMessage({
187
type: event.data.type,
188
data: {
189
workerUrl: FileAccess.asBrowserUri('vs/workbench/api/worker/extensionHostWorkerMain.js').toString(true),
190
fileRoot: globalThis._VSCODE_FILE_ROOT,
191
nls: {
192
messages: getNLSMessages(),
193
language: getNLSLanguage()
194
}
195
}
196
}, '*');
197
return;
198
}
199
const { data } = event.data;
200
if (barrier.isOpen() || !(data instanceof MessagePort)) {
201
console.warn('UNEXPECTED message', event);
202
const err = new Error('UNEXPECTED message');
203
return rejectBarrier(ExtensionHostExitCode.UnexpectedError, err);
204
}
205
resolveBarrier(data);
206
}));
207
208
this._layoutService.mainContainer.appendChild(iframe);
209
this._register(toDisposable(() => iframe.remove()));
210
211
// await MessagePort and use it to directly communicate
212
// with the worker extension host
213
await barrier.wait();
214
215
if (barrierHasError) {
216
throw barrierError;
217
}
218
219
// Send over message ports for extension API
220
const messagePorts = this._environmentService.options?.messagePorts ?? new Map();
221
iframe.contentWindow!.postMessage({ type: 'vscode.init', data: messagePorts }, '*', [...messagePorts.values()]);
222
223
port.onmessage = (event) => {
224
const { data } = event;
225
if (!(data instanceof ArrayBuffer)) {
226
console.warn('UNKNOWN data received', data);
227
this._onDidExit.fire([77, 'UNKNOWN data received']);
228
return;
229
}
230
emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength)));
231
};
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
port.postMessage(data, [data]);
238
}
239
};
240
241
return this._performHandshake(protocol);
242
}
243
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();
253
}
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
return protocol;
264
}
265
266
public override dispose(): void {
267
if (this._isTerminating) {
268
return;
269
}
270
this._isTerminating = true;
271
this._protocol?.send(createMessageOfType(MessageType.Terminate));
272
super.dispose();
273
}
274
275
getInspectPort(): undefined {
276
return undefined;
277
}
278
279
enableInspectPort(): Promise<boolean> {
280
return Promise.resolve(false);
281
}
282
283
private async _createExtHostInitData(): Promise<IExtensionHostInitData> {
284
const initData = await this._initDataProvider.getInitData();
285
this.extensions = initData.extensions;
286
const workspace = this._contextService.getWorkspace();
287
const nlsBaseUrl = this._productService.extensionsGallery?.nlsBaseUrl;
288
let nlsUrlWithDetails: URI | undefined = undefined;
289
// Only use the nlsBaseUrl if we are using a language other than the default, English.
290
if (nlsBaseUrl && this._productService.commit && !platform.Language.isDefaultVariant()) {
291
nlsUrlWithDetails = URI.joinPath(URI.parse(nlsBaseUrl), this._productService.commit, this._productService.version, platform.Language.value());
292
}
293
return {
294
commit: this._productService.commit,
295
version: this._productService.version,
296
quality: this._productService.quality,
297
date: this._productService.date,
298
parentPid: 0,
299
environment: {
300
isExtensionDevelopmentDebug: this._environmentService.debugRenderer,
301
appName: this._productService.nameLong,
302
appHost: this._productService.embedderIdentifier ?? (platform.isWeb ? 'web' : 'desktop'),
303
appUriScheme: this._productService.urlProtocol,
304
appLanguage: platform.language,
305
isExtensionTelemetryLoggingOnly: isLoggingOnly(this._productService, this._environmentService),
306
extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,
307
extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI,
308
globalStorageHome: this._userDataProfilesService.defaultProfile.globalStorageHome,
309
workspaceStorageHome: this._environmentService.workspaceStorageHome,
310
extensionLogLevel: this._environmentService.extensionLogLevel
311
},
312
workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : {
313
configuration: workspace.configuration || undefined,
314
id: workspace.id,
315
name: this._labelService.getWorkspaceLabel(workspace),
316
transient: workspace.transient
317
},
318
consoleForward: {
319
includeStack: false,
320
logNative: this._environmentService.debugRenderer
321
},
322
extensions: this.extensions.toSnapshot(),
323
nlsBaseUrl: nlsUrlWithDetails,
324
telemetryInfo: {
325
sessionId: this._telemetryService.sessionId,
326
machineId: this._telemetryService.machineId,
327
sqmId: this._telemetryService.sqmId,
328
devDeviceId: this._telemetryService.devDeviceId,
329
firstSessionDate: this._telemetryService.firstSessionDate,
330
msftInternal: this._telemetryService.msftInternal
331
},
332
logLevel: this._logService.getLevel(),
333
loggers: [...this._loggerService.getRegisteredLoggers()],
334
logsLocation: this._extensionHostLogsLocation,
335
autoStart: (this.startup === ExtensionHostStartup.EagerAutoStart || this.startup === ExtensionHostStartup.LazyAutoStart),
336
remote: {
337
authority: this._environmentService.remoteAuthority,
338
connectionData: null,
339
isRemote: false
340
},
341
uiKind: platform.isWeb ? UIKind.Web : UIKind.Desktop
342
};
343
}
344
}
345
346