Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resolvers: support nesting and exec server #185169

Merged
merged 1 commit into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension, 'resolvers');
return extHostLabelService.$registerResourceLabelFormatter(formatter);
},
getRemoteExecServer: (authority: string) => {
checkProposedApiEnabled(extension, 'resolvers');
return extensionService.getRemoteExecServer(authority);
},
onDidCreateFiles: (listener, thisArg, disposables) => {
return extHostFileSystemEvent.onDidCreateFile(listener, thisArg, disposables);
},
Expand Down
183 changes: 106 additions & 77 deletions src/vs/workbench/api/common/extHostExtensionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,11 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
});
}

public async getRemoteExecServer(remoteAuthority: string): Promise<vscode.ExecServer | undefined> {
const { resolver } = await this._activateAndGetResolver(remoteAuthority);
return resolver?.resolveExecServer?.(remoteAuthority, { resolveAttempt: 0 });
}

// -- called by main thread

private async _activateAndGetResolver(remoteAuthority: string): Promise<{ authorityPrefix: string; resolver: vscode.RemoteAuthorityResolver | undefined }> {
Expand All @@ -798,99 +803,122 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
return { authorityPrefix, resolver: this._resolvers[authorityPrefix] };
}

public async $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise<Dto<IResolveAuthorityResult>> {
public async $resolveAuthority(remoteAuthorityChain: string, resolveAttempt: number): Promise<Dto<IResolveAuthorityResult>> {
const sw = StopWatch.create(false);
const prefix = () => `[resolveAuthority(${getRemoteAuthorityPrefix(remoteAuthority)},${resolveAttempt})][${sw.elapsed()}ms] `;
const prefix = () => `[resolveAuthority(${getRemoteAuthorityPrefix(remoteAuthorityChain)},${resolveAttempt})][${sw.elapsed()}ms] `;
const logInfo = (msg: string) => this._logService.info(`${prefix()}${msg}`);
const logError = (msg: string, err: any = undefined) => this._logService.error(`${prefix()}${msg}`, err);
const normalizeError = (err: unknown) => {
if (err instanceof RemoteAuthorityResolverError) {
return {
type: 'error' as const,
error: {
code: err._code,
message: err._message,
detail: err._detail
}
};
}
throw err;
};

logInfo(`activating resolver...`);
const { authorityPrefix, resolver } = await this._activateAndGetResolver(remoteAuthority);
if (!resolver) {
logError(`no resolver`);
return {
type: 'error',
error: {
code: RemoteAuthorityResolverErrorCode.NoResolverFound,
message: `No remote extension installed to resolve ${authorityPrefix}.`,
detail: undefined
const chain = remoteAuthorityChain.split(/@|%40/g).reverse();
logInfo(`activating remote resolvers ${chain.join(' -> ')}`);

let resolvers;
try {
resolvers = await Promise.all(chain.map(async remoteAuthority => {
logInfo(`activating resolver...`);
const { resolver, authorityPrefix } = await this._activateAndGetResolver(remoteAuthority);
if (!resolver) {
logError(`no resolver`);
throw new RemoteAuthorityResolverError(`No remote extension installed to resolve ${authorityPrefix}.`, RemoteAuthorityResolverErrorCode.NoResolverFound);
}
};
return { resolver, authorityPrefix, remoteAuthority };
}));
} catch (e) {
return normalizeError(e);
}

const intervalLogger = new IntervalTimer();
try {
logInfo(`setting tunnel factory...`);
this._register(await this._extHostTunnelService.setTunnelFactory(resolver));

intervalLogger.cancelAndSet(() => logInfo('waiting...'), 1000);
logInfo(`invoking resolve()...`);
performance.mark(`code/extHost/willResolveAuthority/${authorityPrefix}`);
const result = await resolver.resolve(remoteAuthority, { resolveAttempt });
performance.mark(`code/extHost/didResolveAuthorityOK/${authorityPrefix}`);
intervalLogger.dispose();

const tunnelInformation: TunnelInformation = {
environmentTunnels: result.environmentTunnels,
features: result.tunnelFeatures
};
intervalLogger.cancelAndSet(() => logInfo('waiting...'), 1000);

// Split merged API result into separate authority/options
const options: ResolvedOptions = {
extensionHostEnv: result.extensionHostEnv,
isTrusted: result.isTrusted,
authenticationSession: result.authenticationSessionForInitializingExtensions ? { id: result.authenticationSessionForInitializingExtensions.id, providerId: result.authenticationSessionForInitializingExtensions.providerId } : undefined
};
let result!: vscode.ResolverResult;
let execServer: vscode.ExecServer | undefined;
for (const [i, { authorityPrefix, resolver, remoteAuthority }] of resolvers.entries()) {
try {
if (i === resolvers.length - 1) {
logInfo(`invoking final resolve()...`);
performance.mark(`code/extHost/willResolveAuthority/${authorityPrefix}`);
result = await resolver.resolve(remoteAuthority, { resolveAttempt, execServer });
performance.mark(`code/extHost/didResolveAuthorityOK/${authorityPrefix}`);
// todo@connor4312: we probably need to chain tunnels too, how does this work with 'public' tunnels?
logInfo(`setting tunnel factory...`);
this._register(await this._extHostTunnelService.setTunnelFactory(resolver));
} else {
logInfo(`invoking resolveExecServer() for ${remoteAuthority}`);
performance.mark(`code/extHost/willResolveExecServer/${authorityPrefix}`);
execServer = await resolver.resolveExecServer?.(remoteAuthority, { resolveAttempt, execServer });
if (!execServer) {
throw new RemoteAuthorityResolverError(`Exec server was not available for ${remoteAuthority}`, RemoteAuthorityResolverErrorCode.NoResolverFound); // we did, in fact, break the chain :(
}
performance.mark(`code/extHost/didResolveExecServerOK/${authorityPrefix}`);
}
} catch (e) {
performance.mark(`code/extHost/didResolveAuthorityError/${authorityPrefix}`);
logError(`returned an error`, e);
intervalLogger.dispose();
return normalizeError(e);
}
}

// extension are not required to return an instance of ResolvedAuthority or ManagedResolvedAuthority, so don't use `instanceof`
logInfo(`returned ${ExtHostManagedResolvedAuthority.isManagedResolvedAuthority(result) ? 'managed authority' : `${result.host}:${result.port}`}`);
intervalLogger.dispose();

let authority: ResolvedAuthority;
if (ExtHostManagedResolvedAuthority.isManagedResolvedAuthority(result)) {
// The socket factory is identified by the `resolveAttempt`, since that is a number which
// always increments and is unique over all resolve() calls in a workbench session.
const socketFactoryId = resolveAttempt;
const tunnelInformation: TunnelInformation = {
environmentTunnels: result.environmentTunnels,
features: result.tunnelFeatures
};

// There is only on managed socket factory at a time, so we can just overwrite the old one.
this._extHostManagedSockets.setFactory(socketFactoryId, result.makeConnection);
// Split merged API result into separate authority/options
const options: ResolvedOptions = {
extensionHostEnv: result.extensionHostEnv,
isTrusted: result.isTrusted,
authenticationSession: result.authenticationSessionForInitializingExtensions ? { id: result.authenticationSessionForInitializingExtensions.id, providerId: result.authenticationSessionForInitializingExtensions.providerId } : undefined
};

authority = {
authority: remoteAuthority,
connectTo: new ManagedRemoteConnection(socketFactoryId),
connectionToken: result.connectionToken
};
} else {
authority = {
authority: remoteAuthority,
connectTo: new WebSocketRemoteConnection(result.host, result.port),
connectionToken: result.connectionToken
};
}
// extension are not required to return an instance of ResolvedAuthority or ManagedResolvedAuthority, so don't use `instanceof`
logInfo(`returned ${ExtHostManagedResolvedAuthority.isManagedResolvedAuthority(result) ? 'managed authority' : `${result.host}:${result.port}`}`);

return {
type: 'ok',
value: {
authority: authority as Dto<ResolvedAuthority>,
options,
tunnelInformation,
}
let authority: ResolvedAuthority;
if (ExtHostManagedResolvedAuthority.isManagedResolvedAuthority(result)) {
// The socket factory is identified by the `resolveAttempt`, since that is a number which
// always increments and is unique over all resolve() calls in a workbench session.
const socketFactoryId = resolveAttempt;

// There is only on managed socket factory at a time, so we can just overwrite the old one.
this._extHostManagedSockets.setFactory(socketFactoryId, result.makeConnection);

authority = {
authority: remoteAuthorityChain,
connectTo: new ManagedRemoteConnection(socketFactoryId),
connectionToken: result.connectionToken
};
} else {
authority = {
authority: remoteAuthorityChain,
connectTo: new WebSocketRemoteConnection(result.host, result.port),
connectionToken: result.connectionToken
};
} catch (err) {
performance.mark(`code/extHost/didResolveAuthorityError/${authorityPrefix}`);
intervalLogger.dispose();
logError(`returned an error`, err);
if (err instanceof RemoteAuthorityResolverError) {
return {
type: 'error',
error: {
code: err._code,
message: err._message,
detail: err._detail
}
};
}
throw err;
}

return {
type: 'ok',
value: {
authority: authority as Dto<ResolvedAuthority>,
options,
tunnelInformation,
}
};
}

public async $getCanonicalURI(remoteAuthority: string, uriComponents: UriComponents): Promise<UriComponents | null> {
Expand Down Expand Up @@ -1047,6 +1075,7 @@ export interface IExtHostExtensionService extends AbstractExtHostExtensionServic
getExtensionRegistry(): Promise<ExtensionDescriptionRegistry>;
getExtensionPathIndex(): Promise<ExtensionPaths>;
registerRemoteAuthorityResolver(authorityPrefix: string, resolver: vscode.RemoteAuthorityResolver): vscode.Disposable;
getRemoteExecServer(authority: string): Promise<vscode.ExecServer | undefined>;

onDidChangeRemoteConnectionData: Event<void>;
getRemoteConnectionData(): IRemoteConnectionData | null;
Expand Down
23 changes: 23 additions & 0 deletions src/vscode-dts/vscode.proposed.resolvers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ declare module 'vscode' {

export interface RemoteAuthorityResolverContext {
resolveAttempt: number;
/**
* Exec server from a recursively-resolved remote authority. If the
* remote authority includes nested authorities delimited by `@`, it is
* resolved from outer to inner authorities with ExecServer passed down
* to each resolver in the chain.
*/
execServer?: ExecServer;
}

export class ResolvedAuthority {
Expand Down Expand Up @@ -142,6 +149,12 @@ declare module 'vscode' {
constructor(message?: string);
}

/**
* Exec server used for nested resolvers. The type is currently not maintained
* in these types, and is a contract between extensions.
*/
export type ExecServer = unknown;

export interface RemoteAuthorityResolver {
/**
* Resolve the authority part of the current opened `vscode-remote://` URI.
Expand All @@ -154,6 +167,15 @@ declare module 'vscode' {
*/
resolve(authority: string, context: RemoteAuthorityResolverContext): ResolverResult | Thenable<ResolverResult>;

/**
* Resolves an exec server interface for the authority. Called if an
* authority is a midpoint in a transit to the desired remote.
*
* @param authority The authority part of the current opened `vscode-remote://` URI.
* @returns The exec server interface, as defined in a contract between extensions.
*/
resolveExecServer?(remoteAuthority: string, context: RemoteAuthorityResolverContext): ExecServer | Thenable<ExecServer>;

/**
* Get the canonical URI (if applicable) for a `vscode-remote://` URI.
*
Expand Down Expand Up @@ -210,6 +232,7 @@ declare module 'vscode' {
export namespace workspace {
export function registerRemoteAuthorityResolver(authorityPrefix: string, resolver: RemoteAuthorityResolver): Disposable;
export function registerResourceLabelFormatter(formatter: ResourceLabelFormatter): Disposable;
export function getRemoteExecServer(authority: string): Thenable<ExecServer | undefined>;
}

export namespace env {
Expand Down