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