remote: make managed web resolvers work

This commit is contained in:
Connor Peet
2023-04-19 12:33:47 -07:00
parent f5427eed53
commit ddfcf37441
4 changed files with 191 additions and 20 deletions

View File

@@ -255,8 +255,7 @@ export class BrowserMain extends Disposable {
// Remote
const connectionToken = environmentService.options.connectionToken || getCookieValue(connectionTokenCookieName);
const expectResolverExtension = !!environmentService.remoteAuthority?.includes('+') && !environmentService.options.webSocketFactory;
const remoteAuthorityResolverService = new RemoteAuthorityResolverService(!expectResolverExtension, connectionToken, this.configuration.resourceUriProvider, productService, logService);
const remoteAuthorityResolverService = new RemoteAuthorityResolverService(!environmentService.expectsResolverExtension, connectionToken, this.configuration.resourceUriProvider, productService, logService);
serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService);
// Signing

View File

@@ -32,6 +32,11 @@ export interface IBrowserWorkbenchEnvironmentService extends IWorkbenchEnvironme
* Options used to configure the workbench.
*/
readonly options?: IWorkbenchConstructionOptions;
/**
* Gets whether a resolver extension is expected for the environment.
*/
readonly expectsResolverExtension: boolean;
}
export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvironmentService {
@@ -41,6 +46,11 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi
@memoize
get remoteAuthority(): string | undefined { return this.options.remoteAuthority; }
@memoize
get expectsResolverExtension(): boolean {
return !!this.options.remoteAuthority?.includes('+') && !this.options.webSocketFactory;
}
@memoize
get isBuilt(): boolean { return !!this.productService.commit; }

View File

@@ -62,6 +62,9 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment
@memoize
get remoteAuthority() { return this.configuration.remoteAuthority; }
@memoize
get expectsResolverExtension() { return !!this.configuration.remoteAuthority; }
@memoize
get execPath() { return this.configuration.execPath; }

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Schemas } from 'vs/base/common/network';
import { StopWatch } from 'vs/base/common/stopwatch';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ExtensionKind } from 'vs/platform/environment/common/environment';
import { ExtensionIdentifier, ExtensionType, IExtension, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
@@ -14,34 +15,40 @@ import { IAutomatedWindow } from 'vs/platform/log/browser/log';
import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IProductService } from 'vs/platform/product/common/productService';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection';
import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode, ResolverResult, getRemoteAuthorityPrefix } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRemoteExtensionsScannerService } from 'vs/platform/remote/common/remoteExtensionsScanner';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust';
import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService';
import { IWebExtensionsScannerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IWebWorkerExtensionHostDataProvider, IWebWorkerExtensionHostInitData, WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost';
import { FetchFileSystemProvider } from 'vs/workbench/services/extensions/browser/webWorkerFileSystemProvider';
import { AbstractExtensionService, IExtensionHostFactory, ResolvedExtensions } from 'vs/workbench/services/extensions/common/abstractExtensionService';
import { AbstractExtensionService, IExtensionHostFactory, ResolvedExtensions, checkEnabledAndProposedAPI } from 'vs/workbench/services/extensions/common/abstractExtensionService';
import { ExtensionHostKind, ExtensionRunningPreference, IExtensionHostKindPicker, extensionHostKindToString, extensionRunningPreferenceToString } from 'vs/workbench/services/extensions/common/extensionHostKind';
import { IResolveAuthorityErrorResult } from 'vs/workbench/services/extensions/common/extensionHostProxy';
import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService';
import { ExtensionRunningLocation } from 'vs/workbench/services/extensions/common/extensionRunningLocation';
import { ExtensionRunningLocationTracker } from 'vs/workbench/services/extensions/common/extensionRunningLocationTracker';
import { ExtensionRunningLocationTracker, filterExtensionDescriptions } from 'vs/workbench/services/extensions/common/extensionRunningLocationTracker';
import { ExtensionHostStartup, IExtensionHost, IExtensionService, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
import { ExtensionsProposedApi } from 'vs/workbench/services/extensions/common/extensionsProposedApi';
import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { IRemoteExtensionHostDataProvider, RemoteExtensionHost } from 'vs/workbench/services/extensions/common/remoteExtensionHost';
import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { IUserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit';
import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
export class ExtensionService extends AbstractExtensionService implements IExtensionService {
private _resolveAuthorityAttempt: number = 0;
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@INotificationService notificationService: INotificationService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IBrowserWorkbenchEnvironmentService private readonly _browserEnvironmentService: IBrowserWorkbenchEnvironmentService,
@ITelemetryService telemetryService: ITelemetryService,
@IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService,
@IFileService fileService: IFileService,
@@ -55,16 +62,21 @@ export class ExtensionService extends AbstractExtensionService implements IExten
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
@IRemoteExtensionsScannerService remoteExtensionsScannerService: IRemoteExtensionsScannerService,
@ILifecycleService lifecycleService: ILifecycleService,
@IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@IUserDataInitializationService private readonly _userDataInitializationService: IUserDataInitializationService,
@IUserDataProfileService private readonly _userDataProfileService: IUserDataProfileService,
@IWorkspaceTrustManagementService private readonly _workspaceTrustManagementService: IWorkspaceTrustManagementService,
@IRemoteExplorerService private readonly _remoteExplorerService: IRemoteExplorerService,
) {
const extensionsProposedApi = instantiationService.createInstance(ExtensionsProposedApi);
const extensionHostFactory = new BrowserExtensionHostFactory(
extensionsProposedApi,
() => this._scanWebExtensions(),
() => this._getExtensions(),
instantiationService,
remoteAgentService,
remoteAuthorityResolverService
_remoteAuthorityResolverService,
extensionEnablementService
);
super(
extensionsProposedApi,
@@ -72,7 +84,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
new BrowserExtensionHostKindPicker(logService),
instantiationService,
notificationService,
environmentService,
_browserEnvironmentService,
telemetryService,
extensionEnablementService,
fileService,
@@ -129,8 +141,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
return dedupExtensions(system, user, development, this._logService);
}
protected async _resolveExtensions(): Promise<ResolvedExtensions> {
// fetch the remote environment
protected async _resolveExtensionsDefault() {
const [localExtensions, remoteExtensions] = await Promise.all([
this._scanWebExtensions(),
this._remoteExtensionsScannerService.scanExtensions()
@@ -139,6 +150,50 @@ export class ExtensionService extends AbstractExtensionService implements IExten
return new ResolvedExtensions(localExtensions, remoteExtensions, /*hasLocalProcess*/false, /*allowRemoteExtensionsInLocalWebWorker*/true);
}
protected async _resolveExtensions(): Promise<ResolvedExtensions> {
if (!this._browserEnvironmentService.expectsResolverExtension) {
return this._resolveExtensionsDefault();
}
const remoteAuthority = this._environmentService.remoteAuthority!;
// Now that the canonical URI provider has been registered, we need to wait for the trust state to be
// calculated. The trust state will be used while resolving the authority, however the resolver can
// override the trust state through the resolver result.
await this._workspaceTrustManagementService.workspaceResolved;
let resolverResult: ResolverResult;
try {
resolverResult = await this._resolveAuthorityInitial(remoteAuthority);
} catch (err) {
if (RemoteAuthorityResolverError.isHandled(err)) {
console.log(`Error handled: Not showing a notification for the error`);
}
this._remoteAuthorityResolverService._setResolvedAuthorityError(remoteAuthority, err);
// Proceed with the local extension host
return this._resolveExtensionsDefault();
}
// set the resolved authority
this._remoteAuthorityResolverService._setResolvedAuthority(resolverResult.authority, resolverResult.options);
this._remoteExplorerService.setTunnelInformation(resolverResult.tunnelInformation);
// monitor for breakage
const connection = this._remoteAgentService.getConnection();
if (connection) {
connection.onDidStateChange(async (e) => {
if (e.type === PersistentConnectionEventType.ConnectionLost) {
this._remoteAuthorityResolverService._clearResolvedAuthority(remoteAuthority);
}
});
connection.onReconnecting(() => this._resolveAuthorityAgain());
}
return this._resolveExtensionsDefault();
}
protected _onExtensionHostExit(code: number): void {
// Dispose everything associated with the extension host
this.stopExtensionHosts();
@@ -148,15 +203,107 @@ export class ExtensionService extends AbstractExtensionService implements IExten
automatedWindow.codeAutomationExit(code);
}
}
// impl
private async _resolveAuthorityAgain(): Promise<void> {
const remoteAuthority = this._environmentService.remoteAuthority;
if (!remoteAuthority) {
return;
}
this._remoteAuthorityResolverService._clearResolvedAuthority(remoteAuthority);
try {
const result = await this._resolveAuthorityWithLogging(remoteAuthority);
this._remoteAuthorityResolverService._setResolvedAuthority(result.authority, result.options);
} catch (err) {
this._remoteAuthorityResolverService._setResolvedAuthorityError(remoteAuthority, err);
}
}
private async _resolveAuthorityInitial(remoteAuthority: string): Promise<ResolverResult> {
const MAX_ATTEMPTS = 5;
for (let attempt = 1; ; attempt++) {
try {
return this._resolveAuthorityWithLogging(remoteAuthority);
} catch (err) {
if (RemoteAuthorityResolverError.isNoResolverFound(err)) {
// There is no point in retrying if there is no resolver found
throw err;
}
if (RemoteAuthorityResolverError.isNotAvailable(err)) {
// The resolver is not available and asked us to not retry
throw err;
}
if (attempt >= MAX_ATTEMPTS) {
// Too many failed attempts, give up
throw err;
}
}
}
}
private async _resolveAuthorityWithLogging(remoteAuthority: string): Promise<ResolverResult> {
const authorityPrefix = getRemoteAuthorityPrefix(remoteAuthority);
const sw = StopWatch.create(false);
this._logService.info(`Invoking resolveAuthority(${authorityPrefix})...`);
try {
performance.mark(`code/willResolveAuthority/${authorityPrefix}`);
const result = await this._resolveAuthority(remoteAuthority);
performance.mark(`code/didResolveAuthorityOK/${authorityPrefix}`);
this._logService.info(`resolveAuthority(${authorityPrefix}) returned '${result.authority}' after ${sw.elapsed()} ms`);
return result;
} catch (err) {
performance.mark(`code/didResolveAuthorityError/${authorityPrefix}`);
this._logService.error(`resolveAuthority(${authorityPrefix}) returned an error after ${sw.elapsed()} ms`, err);
throw err;
}
}
private async _resolveAuthority(remoteAuthority: string): Promise<ResolverResult> {
const localWebWorkerExtensionHosts = this._getExtensionHostManagers(ExtensionHostKind.LocalWebWorker);
if (localWebWorkerExtensionHosts.length === 0) {
// no local process extension hosts
throw new Error(`Cannot resolve authority`);
}
this._resolveAuthorityAttempt++;
const results = await Promise.all(localWebWorkerExtensionHosts.map(extHost => extHost.resolveAuthority(remoteAuthority, this._resolveAuthorityAttempt)));
let bestErrorResult: IResolveAuthorityErrorResult | null = null;
for (const result of results) {
if (result.type === 'ok') {
return result.value;
}
if (!bestErrorResult) {
bestErrorResult = result;
continue;
}
const bestErrorIsUnknown = (bestErrorResult.error.code === RemoteAuthorityResolverErrorCode.Unknown);
const errorIsUnknown = (result.error.code === RemoteAuthorityResolverErrorCode.Unknown);
if (bestErrorIsUnknown && !errorIsUnknown) {
bestErrorResult = result;
}
}
// we can only reach this if there is an error
throw new RemoteAuthorityResolverError(bestErrorResult!.error.message, bestErrorResult!.error.code, bestErrorResult!.error.detail);
}
}
class BrowserExtensionHostFactory implements IExtensionHostFactory {
constructor(
private readonly _extensionsProposedApi: ExtensionsProposedApi,
private readonly _scanWebExtensions: () => Promise<IExtensionDescription[]>,
private readonly _getExtensions: () => Promise<IExtensionDescription[]>,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService,
@IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService,
@IWorkbenchExtensionEnablementService private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService,
) { }
createExtensionHost(runningLocations: ExtensionRunningLocationTracker, runningLocation: ExtensionRunningLocation, isInitialStart: boolean): IExtensionHost | null {
@@ -165,7 +312,7 @@ class BrowserExtensionHostFactory implements IExtensionHostFactory {
return null;
}
case ExtensionHostKind.LocalWebWorker: {
return this._instantiationService.createInstance(WebWorkerExtensionHost, runningLocation, ExtensionHostStartup.EagerAutoStart, this._createLocalExtensionHostDataProvider(runningLocations, runningLocation));
return this._instantiationService.createInstance(WebWorkerExtensionHost, runningLocation, ExtensionHostStartup.EagerAutoStart, this._createLocalExtensionHostDataProvider(runningLocations, runningLocation, isInitialStart));
}
case ExtensionHostKind.Remote: {
const remoteAgentConnection = this._remoteAgentService.getConnection();
@@ -177,15 +324,27 @@ class BrowserExtensionHostFactory implements IExtensionHostFactory {
}
}
private _createLocalExtensionHostDataProvider(runningLocations: ExtensionRunningLocationTracker, desiredRunningLocation: ExtensionRunningLocation): IWebWorkerExtensionHostDataProvider {
private _createLocalExtensionHostDataProvider(runningLocations: ExtensionRunningLocationTracker, desiredRunningLocation: ExtensionRunningLocation, isInitialStart: boolean): IWebWorkerExtensionHostDataProvider {
return {
getInitData: async (): Promise<IWebWorkerExtensionHostInitData> => {
const allExtensions = await this._getExtensions();
const localWebWorkerExtensions = runningLocations.filterByRunningLocation(allExtensions, desiredRunningLocation);
return {
allExtensions: allExtensions,
myExtensions: localWebWorkerExtensions.map(extension => extension.identifier)
};
if (isInitialStart) {
// Here we load even extensions that would be disabled by workspace trust
const localExtensions = checkEnabledAndProposedAPI(this._extensionEnablementService, this._extensionsProposedApi, await this._scanWebExtensions(), /* ignore workspace trust */true);
const runningLocation = runningLocations.computeRunningLocation(localExtensions, [], false);
const myExtensions = filterExtensionDescriptions(localExtensions, runningLocation, extRunningLocation => desiredRunningLocation.equals(extRunningLocation));
return {
allExtensions: localExtensions,
myExtensions: myExtensions.map(extension => extension.identifier)
};
} else {
// restart case
const allExtensions = await this._getExtensions();
const myExtensions = runningLocations.filterByRunningLocation(allExtensions, desiredRunningLocation);
return {
allExtensions: allExtensions,
myExtensions: myExtensions.map(extension => extension.identifier)
};
}
}
};
}