diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 922b021a1f9..ae36c0efa10 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -173,6 +173,11 @@ class FileAccessImpl { asBrowserUri(uri: URI): URI; asBrowserUri(moduleId: string, moduleIdToUrl: { toUrl(moduleId: string): string }): URI; asBrowserUri(uriOrModule: URI | string, moduleIdToUrl?: { toUrl(moduleId: string): string }): URI { + if (URI.isUri(uriOrModule) && platform.isWebWorker) { + // In the web worker, only paths can be safely converted to browser URIs. + // Other resources such as extension resources need to go to the main thread for conversion. + console.warn(`FileAccess.asBrowserUri should not be used in the web worker!`); + } const uri = this.toUri(uriOrModule, moduleIdToUrl); // Handle remote URIs via `RemoteAuthorities` @@ -188,7 +193,7 @@ class FileAccessImpl { // ...and we run in native environments platform.isNative || // ...or web worker extensions on desktop - (typeof platform.globals.importScripts === 'function' && platform.globals.origin === `${Schemas.vscodeFileResource}://${FileAccessImpl.FALLBACK_AUTHORITY}`) + (platform.isWebWorker && platform.globals.origin === `${Schemas.vscodeFileResource}://${FileAccessImpl.FALLBACK_AUTHORITY}`) ) ) { return uri.with({ diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index eea237c2b41..b2e6fef5f92 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -147,6 +147,7 @@ export const isLinuxSnap = _isLinuxSnap; export const isNative = _isNative; export const isElectron = _isElectron; export const isWeb = _isWeb; +export const isWebWorker = (_isWeb && typeof globals.importScripts === 'function'); export const isIOS = _isIOS; /** * Whether we run inside a CI environment, such as diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index 6456ee59f11..a77cf0e46e6 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -24,7 +24,8 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IExtensionHostProxy, IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; import { VSBuffer } from 'vs/base/common/buffer'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { FileAccess } from 'vs/base/common/network'; @extHostNamedCustomer(MainContext.MainThreadExtensionService) export class MainThreadExtensionService implements MainThreadExtensionServiceShape { @@ -182,6 +183,10 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha this._timerService.setPerformanceMarks('remoteExtHost', marks); } } + + async $asBrowserUri(uri: UriComponents): Promise { + return FileAccess.asBrowserUri(URI.revive(uri)); + } } class ExtensionHostProxy implements IExtensionHostProxy { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f5c4a615e34..91b1a26066f 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1041,6 +1041,7 @@ export interface MainThreadExtensionServiceShape extends IDisposable { $onExtensionActivationError(extensionId: ExtensionIdentifier, error: SerializedError, missingExtensionDependency: MissingExtensionDependency | null): Promise; $onExtensionRuntimeError(extensionId: ExtensionIdentifier, error: SerializedError): void; $setPerformanceMarks(marks: performance.PerformanceMark[]): Promise; + $asBrowserUri(uri: UriComponents): Promise; } export interface SCMProviderFeatures { diff --git a/src/vs/workbench/api/common/extensionHostMain.ts b/src/vs/workbench/api/common/extensionHostMain.ts index bfa0ac3126f..80f8d74b967 100644 --- a/src/vs/workbench/api/common/extensionHostMain.ts +++ b/src/vs/workbench/api/common/extensionHostMain.ts @@ -119,6 +119,11 @@ export class ExtensionHostMain { }); } + async asBrowserUri(uri: URI): Promise { + const mainThreadExtensionsProxy = this._rpcProtocol.getProxy(MainContext.MainThreadExtensionService); + return URI.revive(await mainThreadExtensionsProxy.$asBrowserUri(uri)); + } + terminate(reason: string): void { if (this._isTerminating) { // we are already shutting down... diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index c219a2eec64..4927fc3be3e 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -12,7 +12,6 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; import { timeout } from 'vs/base/common/async'; import { MainContext, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol'; -import { FileAccess } from 'vs/base/common/network'; class WorkerRequireInterceptor extends RequireInterceptor { @@ -57,12 +56,16 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { } protected async _loadCommonJSModule(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { - module = module.with({ path: ensureSuffix(module.path, '.js') }); if (extensionId) { performance.mark(`code/extHost/willFetchExtensionCode/${extensionId.value}`); } - const response = await fetch(FileAccess.asBrowserUri(module).toString(true)); + + // First resolve the extension entry point URI to something we can load using `fetch` + // This needs to be done on the main thread due to a potential `resourceUriProvider` (workbench api) + // which is only available in the main thread + const browserUri = URI.revive(await this._mainThreadExtensionsProxy.$asBrowserUri(module)); + const response = await fetch(browserUri.toString(true)); if (extensionId) { performance.mark(`code/extHost/didFetchExtensionCode/${extensionId.value}`); } diff --git a/src/vs/workbench/api/worker/extensionHostWorker.ts b/src/vs/workbench/api/worker/extensionHostWorker.ts index f439c2929ff..872f077cbb1 100644 --- a/src/vs/workbench/api/worker/extensionHostWorker.ts +++ b/src/vs/workbench/api/worker/extensionHostWorker.ts @@ -43,26 +43,37 @@ self.close = () => console.trace(`'close' has been blocked`); const nativePostMessage = postMessage.bind(self); self.postMessage = () => console.trace(`'postMessage' has been blocked`); -const nativeFetch = fetch.bind(self); -self.fetch = function (input, init) { - if (input instanceof Request) { - // Request object - massage not supported - return nativeFetch(input, init); - } - if (/^file:/i.test(String(input))) { - input = FileAccess.asBrowserUri(URI.parse(String(input))).toString(true); - } - return nativeFetch(input, init); -}; +function shouldTransformUri(uri: string): boolean { + // In principle, we could convert any URI, but we have concerns + // that parsing https URIs might end up decoding escape characters + // and result in an unintended transformation + return /^(file|vscode-remote):/i.test(uri); +} -self.XMLHttpRequest = class extends XMLHttpRequest { - override open(method: string, url: string | URL, async?: boolean, username?: string | null, password?: string | null): void { - if (/^file:/i.test(url.toString())) { - url = FileAccess.asBrowserUri(URI.parse(url.toString())).toString(true); +const nativeFetch = fetch.bind(self); +function patchFetching(asBrowserUri: (uri: URI) => Promise) { + self.fetch = async function (input, init) { + if (input instanceof Request) { + // Request object - massage not supported + return nativeFetch(input, init); } - return super.open(method, url, async ?? true, username, password); - } -}; + if (shouldTransformUri(String(input))) { + input = (await asBrowserUri(URI.parse(String(input)))).toString(true); + } + return nativeFetch(input, init); + }; + + self.XMLHttpRequest = class extends XMLHttpRequest { + override open(method: string, url: string | URL, async?: boolean, username?: string | null, password?: string | null): void { + (async () => { + if (shouldTransformUri(url.toString())) { + url = (await asBrowserUri(URI.parse(url.toString()))).toString(true); + } + super.open(method, url, async ?? true, username, password); + })(); + } + }; +} self.importScripts = () => { throw new Error(`'importScripts' has been blocked`); }; @@ -85,6 +96,11 @@ if ((self).Worker) { Worker = function (stringUrl: string | URL, options?: WorkerOptions) { if (/^file:/i.test(stringUrl.toString())) { stringUrl = FileAccess.asBrowserUri(URI.parse(stringUrl.toString())).toString(true); + } else if (/^vscode-remote:/i.test(stringUrl.toString())) { + // Supporting transformation of vscode-remote URIs requires an async call to the main thread, + // but we cannot do this call from within the embedded Worker, and the only way out would be + // to use templating instead of a function in the web api (`resourceUriProvider`) + throw new Error(`Creating workers from remote extensions is currently not supported.`); } // 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 } { message.data ); + patchFetching(uri => extHostMain.asBrowserUri(uri)); + onTerminate = (reason: string) => extHostMain.terminate(reason); }); } diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index a3372fab840..316fcc1040d 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -197,10 +197,15 @@ export class ExtensionService extends AbstractExtensionService implements IExten remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions, false); const remoteAgentConnection = this._remoteAgentService.getConnection(); + // `determineRunningLocation` will look at the complete picture (e.g. an extension installed on both sides), + // takes care of duplicates and picks a running location for each extension this._runningLocation = this._runningLocationClassifier.determineRunningLocation(localExtensions, remoteExtensions); - localExtensions = filterByRunningLocation(localExtensions, this._runningLocation, ExtensionRunningLocation.LocalWebWorker); - remoteExtensions = filterByRunningLocation(remoteExtensions, this._runningLocation, ExtensionRunningLocation.Remote); + // Remote extensions can run locally in the web worker, so mix everything up and split them again based on running location + // NOTE: An extension can appear twice in `allExtensions`, but it will be filtered out based on running location below: + const allExtensions = remoteExtensions.concat(localExtensions); + localExtensions = filterByRunningLocation(allExtensions, this._runningLocation, ExtensionRunningLocation.LocalWebWorker); + remoteExtensions = filterByRunningLocation(allExtensions, this._runningLocation, ExtensionRunningLocation.Remote); const result = this._registry.deltaExtensions(remoteExtensions.concat(localExtensions), []); if (result.removedDueToLooping.length > 0) {