Skip to content

Commit 6924627

Browse files
authored
resolvers: support nesting and exec server (microsoft#185169)
This adds support for nesting remote authorities via passing through ExecServers. - An authority like `wsl+Ubuntu@tunnel+my-pc` is parsed into the chain `tunnel+my-pc` -> `wsl+Ubuntu` - An extension for the `tunnel` prefixed is resolved. We expect the resolver to implement the new `resolveExecServer` method. - Resolution continues. `wsl+Ubuntu` is the last resolver, so the `resolve()` method is called and the exec server is passed in its `RemoteAuthorityResolverContext` Currently the ExecServer is typed as `unknown` in the API. _Maybe_ we want to make it real API in the future, but I don't want to do this until it's generalized beyond a single consumer (WSL). This also has adds utility method `getRemoteExecServer` to get an exec server for a given remote. This is used by WSL to probe information about a tunneled machine even when WSL isn't opened (e.g. to get the list of distros to shop.) The new `@` handling should not break remotes. WSL doesn't use `@` in its remotes, SSH and containers both hex-encode information contained in authorities. Codespaces does put the codespace name in the remote authority, but it doesn't seem like it's possible to get `@` in those names, since they're generated either randomly when opening a template or from a repo name (where @ is not allowed).
1 parent 979ae39 commit 6924627

File tree

3 files changed

+133
-77
lines changed

3 files changed

+133
-77
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
10421042
checkProposedApiEnabled(extension, 'resolvers');
10431043
return extHostLabelService.$registerResourceLabelFormatter(formatter);
10441044
},
1045+
getRemoteExecServer: (authority: string) => {
1046+
checkProposedApiEnabled(extension, 'resolvers');
1047+
return extensionService.getRemoteExecServer(authority);
1048+
},
10451049
onDidCreateFiles: (listener, thisArg, disposables) => {
10461050
return extHostFileSystemEvent.onDidCreateFile(listener, thisArg, disposables);
10471051
},

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

Lines changed: 106 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,11 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
783783
});
784784
}
785785

786+
public async getRemoteExecServer(remoteAuthority: string): Promise<vscode.ExecServer | undefined> {
787+
const { resolver } = await this._activateAndGetResolver(remoteAuthority);
788+
return resolver?.resolveExecServer?.(remoteAuthority, { resolveAttempt: 0 });
789+
}
790+
786791
// -- called by main thread
787792

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

