Skip to content

Commit 32b031e

Browse files
committed
Add support for running remote extensions in the local web worker extension host (#141322)
1 parent daf029d commit 32b031e

File tree

8 files changed

+66
-23
lines changed

8 files changed

+66
-23
lines changed

src/vs/base/common/network.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ class FileAccessImpl {
173173
asBrowserUri(uri: URI): URI;
174174
asBrowserUri(moduleId: string, moduleIdToUrl: { toUrl(moduleId: string): string }): URI;
175175
asBrowserUri(uriOrModule: URI | string, moduleIdToUrl?: { toUrl(moduleId: string): string }): URI {
176+
if (URI.isUri(uriOrModule) && platform.isWebWorker) {
177+
// In the web worker, only paths can be safely converted to browser URIs.
178+
// Other resources such as extension resources need to go to the main thread for conversion.
179+
console.warn(`FileAccess.asBrowserUri should not be used in the web worker!`);
180+
}
176181
const uri = this.toUri(uriOrModule, moduleIdToUrl);
177182

178183
// Handle remote URIs via `RemoteAuthorities`
@@ -188,7 +193,7 @@ class FileAccessImpl {
188193
// ...and we run in native environments
189194
platform.isNative ||
190195
// ...or web worker extensions on desktop
191-
(typeof platform.globals.importScripts === 'function' && platform.globals.origin === `${Schemas.vscodeFileResource}://${FileAccessImpl.FALLBACK_AUTHORITY}`)
196+
(platform.isWebWorker && platform.globals.origin === `${Schemas.vscodeFileResource}://${FileAccessImpl.FALLBACK_AUTHORITY}`)
192197
)
193198
) {
194199
return uri.with({

src/vs/base/common/platform.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export const isLinuxSnap = _isLinuxSnap;
147147
export const isNative = _isNative;
148148
export const isElectron = _isElectron;
149149
export const isWeb = _isWeb;
150+
export const isWebWorker = (_isWeb && typeof globals.importScripts === 'function');
150151
export const isIOS = _isIOS;
151152
/**
152153
* Whether we run inside a CI environment, such as

src/vs/workbench/api/browser/mainThreadExtensionService.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import { ICommandService } from 'vs/platform/commands/common/commands';
2424
import { IExtensionHostProxy, IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy';
2525
import { VSBuffer } from 'vs/base/common/buffer';
2626
import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
27-
import { URI } from 'vs/base/common/uri';
27+
import { URI, UriComponents } from 'vs/base/common/uri';
28+
import { FileAccess } from 'vs/base/common/network';
2829

2930
@extHostNamedCustomer(MainContext.MainThreadExtensionService)
3031
export class MainThreadExtensionService implements MainThreadExtensionServiceShape {
@@ -182,6 +183,10 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha
182183
this._timerService.setPerformanceMarks('remoteExtHost', marks);
183184
}
184185
}
186+
187+
async $asBrowserUri(uri: UriComponents): Promise<UriComponents> {
188+
return FileAccess.asBrowserUri(URI.revive(uri));
189+
}
185190
}
186191

187192
class ExtensionHostProxy implements IExtensionHostProxy {

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,7 @@ export interface MainThreadExtensionServiceShape extends IDisposable {
10411041
$onExtensionActivationError(extensionId: ExtensionIdentifier, error: SerializedError, missingExtensionDependency: MissingExtensionDependency | null): Promise<void>;
10421042
$onExtensionRuntimeError(extensionId: ExtensionIdentifier, error: SerializedError): void;
10431043
$setPerformanceMarks(marks: performance.PerformanceMark[]): Promise<void>;
1044+
$asBrowserUri(uri: UriComponents): Promise<UriComponents>;
10441045
}
10451046

10461047
export interface SCMProviderFeatures {

src/vs/workbench/api/common/extensionHostMain.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ export class ExtensionHostMain {
119119
});
120120
}
121121

122+
async asBrowserUri(uri: URI): Promise<URI> {
123+
const mainThreadExtensionsProxy = this._rpcProtocol.getProxy(MainContext.MainThreadExtensionService);
124+
return URI.revive(await mainThreadExtensionsProxy.$asBrowserUri(uri));
125+
}
126+
122127
terminate(reason: string): void {
123128
if (this._isTerminating) {
124129
// we are already shutting down...

src/vs/workbench/api/worker/extHostExtensionService.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio
1212
import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes';
1313
import { timeout } from 'vs/base/common/async';
1414
import { MainContext, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol';
15-
import { FileAccess } from 'vs/base/common/network';
1615

1716
class WorkerRequireInterceptor extends RequireInterceptor {
1817

@@ -57,12 +56,16 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
5756
}
5857

5958
protected async _loadCommonJSModule<T>(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
60-
6159
module = module.with({ path: ensureSuffix(module.path, '.js') });
6260
if (extensionId) {
6361
performance.mark(`code/extHost/willFetchExtensionCode/${extensionId.value}`);
6462
}
65-
const response = await fetch(FileAccess.asBrowserUri(module).toString(true));
63+
64+
// First resolve the extension entry point URI to something we can load using `fetch`
65+
// This needs to be done on the main thread due to a potential `resourceUriProvider` (workbench api)
66+
// which is only available in the main thread
67+
const browserUri = URI.revive(await this._mainThreadExtensionsProxy.$asBrowserUri(module));
68+
const response = await fetch(browserUri.toString(true));
6669
if (extensionId) {
6770
performance.mark(`code/extHost/didFetchExtensionCode/${extensionId.value}`);
6871
}

src/vs/workbench/api/worker/extensionHostWorker.ts

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,26 +43,37 @@ self.close = () => console.trace(`'close' has been blocked`);
4343
const nativePostMessage = postMessage.bind(self);
4444
self.postMessage = () => console.trace(`'postMessage' has been blocked`);
4545

46+
function shouldTransformUri(uri: string): boolean {
47+
// In principle, we could convert any URI, but we have concerns
48+
// that parsing https URIs might end up decoding escape characters
49+
// and result in an unintended transformation
50+
return /^(file|vscode-remote):/i.test(uri);
51+
}
52+
4653
const nativeFetch = fetch.bind(self);
47-
self.fetch = function (input, init) {
48-
if (input instanceof Request) {
49-
// Request object - massage not supported
54+
function patchFetching(asBrowserUri: (uri: URI) => Promise<URI>) {
55+
self.fetch = async function (input, init) {
56+
if (input instanceof Request) {
57+
// Request object - massage not supported
58+
return nativeFetch(input, init);
59+
}
60+
if (shouldTransformUri(String(input))) {
61+
input = (await asBrowserUri(URI.parse(String(input)))).toString(true);
62+
}
5063
return nativeFetch(input, init);
51-
}
52-
if (/^file:/i.test(String(input))) {
53-
input = FileAccess.asBrowserUri(URI.parse(String(input))).toString(true);
54-
}
55-
return nativeFetch(input, init);
56-
};
64+
};
5765

58-
self.XMLHttpRequest = class extends XMLHttpRequest {
59-
override open(method: string, url: string | URL, async?: boolean, username?: string | null, password?: string | null): void {
60-
if (/^file:/i.test(url.toString())) {
61-
url = FileAccess.asBrowserUri(URI.parse(url.toString())).toString(true);
66+
self.XMLHttpRequest = class extends XMLHttpRequest {
67+
override open(method: string, url: string | URL, async?: boolean, username?: string | null, password?: string | null): void {
68+
(async () => {
69+
if (shouldTransformUri(url.toString())) {
70+
url = (await asBrowserUri(URI.parse(url.toString()))).toString(true);
71+
}
72+
super.open(method, url, async ?? true, username, password);
73+
})();
6274
}
63-
return super.open(method, url, async ?? true, username, password);
64-
}
65-
};
75+
};
76+
}
6677

6778
self.importScripts = () => { throw new Error(`'importScripts' has been blocked`); };
6879

@@ -85,6 +96,11 @@ if ((<any>self).Worker) {
8596
Worker = <any>function (stringUrl: string | URL, options?: WorkerOptions) {
8697
if (/^file:/i.test(stringUrl.toString())) {
8798
stringUrl = FileAccess.asBrowserUri(URI.parse(stringUrl.toString())).toString(true);
99+
} else if (/^vscode-remote:/i.test(stringUrl.toString())) {
100+
// Supporting transformation of vscode-remote URIs requires an async call to the main thread,
101+
// but we cannot do this call from within the embedded Worker, and the only way out would be
102+
// to use templating instead of a function in the web api (`resourceUriProvider`)
103+
throw new Error(`Creating workers from remote extensions is currently not supported.`);
88104
}
89105

90106
// IMPORTANT: bootstrapFn is stringified and injected as worker blob-url. Because of that it CANNOT
@@ -244,6 +260,8 @@ export function create(): { onmessage: (message: any) => void } {
244260
message.data
245261
);
246262

263+
patchFetching(uri => extHostMain.asBrowserUri(uri));
264+
247265
onTerminate = (reason: string) => extHostMain.terminate(reason);
248266
});
249267
}

src/vs/workbench/services/extensions/browser/extensionService.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,15 @@ export class ExtensionService extends AbstractExtensionService implements IExten
197197
remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions, false);
198198

199199
const remoteAgentConnection = this._remoteAgentService.getConnection();
200+
// `determineRunningLocation` will look at the complete picture (e.g. an extension installed on both sides),
201+
// takes care of duplicates and picks a running location for each extension
200202
this._runningLocation = this._runningLocationClassifier.determineRunningLocation(localExtensions, remoteExtensions);
201203

202-
localExtensions = filterByRunningLocation(localExtensions, this._runningLocation, ExtensionRunningLocation.LocalWebWorker);
203-
remoteExtensions = filterByRunningLocation(remoteExtensions, this._runningLocation, ExtensionRunningLocation.Remote);
204+
// Remote extensions can run locally in the web worker, so mix everything up and split them again based on running location
205+
// NOTE: An extension can appear twice in `allExtensions`, but it will be filtered out based on running location below:
206+
const allExtensions = remoteExtensions.concat(localExtensions);
207+
localExtensions = filterByRunningLocation(allExtensions, this._runningLocation, ExtensionRunningLocation.LocalWebWorker);
208+
remoteExtensions = filterByRunningLocation(allExtensions, this._runningLocation, ExtensionRunningLocation.Remote);
204209

205210
const result = this._registry.deltaExtensions(remoteExtensions.concat(localExtensions), []);
206211
if (result.removedDueToLooping.length > 0) {

0 commit comments

Comments
 (0)