From 27bac598cc7239a46f88b6050c840d3416e3328e Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 18 Sep 2019 18:20:08 -0700 Subject: [PATCH] Basic implementation of `resolveExternalUri` --- src/vs/vscode.proposed.d.ts | 23 +++++++++ .../workbench/api/browser/mainThreadWindow.ts | 47 ++++++++++++++----- .../workbench/api/common/extHost.api.impl.ts | 4 ++ .../workbench/api/common/extHost.protocol.ts | 2 + src/vs/workbench/api/common/extHostWindow.ts | 17 +++++++ 5 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 7dc2a800f64a7..55d6e3e99c5b1 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1089,4 +1089,27 @@ declare module 'vscode' { } //#endregion + + // #region resolveExternalUri — mjbvz + + namespace env { + /** + * Resolves an *external* uri, such as a `http:` or `https:` link, from where the extension is running to a + * uri to the same resource on the client machine. + * + * This is a no-oop if the extension is running locally. Currently only supports `https:` and `http:`. + * + * If the extension is running remotely, this function automatically establishes port forwarding from + * the local machine to `target` on the remote and returns a local uri that can be used to for this connection. + * + * Note that uris passed through `openExternal` are automatically resolved. + * + * @return A uri that can be used on the client machine. Extensions should dispose of the returned value when + * both the extension and the user are no longer using the value. For port forwarded uris, `dispose` will + * close the connection. + */ + export function resolveExternalUri(target: Uri): Thenable<{ resolved: Uri, dispose(): void }>; + } + + //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadWindow.ts b/src/vs/workbench/api/browser/mainThreadWindow.ts index df059758f65e9..869f1b68fe78f 100644 --- a/src/vs/workbench/api/browser/mainThreadWindow.ts +++ b/src/vs/workbench/api/browser/mainThreadWindow.ts @@ -19,7 +19,7 @@ export class MainThreadWindow implements MainThreadWindowShape { private readonly proxy: ExtHostWindowShape; private readonly disposables = new DisposableStore(); - private readonly _tunnels = new Map>(); + private readonly _tunnels = new Map }>(); constructor( extHostContext: IExtHostContext, @@ -37,8 +37,8 @@ export class MainThreadWindow implements MainThreadWindowShape { dispose(): void { this.disposables.dispose(); - for (const tunnel of this._tunnels.values()) { - tunnel.then(tunnel => tunnel.dispose()); + for (const { value } of this._tunnels.values()) { + value.then(tunnel => tunnel.dispose()); } this._tunnels.clear(); } @@ -47,29 +47,52 @@ export class MainThreadWindow implements MainThreadWindowShape { return this.windowService.isFocused(); } - async $openUri(uriComponent: UriComponents, options: IOpenUriOptions): Promise { - let uri = URI.revive(uriComponent); + async $openUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise { + const uri = await this.resolveExternalUri(URI.from(uriComponents), options); + return this.openerService.open(uri, { openExternal: true }); + } + + async $resolveExternalUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise { + const uri = URI.revive(uriComponents); + return this.resolveExternalUri(uri, options); + } + + async $releaseResolvedExternalUri(uriComponents: UriComponents): Promise { + const portMappingRequest = extractLocalHostUriMetaDataForPortMapping(URI.from(uriComponents)); + if (portMappingRequest) { + const existing = this._tunnels.get(portMappingRequest.port); + if (existing) { + if (--existing.refcount <= 0) { + existing.value.then(tunnel => tunnel.dispose()); + this._tunnels.delete(portMappingRequest.port); + } + } + } + return true; + } + + private async resolveExternalUri(uri: URI, options: IOpenUriOptions): Promise { if (options.allowTunneling && !!this.environmentService.configuration.remoteAuthority) { const portMappingRequest = extractLocalHostUriMetaDataForPortMapping(uri); if (portMappingRequest) { - const tunnel = await this.getOrCreateTunnel(portMappingRequest.port); + const tunnel = await this.retainOrCreateTunnel(portMappingRequest.port); if (tunnel) { - uri = uri.with({ authority: `127.0.0.1:${tunnel.tunnelLocalPort}` }); + return uri.with({ authority: `127.0.0.1:${tunnel.tunnelLocalPort}` }); } } } - - return this.openerService.open(uri, { openExternal: true }); + return uri; } - private getOrCreateTunnel(remotePort: number): Promise | undefined { + private retainOrCreateTunnel(remotePort: number): Promise | undefined { const existing = this._tunnels.get(remotePort); if (existing) { - return existing; + ++existing.refcount; + return existing.value; } const tunnel = this.tunnelService.openTunnel(remotePort); if (tunnel) { - this._tunnels.set(remotePort, tunnel); + this._tunnels.set(remotePort, { refcount: 1, value: tunnel }); } return tunnel; } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 417b85c2883da..fda704b981019 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -248,6 +248,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I openExternal(uri: URI) { return extHostWindow.openUri(uri, { allowTunneling: !!initData.remote.isRemote }); }, + resolveExternalUri(uri: URI) { + checkProposedApiEnabled(extension); + return extHostWindow.resolveExternalUri(uri, { allowTunneling: !!initData.remote.isRemote }); + }, get remoteName() { return getRemoteName(initData.remote.authority); }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c62623e392972..d0ca5f56fd9fb 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -738,6 +738,8 @@ export interface IOpenUriOptions { export interface MainThreadWindowShape extends IDisposable { $getWindowVisibility(): Promise; $openUri(uri: UriComponents, options: IOpenUriOptions): Promise; + $resolveExternalUri(uri: UriComponents, options: IOpenUriOptions): Promise; + $releaseResolvedExternalUri(uri: UriComponents): Promise; } // -- extension host diff --git a/src/vs/workbench/api/common/extHostWindow.ts b/src/vs/workbench/api/common/extHostWindow.ts index b4e764ced09d4..4aeeba43a754e 100644 --- a/src/vs/workbench/api/common/extHostWindow.ts +++ b/src/vs/workbench/api/common/extHostWindow.ts @@ -9,6 +9,7 @@ import { WindowState } from 'vscode'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { once } from 'vs/base/common/functional'; export class ExtHostWindow implements ExtHostWindowShape { @@ -53,4 +54,20 @@ export class ExtHostWindow implements ExtHostWindowShape { } return this._proxy.$openUri(stringOrUri, options); } + + async resolveExternalUri(uri: URI, options: IOpenUriOptions): Promise<{ resolved: URI, dispose(): void }> { + if (isFalsyOrWhitespace(uri.scheme)) { + return Promise.reject('Invalid scheme - cannot be empty'); + } else if (!new Set([Schemas.http, Schemas.https]).has(uri.scheme)) { + return Promise.reject(`Invalid scheme '${uri.scheme}'`); + } + + const resolved = await this._proxy.$resolveExternalUri(uri, options); + return { + resolved: URI.from(resolved), + dispose: once(() => { + this._proxy.$releaseResolvedExternalUri(uri); + }), + }; + } }