801-
public async $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise<Dto<IResolveAuthorityResult>> {
806+
public async $resolveAuthority(remoteAuthorityChain: string, resolveAttempt: number): Promise<Dto<IResolveAuthorityResult>> {
802807
const sw = StopWatch.create(false);
803-
const prefix = () => `[resolveAuthority(${getRemoteAuthorityPrefix(remoteAuthority)},${resolveAttempt})][${sw.elapsed()}ms] `;
808+
const prefix = () => `[resolveAuthority(${getRemoteAuthorityPrefix(remoteAuthorityChain)},${resolveAttempt})][${sw.elapsed()}ms] `;
804809
const logInfo = (msg: string) => this._logService.info(`${prefix()}${msg}`);
805810
const logError = (msg: string, err: any = undefined) => this._logService.error(`${prefix()}${msg}`, err);
811+
const normalizeError = (err: unknown) => {
812+
if (err instanceof RemoteAuthorityResolverError) {
813+
return {
814+
type: 'error' as const,
815+
error: {
816+
code: err._code,
817+
message: err._message,
818+
detail: err._detail
819+
}
820+
};
821+
}
822+
throw err;
823+
};
806824

807-
logInfo(`activating resolver...`);
808-
const { authorityPrefix, resolver } = await this._activateAndGetResolver(remoteAuthority);
809-
if (!resolver) {
810-
logError(`no resolver`);
811-
return {
812-
type: 'error',
813-
error: {
814-
code: RemoteAuthorityResolverErrorCode.NoResolverFound,
815-
message: `No remote extension installed to resolve ${authorityPrefix}.`,
816-
detail: undefined
825+
const chain = remoteAuthorityChain.split(/@|%40/g).reverse();
826+
logInfo(`activating remote resolvers ${chain.join(' -> ')}`);
827+
828+
let resolvers;
829+
try {
830+
resolvers = await Promise.all(chain.map(async remoteAuthority => {
831+
logInfo(`activating resolver...`);
832+
const { resolver, authorityPrefix } = await this._activateAndGetResolver(remoteAuthority);
833+
if (!resolver) {
834+
logError(`no resolver`);
835+
throw new RemoteAuthorityResolverError(`No remote extension installed to resolve ${authorityPrefix}.`, RemoteAuthorityResolverErrorCode.NoResolverFound);
817836
}
818-
};
837+
return { resolver, authorityPrefix, remoteAuthority };
838+
}));
839+
} catch (e) {
840+
return normalizeError(e);
819841
}
820842

821843
const intervalLogger = new IntervalTimer();
822-
try {
823-
logInfo(`setting tunnel factory...`);
824-
this._register(await this._extHostTunnelService.setTunnelFactory(resolver));
825-
826-
intervalLogger.cancelAndSet(() => logInfo('waiting...'), 1000);
827-
logInfo(`invoking resolve()...`);
828-
performance.mark(`code/extHost/willResolveAuthority/${authorityPrefix}`);
829-
const result = await resolver.resolve(remoteAuthority, { resolveAttempt });
830-
performance.mark(`code/extHost/didResolveAuthorityOK/${authorityPrefix}`);
831-
intervalLogger.dispose();
832-
833-
const tunnelInformation: TunnelInformation = {
834-
environmentTunnels: result.environmentTunnels,
835-
features: result.tunnelFeatures
836-
};
844+
intervalLogger.cancelAndSet(() => logInfo('waiting...'), 1000);
837845

838-
// Split merged API result into separate authority/options
839-
const options: ResolvedOptions = {
840-
extensionHostEnv: result.extensionHostEnv,
841-
isTrusted: result.isTrusted,
842-
authenticationSession: result.authenticationSessionForInitializingExtensions ? { id: result.authenticationSessionForInitializingExtensions.id, providerId: result.authenticationSessionForInitializingExtensions.providerId } : undefined
843-
};
846+
let result!: vscode.ResolverResult;
847+
let execServer: vscode.ExecServer | undefined;
848+
for (const [i, { authorityPrefix, resolver, remoteAuthority }] of resolvers.entries()) {
849+
try {
850+
if (i === resolvers.length - 1) {
851+
logInfo(`invoking final resolve()...`);
852+
performance.mark(`code/extHost/willResolveAuthority/${authorityPrefix}`);
853+
result = await resolver.resolve(remoteAuthority, { resolveAttempt, execServer });
854+
performance.mark(`code/extHost/didResolveAuthorityOK/${authorityPrefix}`);
855+
// todo@connor4312: we probably need to chain tunnels too, how does this work with 'public' tunnels?
856+
logInfo(`setting tunnel factory...`);
857+
this._register(await this._extHostTunnelService.setTunnelFactory(resolver));
858+
} else {
859+
logInfo(`invoking resolveExecServer() for ${remoteAuthority}`);
860+
performance.mark(`code/extHost/willResolveExecServer/${authorityPrefix}`);
861+
execServer = await resolver.resolveExecServer?.(remoteAuthority, { resolveAttempt, execServer });
862+
if (!execServer) {
863+
throw new RemoteAuthorityResolverError(`Exec server was not available for ${remoteAuthority}`, RemoteAuthorityResolverErrorCode.NoResolverFound); // we did, in fact, break the chain :(
864+
}
865+
performance.mark(`code/extHost/didResolveExecServerOK/${authorityPrefix}`);
866+
}
867+
} catch (e) {
868+
performance.mark(`code/extHost/didResolveAuthorityError/${authorityPrefix}`);
869+
logError(`returned an error`, e);
870+
intervalLogger.dispose();
871+
return normalizeError(e);
872+
}
873+
}
844874

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

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

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

857-
authority = {
858-
authority: remoteAuthority,
859-
connectTo: new ManagedRemoteConnection(socketFactoryId),
860-
connectionToken: result.connectionToken
861-
};
862-
} else {
863-
authority = {
864-
authority: remoteAuthority,
865-
connectTo: new WebSocketRemoteConnection(result.host, result.port),
866-
connectionToken: result.connectionToken
867-
};
868-
}
889+
// extension are not required to return an instance of ResolvedAuthority or ManagedResolvedAuthority, so don't use `instanceof`
890+
logInfo(`returned ${ExtHostManagedResolvedAuthority.isManagedResolvedAuthority(result) ? 'managed authority' : `${result.host}:${result.port}`}`);
869891

870-
return {
871-
type: 'ok',
872-
value: {
873-
authority: authority as Dto<ResolvedAuthority>,
874-
options,
875-
tunnelInformation,
876-
}
892+
let authority: ResolvedAuthority;
893+
if (ExtHostManagedResolvedAuthority.isManagedResolvedAuthority(result)) {
894+
// The socket factory is identified by the `resolveAttempt`, since that is a number which
895+
// always increments and is unique over all resolve() calls in a workbench session.
896+
const socketFactoryId = resolveAttempt;
897+
898+
// There is only on managed socket factory at a time, so we can just overwrite the old one.
899+
this._extHostManagedSockets.setFactory(socketFactoryId, result.makeConnection);
900+
901+
authority = {
902+
authority: remoteAuthorityChain,
903+
connectTo: new ManagedRemoteConnection(socketFactoryId),
904+
connectionToken: result.connectionToken
905+
};
906+
} else {
907+
authority = {
908+
authority: remoteAuthorityChain,
909+
connectTo: new WebSocketRemoteConnection(result.host, result.port),
910+
connectionToken: result.connectionToken
877911
};
878-
} catch (err) {
879-
performance.mark(`code/extHost/didResolveAuthorityError/${authorityPrefix}`);
880-
intervalLogger.dispose();
881-
logError(`returned an error`, err);
882-
if (err instanceof RemoteAuthorityResolverError) {
883-
return {
884-
type: 'error',
885-
error: {
886-
code: err._code,
887-
message: err._message,
888-
detail: err._detail
889-
}
890-
};
891-
}
892-
throw err;
893912
}
913+
914+
return {
915+
type: 'ok',
916+
value: {
917+
authority: authority as Dto<ResolvedAuthority>,
918+
options,
919+
tunnelInformation,
920+
}
921+
};
894922
}
895923

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

10511080
onDidChangeRemoteConnectionData: Event<void>;
10521081
getRemoteConnectionData(): IRemoteConnectionData | null;

src/vscode-dts/vscode.proposed.resolvers.d.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ declare module 'vscode' {
1616

1717
export interface RemoteAuthorityResolverContext {
1818
resolveAttempt: number;
19+
/**
20+
* Exec server from a recursively-resolved remote authority. If the
21+
* remote authority includes nested authorities delimited by `@`, it is
22+
* resolved from outer to inner authorities with ExecServer passed down
23+
* to each resolver in the chain.
24+
*/
25+
execServer?: ExecServer;
1926
}
2027

2128
export class ResolvedAuthority {
@@ -142,6 +149,12 @@ declare module 'vscode' {
142149
constructor(message?: string);
143150
}
144151

152+
/**
153+
* Exec server used for nested resolvers. The type is currently not maintained
154+
* in these types, and is a contract between extensions.
155+
*/
156+
export type ExecServer = unknown;
157+
145158
export interface RemoteAuthorityResolver {
146159
/**
147160
* Resolve the authority part of the current opened `vscode-remote://` URI.
@@ -154,6 +167,15 @@ declare module 'vscode' {
154167
*/
155168
resolve(authority: string, context: RemoteAuthorityResolverContext): ResolverResult | Thenable<ResolverResult>;
156169

170+
/**
171+
* Resolves an exec server interface for the authority. Called if an
172+
* authority is a midpoint in a transit to the desired remote.
173+
*
174+
* @param authority The authority part of the current opened `vscode-remote://` URI.
175+
* @returns The exec server interface, as defined in a contract between extensions.
176+
*/
177+
resolveExecServer?(remoteAuthority: string, context: RemoteAuthorityResolverContext): ExecServer | Thenable<ExecServer>;
178+
157179
/**
158180
* Get the canonical URI (if applicable) for a `vscode-remote://` URI.
159181
*
@@ -210,6 +232,7 @@ declare module 'vscode' {
210232
export namespace workspace {
211233
export function registerRemoteAuthorityResolver(authorityPrefix: string, resolver: RemoteAuthorityResolver): Disposable;
212234
export function registerResourceLabelFormatter(formatter: ResourceLabelFormatter): Disposable;
235+
export function getRemoteExecServer(authority: string): Thenable<ExecServer | undefined>;
213236
}
214237

215238
export namespace env {

0 commit comments

Comments
 (0)