Skip to content

Commit

Permalink
Basic implementation of resolveExternalUri
Browse files Browse the repository at this point in the history
  • Loading branch information
mjbvz committed Sep 19, 2019
1 parent 16577ca commit 27bac59
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 12 deletions.
23 changes: 23 additions & 0 deletions src/vs/vscode.proposed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
47 changes: 35 additions & 12 deletions src/vs/workbench/api/browser/mainThreadWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class MainThreadWindow implements MainThreadWindowShape {

private readonly proxy: ExtHostWindowShape;
private readonly disposables = new DisposableStore();
private readonly _tunnels = new Map<number, Promise<RemoteTunnel>>();
private readonly _tunnels = new Map<number, { refcount: number, readonly value: Promise<RemoteTunnel> }>();

constructor(
extHostContext: IExtHostContext,
Expand All @@ -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();
}
Expand All @@ -47,29 +47,52 @@ export class MainThreadWindow implements MainThreadWindowShape {
return this.windowService.isFocused();
}

async $openUri(uriComponent: UriComponents, options: IOpenUriOptions): Promise<boolean> {
let uri = URI.revive(uriComponent);
async $openUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise<boolean> {
const uri = await this.resolveExternalUri(URI.from(uriComponents), options);
return this.openerService.open(uri, { openExternal: true });
}

async $resolveExternalUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise<UriComponents> {
const uri = URI.revive(uriComponents);
return this.resolveExternalUri(uri, options);
}

async $releaseResolvedExternalUri(uriComponents: UriComponents): Promise<boolean> {
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<URI> {
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<RemoteTunnel> | undefined {
private retainOrCreateTunnel(remotePort: number): Promise<RemoteTunnel> | 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;
}
Expand Down
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 @@ -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);
},
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,8 @@ export interface IOpenUriOptions {
export interface MainThreadWindowShape extends IDisposable {
$getWindowVisibility(): Promise<boolean>;
$openUri(uri: UriComponents, options: IOpenUriOptions): Promise<boolean>;
$resolveExternalUri(uri: UriComponents, options: IOpenUriOptions): Promise<UriComponents>;
$releaseResolvedExternalUri(uri: UriComponents): Promise<boolean>;
}

// -- extension host
Expand Down
17 changes: 17 additions & 0 deletions src/vs/workbench/api/common/extHostWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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);
}),
};
}
}

0 comments on commit 27bac59

Please sign in to comment.