diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts index ea05c94aaaf8b27c42ce48e5b2676f445c290bdc..7693ad596c6001dd4e42c47af5053d45148ad387 100644 --- a/src/vs/platform/remote/common/tunnel.ts +++ b/src/vs/platform/remote/common/tunnel.ts @@ -6,17 +6,28 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; export const ITunnelService = createDecorator('tunnelService'); export interface RemoteTunnel { readonly tunnelRemotePort: number; readonly tunnelRemoteHost: string; - readonly tunnelLocalPort: number; + readonly tunnelLocalPort?: number; readonly localAddress: string; dispose(): void; } +export interface TunnelOptions { + remote: { port: number, host: string }; + localPort?: number; + name?: string; +} + +export interface ITunnelProvider { + forwardPort(tunnelOptions: TunnelOptions): Promise | undefined; +} + export interface ITunnelService { _serviceBrand: undefined; @@ -26,6 +37,7 @@ export interface ITunnelService { openTunnel(remotePort: number, localPort?: number): Promise | undefined; closeTunnel(remotePort: number): Promise; + setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable; } export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string, port: number } | undefined { diff --git a/src/vs/platform/remote/common/tunnelService.ts b/src/vs/platform/remote/common/tunnelService.ts index 44017cb2a0fbbe0a28ccaccc6d68b37d60b0c28c..2501ebc90f1022dc895872236655ccaabad5b704 100644 --- a/src/vs/platform/remote/common/tunnelService.ts +++ b/src/vs/platform/remote/common/tunnelService.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { ITunnelService, RemoteTunnel, ITunnelProvider } from 'vs/platform/remote/common/tunnel'; import { Event, Emitter } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; export class NoOpTunnelService implements ITunnelService { _serviceBrand: undefined; @@ -19,4 +20,7 @@ export class NoOpTunnelService implements ITunnelService { } async closeTunnel(_remotePort: number): Promise { } + setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { + throw new Error('Method not implemented.'); + } } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 8ef405a7e4d9ade8cc1e28a20563086ca6bc4155..975f11aebd9da29d6bde2ac5e0e0117d64af0b50 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -39,9 +39,11 @@ declare module 'vscode' { name?: string; } - export interface Tunnel extends Disposable { + export interface Tunnel { remote: { port: number, host: string }; localAddress: string; + onDispose: Event; + dispose(): void; } /** @@ -72,7 +74,7 @@ declare module 'vscode' { * When not implemented, the core will use its default forwarding logic. * When implemented, the core will use this to forward ports. */ - forwardPort?(tunnelOptions: TunnelOptions): Thenable; + forwardPort?(tunnelOptions: TunnelOptions): Thenable | undefined; } export namespace workspace { diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index fd4118f53247b0cab4ae13da94314fd994848060..482fe0f88083640ab5897490861fb4504087910e 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -7,6 +7,7 @@ import { MainThreadTunnelServiceShape, IExtHostContext, MainContext, ExtHostCont import { TunnelOptions, TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { ITunnelProvider, ITunnelService } from 'vs/platform/remote/common/tunnel'; @extHostNamedCustomer(MainContext.MainThreadTunnelService) export class MainThreadTunnelService implements MainThreadTunnelServiceShape { @@ -14,7 +15,8 @@ export class MainThreadTunnelService implements MainThreadTunnelServiceShape { constructor( extHostContext: IExtHostContext, - @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService + @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, + @ITunnelService private readonly tunnelService: ITunnelService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTunnelService); } @@ -22,7 +24,7 @@ export class MainThreadTunnelService implements MainThreadTunnelServiceShape { async $openTunnel(tunnelOptions: TunnelOptions): Promise { const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remote.port, tunnelOptions.localPort, tunnelOptions.name); if (tunnel) { - return { remote: { host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }, localAddress: tunnel.localAddress }; + return TunnelDto.fromServiceTunnel(tunnel); } return undefined; } @@ -35,6 +37,28 @@ export class MainThreadTunnelService implements MainThreadTunnelServiceShape { this.remoteExplorerService.registerCandidateFinder(() => this._proxy.$findCandidatePorts()); } + async $setTunnelProvider(): Promise { + const tunnelProvider: ITunnelProvider = { + forwardPort: (tunnelOptions: TunnelOptions) => { + const forward = this._proxy.$forwardPort(tunnelOptions); + if (forward) { + return forward.then(tunnel => { + return { + tunnelRemotePort: tunnel.remote.port, + tunnelRemoteHost: tunnel.remote.host, + localAddress: tunnel.localAddress, + dispose: () => { + this._proxy.$closeTunnel({ host: tunnel.remote.host, port: tunnel.remote.port }); + } + }; + }); + } + return undefined; + } + }; + this.tunnelService.setTunnelProvider(tunnelProvider); + } + dispose(): void { // } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index b79e49ac355ad6b6ceedf4b63bb8a22065f99fb8..892bcaeb76347f2dcf26f28a6d4cc877a59751d3 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -777,6 +777,7 @@ export interface MainThreadTunnelServiceShape extends IDisposable { $openTunnel(tunnelOptions: TunnelOptions): Promise; $closeTunnel(remotePort: number): Promise; $registerCandidateFinder(): Promise; + $setTunnelProvider(): Promise; } // -- extension host @@ -1396,6 +1397,8 @@ export interface ExtHostStorageShape { export interface ExtHostTunnelServiceShape { $findCandidatePorts(): Promise<{ port: number, detail: string }[]>; + $forwardPort(tunnelOptions: TunnelOptions): Promise | undefined; + $closeTunnel(remote: { host: string, port: number }): Promise; } // --- proxy identifiers diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 2afac9ab65280ced14b09a8cbaa8d53ebc69429b..caa8fee5d815ee075a5726cd8ddedf1f574790bf 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -32,6 +32,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; interface ITestRunner { /** Old test runner API, as exported from `vscode/lib/testrunner` */ @@ -76,6 +77,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio protected readonly _extHostWorkspace: ExtHostWorkspace; protected readonly _extHostConfiguration: ExtHostConfiguration; protected readonly _logService: ILogService; + protected readonly _extHostTunnelService: IExtHostTunnelService; protected readonly _mainThreadWorkspaceProxy: MainThreadWorkspaceShape; protected readonly _mainThreadTelemetryProxy: MainThreadTelemetryShape; @@ -104,7 +106,8 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio @IExtHostConfiguration extHostConfiguration: IExtHostConfiguration, @ILogService logService: ILogService, @IExtHostInitDataService initData: IExtHostInitDataService, - @IExtensionStoragePaths storagePath: IExtensionStoragePaths + @IExtensionStoragePaths storagePath: IExtensionStoragePaths, + @IExtHostTunnelService extHostTunnelService: IExtHostTunnelService ) { this._hostUtils = hostUtils; this._extHostContext = extHostContext; @@ -113,6 +116,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio this._extHostWorkspace = extHostWorkspace; this._extHostConfiguration = extHostConfiguration; this._logService = logService; + this._extHostTunnelService = extHostTunnelService; this._disposables = new DisposableStore(); this._mainThreadWorkspaceProxy = this._extHostContext.getProxy(MainContext.MainThreadWorkspace); @@ -641,6 +645,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio try { const result = await resolver.resolve(remoteAuthority, { resolveAttempt }); + this._disposables.add(await this._extHostTunnelService.setForwardPortProvider(resolver)); // Split merged API result into separate authority/options const authority: ResolvedAuthority = { diff --git a/src/vs/workbench/api/common/extHostTunnelService.ts b/src/vs/workbench/api/common/extHostTunnelService.ts index bc5d17b76e07f3c7ee1ab82db7d43ddb984b42d5..40c37a6c00c02fc6c3adb6122c3351a28a16f052 100644 --- a/src/vs/workbench/api/common/extHostTunnelService.ts +++ b/src/vs/workbench/api/common/extHostTunnelService.ts @@ -6,6 +6,8 @@ import { ExtHostTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import * as vscode from 'vscode'; +import { RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { IDisposable } from 'vs/base/common/lifecycle'; export interface TunnelOptions { remote: { port: number, host: string }; @@ -19,9 +21,24 @@ export interface TunnelDto { localAddress: string; } +export namespace TunnelDto { + export function fromApiTunnel(tunnel: vscode.Tunnel): TunnelDto { + return { remote: tunnel.remote, localAddress: tunnel.localAddress }; + } + export function fromServiceTunnel(tunnel: RemoteTunnel): TunnelDto { + return { remote: { host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }, localAddress: tunnel.localAddress }; + } +} + +export interface Tunnel extends vscode.Disposable { + remote: { port: number, host: string }; + localAddress: string; +} + export interface IExtHostTunnelService extends ExtHostTunnelServiceShape { readonly _serviceBrand: undefined; makeTunnel(forward: TunnelOptions): Promise; + setForwardPortProvider(provider: vscode.RemoteAuthorityResolver | undefined): Promise; } export const IExtHostTunnelService = createDecorator('IExtHostTunnelService'); @@ -34,4 +51,8 @@ export class ExtHostTunnelService implements IExtHostTunnelService { async $findCandidatePorts(): Promise<{ port: number; detail: string; }[]> { return []; } + async setForwardPortProvider(provider: vscode.RemoteAuthorityResolver | undefined): Promise { return { dispose: () => { } }; } + $forwardPort(tunnelOptions: TunnelOptions): Promise | undefined { return undefined; } + async $closeTunnel(remote: { host: string, port: number }): Promise { } + } diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index 73ac33d5823df22c86d38440d122e5ea6dacdde7..9dbde0ddabb2a43f76ce005c67d42ed1729ec7b7 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -6,18 +6,37 @@ import { MainThreadTunnelServiceShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import * as vscode from 'vscode'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { URI } from 'vs/base/common/uri'; import { exec } from 'child_process'; import * as resources from 'vs/base/common/resources'; import * as fs from 'fs'; import { isLinux } from 'vs/base/common/platform'; -import { IExtHostTunnelService, TunnelOptions } from 'vs/workbench/api/common/extHostTunnelService'; +import { IExtHostTunnelService, TunnelOptions, TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; +import { asPromise } from 'vs/base/common/async'; +import { Event, Emitter } from 'vs/base/common/event'; + +class ExtensionTunnel implements vscode.Tunnel { + private _onDispose: Emitter = new Emitter(); + onDispose: Event = this._onDispose.event; + + constructor( + public readonly remote: { port: number; host: string; }, + public readonly localAddress: string, + private readonly _dispose: () => void) { } + + dispose(): void { + this._onDispose.fire(); + this._dispose(); + } +} export class ExtHostTunnelService extends Disposable implements IExtHostTunnelService { readonly _serviceBrand: undefined; private readonly _proxy: MainThreadTunnelServiceShape; + private _forwardPortProvider: ((tunnelOptions: TunnelOptions) => Thenable | undefined) | undefined; + private _extensionTunnels: Map> = new Map(); constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @@ -32,13 +51,9 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe async makeTunnel(forward: TunnelOptions): Promise { const tunnel = await this._proxy.$openTunnel(forward); if (tunnel) { - const disposableTunnel: vscode.Tunnel = { - remote: tunnel.remote, - localAddress: tunnel.localAddress, - dispose: () => { - return this._proxy.$closeTunnel(tunnel.remote.port); - } - }; + const disposableTunnel: vscode.Tunnel = new ExtensionTunnel(tunnel.remote, tunnel.localAddress, () => { + return this._proxy.$closeTunnel(tunnel.remote.port); + }); this._register(disposableTunnel); return disposableTunnel; } @@ -49,6 +64,46 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe return this._proxy.$registerCandidateFinder(); } + async setForwardPortProvider(provider: vscode.RemoteAuthorityResolver | undefined): Promise { + if (provider && provider.forwardPort) { + this._forwardPortProvider = provider.forwardPort; + await this._proxy.$setTunnelProvider(); + } else { + this._forwardPortProvider = undefined; + } + return toDisposable(() => { + this._forwardPortProvider = undefined; + }); + } + + async $closeTunnel(remote: { host: string, port: number }): Promise { + if (this._extensionTunnels.has(remote.host)) { + const hostMap = this._extensionTunnels.get(remote.host)!; + if (hostMap.has(remote.port)) { + hostMap.get(remote.port)!.dispose(); + hostMap.delete(remote.port); + } + } + } + + $forwardPort(tunnelOptions: TunnelOptions): Promise | undefined { + if (this._forwardPortProvider) { + const providedPort = this._forwardPortProvider!(tunnelOptions); + if (providedPort !== undefined) { + return asPromise(() => providedPort).then(tunnel => { + if (!this._extensionTunnels.has(tunnelOptions.remote.host)) { + this._extensionTunnels.set(tunnelOptions.remote.host, new Map()); + } + this._extensionTunnels.get(tunnelOptions.remote.host)!.set(tunnelOptions.remote.port, tunnel); + this._register(tunnel.onDispose(() => this._proxy.$closeTunnel(tunnel.remote.port))); + return Promise.resolve(TunnelDto.fromApiTunnel(tunnel)); + }); + } + } + return undefined; + } + + async $findCandidatePorts(): Promise<{ port: number, detail: string }[]> { if (!isLinux) { return []; diff --git a/src/vs/workbench/services/remote/node/tunnelService.ts b/src/vs/workbench/services/remote/node/tunnelService.ts index 74f93537d480fdef3b60aa0b9dbd801285198ad2..4e60ad01e15fd3698e30ef07c41e8e83f1151230 100644 --- a/src/vs/workbench/services/remote/node/tunnelService.ts +++ b/src/vs/workbench/services/remote/node/tunnelService.ts @@ -5,13 +5,13 @@ import * as net from 'net'; import { Barrier } from 'vs/base/common/async'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import product from 'vs/platform/product/common/product'; import { connectRemoteAgentTunnel, IConnectionOptions } from 'vs/platform/remote/common/remoteAgentConnection'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; -import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { ITunnelService, RemoteTunnel, ITunnelProvider } from 'vs/platform/remote/common/tunnel'; import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; import { ISignService } from 'vs/platform/sign/common/sign'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -106,6 +106,7 @@ export class TunnelService implements ITunnelService { private _onTunnelClosed: Emitter = new Emitter(); public onTunnelClosed: Event = this._onTunnelClosed.event; private readonly _tunnels = new Map }>(); + private _tunnelProvider: ITunnelProvider | undefined; public constructor( @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @@ -114,6 +115,20 @@ export class TunnelService implements ITunnelService { @ILogService private readonly logService: ILogService, ) { } + setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { + if (!provider) { + return { + dispose: () => { } + }; + } + this._tunnelProvider = provider; + return { + dispose: () => { + this._tunnelProvider = undefined; + } + }; + } + public get tunnels(): Promise { return Promise.all(Array.from(this._tunnels.values()).map(x => x.value)); } @@ -185,22 +200,30 @@ export class TunnelService implements ITunnelService { return existing.value; } - const options: IConnectionOptions = { - commit: product.commit, - socketFactory: nodeSocketFactory, - addressProvider: { - getAddress: async () => { - const { authority } = await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority); - return { host: authority.host, port: authority.port }; - } - }, - signService: this.signService, - logService: this.logService - }; - - const tunnel = createRemoteTunnel(options, remotePort, localPort); - this._tunnels.set(remotePort, { refcount: 1, value: tunnel }); - return tunnel; + if (this._tunnelProvider) { + const tunnel = this._tunnelProvider.forwardPort({ remote: { host: 'localhost', port: remotePort } }); + if (tunnel) { + this._tunnels.set(remotePort, { refcount: 1, value: tunnel }); + } + return tunnel; + } else { + const options: IConnectionOptions = { + commit: product.commit, + socketFactory: nodeSocketFactory, + addressProvider: { + getAddress: async () => { + const { authority } = await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority); + return { host: authority.host, port: authority.port }; + } + }, + signService: this.signService, + logService: this.logService + }; + + const tunnel = createRemoteTunnel(options, remotePort, localPort); + this._tunnels.set(remotePort, { refcount: 1, value: tunnel }); + return tunnel; + } } }