From 978373f14b9d54382c6e02b832521dfeba46d27a Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 9 Jan 2020 09:29:09 +0100 Subject: [PATCH] TunnelFactory web api (#88200) Web API for tunnels Part of #81388 --- .../platform/remote/common/tunnelService.ts | 26 --- .../remote/common/remote.contribution.ts | 2 + .../contrib/remote/common/tunnelFactory.ts | 39 +++++ .../services/remote/common/tunnelService.ts | 151 ++++++++++++++++++ .../services/remote/node/tunnelService.ts | 125 ++------------- src/vs/workbench/workbench.web.api.ts | 25 +++ src/vs/workbench/workbench.web.main.ts | 4 +- 7 files changed, 228 insertions(+), 144 deletions(-) delete mode 100644 src/vs/platform/remote/common/tunnelService.ts create mode 100644 src/vs/workbench/contrib/remote/common/tunnelFactory.ts create mode 100644 src/vs/workbench/services/remote/common/tunnelService.ts diff --git a/src/vs/platform/remote/common/tunnelService.ts b/src/vs/platform/remote/common/tunnelService.ts deleted file mode 100644 index a5fa07ac174ee..0000000000000 --- a/src/vs/platform/remote/common/tunnelService.ts +++ /dev/null @@ -1,26 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -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; - - public readonly tunnels: Promise = Promise.resolve([]); - private _onTunnelOpened: Emitter = new Emitter(); - public onTunnelOpened: Event = this._onTunnelOpened.event; - private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter(); - public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event; - openTunnel(_remoteHost: string, _remotePort: number): Promise | undefined { - return undefined; - } - async closeTunnel(_remoteHost: string, _remotePort: number): Promise { - } - setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { - throw new Error('Method not implemented.'); - } -} diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index bd64362867000..e5bdd22c73bbc 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -16,6 +16,7 @@ import { IOutputChannelRegistry, Extensions as OutputExt, } from 'vs/workbench/s import { localize } from 'vs/nls'; import { joinPath } from 'vs/base/common/resources'; import { Disposable } from 'vs/base/common/lifecycle'; +import { TunnelFactoryContribution } from 'vs/workbench/contrib/remote/common/tunnelFactory'; export const VIEWLET_ID = 'workbench.view.remote'; @@ -83,3 +84,4 @@ const workbenchContributionsRegistry = Registry.as | undefined => { + const tunnelPromise = workbenchEnvironmentService.options!.tunnelFactory!(tunnelOptions); + if (!tunnelPromise) { + return undefined; + } + return new Promise(resolve => { + tunnelPromise.then(tunnel => { + const remoteTunnel: RemoteTunnel = { + tunnelRemotePort: tunnel.remoteAddress.port, + tunnelRemoteHost: tunnel.remoteAddress.host, + localAddress: tunnel.localAddress, + dispose: tunnel.dispose + }; + resolve(remoteTunnel); + }); + }); + } + })); + } + } +} diff --git a/src/vs/workbench/services/remote/common/tunnelService.ts b/src/vs/workbench/services/remote/common/tunnelService.ts new file mode 100644 index 0000000000000..967299787d85d --- /dev/null +++ b/src/vs/workbench/services/remote/common/tunnelService.ts @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +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'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ILogService } from 'vs/platform/log/common/log'; + +export abstract class AbstractTunnelService implements ITunnelService { + _serviceBrand: undefined; + + private _onTunnelOpened: Emitter = new Emitter(); + public onTunnelOpened: Event = this._onTunnelOpened.event; + private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter(); + public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event; + protected readonly _tunnels = new Map }>>(); + protected _tunnelProvider: ITunnelProvider | undefined; + + public constructor( + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ILogService protected readonly logService: ILogService + ) { } + + setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { + if (!provider) { + return { + dispose: () => { } + }; + } + this._tunnelProvider = provider; + return { + dispose: () => { + this._tunnelProvider = undefined; + } + }; + } + + public get tunnels(): Promise { + const promises: Promise[] = []; + Array.from(this._tunnels.values()).forEach(portMap => Array.from(portMap.values()).forEach(x => promises.push(x.value))); + return Promise.all(promises); + } + + dispose(): void { + for (const portMap of this._tunnels.values()) { + for (const { value } of portMap.values()) { + value.then(tunnel => tunnel.dispose()); + } + portMap.clear(); + } + this._tunnels.clear(); + } + + openTunnel(remoteHost: string | undefined, remotePort: number, localPort: number): Promise | undefined { + const remoteAuthority = this.environmentService.configuration.remoteAuthority; + if (!remoteAuthority) { + return undefined; + } + + if (!remoteHost || (remoteHost === '127.0.0.1')) { + remoteHost = 'localhost'; + } + + const resolvedTunnel = this.retainOrCreateTunnel(remoteAuthority, remoteHost, remotePort, localPort); + if (!resolvedTunnel) { + return resolvedTunnel; + } + + return resolvedTunnel.then(tunnel => { + const newTunnel = this.makeTunnel(tunnel); + if (tunnel.tunnelRemoteHost !== remoteHost || tunnel.tunnelRemotePort !== remotePort) { + this.logService.warn('Created tunnel does not match requirements of requested tunnel. Host or port mismatch.'); + } + this._onTunnelOpened.fire(newTunnel); + return newTunnel; + }); + } + + private makeTunnel(tunnel: RemoteTunnel): RemoteTunnel { + return { + tunnelRemotePort: tunnel.tunnelRemotePort, + tunnelRemoteHost: tunnel.tunnelRemoteHost, + tunnelLocalPort: tunnel.tunnelLocalPort, + localAddress: tunnel.localAddress, + dispose: () => { + const existingHost = this._tunnels.get(tunnel.tunnelRemoteHost); + if (existingHost) { + const existing = existingHost.get(tunnel.tunnelRemotePort); + if (existing) { + existing.refcount--; + this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing); + } + } + } + }; + } + + private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise }): Promise { + if (tunnel.refcount <= 0) { + const disposePromise: Promise = tunnel.value.then(tunnel => { + tunnel.dispose(); + this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); + }); + if (this._tunnels.has(remoteHost)) { + this._tunnels.get(remoteHost)!.delete(remotePort); + } + return disposePromise; + } + } + + async closeTunnel(remoteHost: string, remotePort: number): Promise { + const portMap = this._tunnels.get(remoteHost); + if (portMap && portMap.has(remotePort)) { + const value = portMap.get(remotePort)!; + value.refcount = 0; + await this.tryDisposeTunnel(remoteHost, remotePort, value); + } + } + + protected addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise) { + if (!this._tunnels.has(remoteHost)) { + this._tunnels.set(remoteHost, new Map()); + } + this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel }); + } + + protected abstract retainOrCreateTunnel(remoteAuthority: string, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined; +} + +export class TunnelService extends AbstractTunnelService { + protected retainOrCreateTunnel(remoteAuthority: string, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise | undefined { + const portMap = this._tunnels.get(remoteHost); + const existing = portMap ? portMap.get(remotePort) : undefined; + if (existing) { + ++existing.refcount; + return existing.value; + } + + if (this._tunnelProvider) { + const tunnel = this._tunnelProvider.forwardPort({ remoteAddress: { host: remoteHost, port: remotePort } }); + if (tunnel) { + this.addTunnelToMap(remoteHost, remotePort, tunnel); + } + return tunnel; + } + return undefined; + } +} diff --git a/src/vs/workbench/services/remote/node/tunnelService.ts b/src/vs/workbench/services/remote/node/tunnelService.ts index 2c9fd2942514f..194f4edfe586b 100644 --- a/src/vs/workbench/services/remote/node/tunnelService.ts +++ b/src/vs/workbench/services/remote/node/tunnelService.ts @@ -5,19 +5,19 @@ import * as net from 'net'; import { Barrier } from 'vs/base/common/async'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable } 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, ITunnelProvider } from 'vs/platform/remote/common/tunnel'; +import { ITunnelService, RemoteTunnel } 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'; import { ILogService } from 'vs/platform/log/common/log'; import { findFreePort } from 'vs/base/node/ports'; -import { Event, Emitter } from 'vs/base/common/event'; +import { AbstractTunnelService } from 'vs/workbench/services/remote/common/tunnelService'; export async function createRemoteTunnel(options: IConnectionOptions, tunnelRemoteHost: string, tunnelRemotePort: number, tunnelLocalPort?: number): Promise { const tunnel = new NodeRemoteTunnel(options, tunnelRemoteHost, tunnelRemotePort, tunnelLocalPort); @@ -98,124 +98,17 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { } } -export class TunnelService implements ITunnelService { - _serviceBrand: undefined; - - private _onTunnelOpened: Emitter = new Emitter(); - public onTunnelOpened: Event = this._onTunnelOpened.event; - private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter(); - public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event; - private readonly _tunnels = new Map }>>(); - private _tunnelProvider: ITunnelProvider | undefined; - +export class TunnelService extends AbstractTunnelService { public constructor( - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @ILogService logService: ILogService, @IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService, @ISignService private readonly signService: ISignService, - @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 { - const promises: Promise[] = []; - Array.from(this._tunnels.values()).forEach(portMap => Array.from(portMap.values()).forEach(x => promises.push(x.value))); - return Promise.all(promises); - } - - dispose(): void { - for (const portMap of this._tunnels.values()) { - for (const { value } of portMap.values()) { - value.then(tunnel => tunnel.dispose()); - } - portMap.clear(); - } - this._tunnels.clear(); - } - - openTunnel(remoteHost: string | undefined, remotePort: number, localPort: number): Promise | undefined { - const remoteAuthority = this.environmentService.configuration.remoteAuthority; - if (!remoteAuthority) { - return undefined; - } - - if (!remoteHost || (remoteHost === '127.0.0.1')) { - remoteHost = 'localhost'; - } - - const resolvedTunnel = this.retainOrCreateTunnel(remoteAuthority, remoteHost, remotePort, localPort); - if (!resolvedTunnel) { - return resolvedTunnel; - } - - return resolvedTunnel.then(tunnel => { - const newTunnel = this.makeTunnel(tunnel); - this._onTunnelOpened.fire(newTunnel); - return newTunnel; - }); - } - - private makeTunnel(tunnel: RemoteTunnel): RemoteTunnel { - return { - tunnelRemotePort: tunnel.tunnelRemotePort, - tunnelRemoteHost: tunnel.tunnelRemoteHost, - tunnelLocalPort: tunnel.tunnelLocalPort, - localAddress: tunnel.localAddress, - dispose: () => { - const existingHost = this._tunnels.get(tunnel.tunnelRemoteHost); - if (existingHost) { - const existing = existingHost.get(tunnel.tunnelRemotePort); - if (existing) { - existing.refcount--; - this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing); - } - } - } - }; - } - - private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise }): Promise { - if (tunnel.refcount <= 0) { - const disposePromise: Promise = tunnel.value.then(tunnel => { - tunnel.dispose(); - this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); - }); - if (this._tunnels.has(remoteHost)) { - this._tunnels.get(remoteHost)!.delete(remotePort); - } - return disposePromise; - } - } - - async closeTunnel(remoteHost: string, remotePort: number): Promise { - const portMap = this._tunnels.get(remoteHost); - if (portMap && portMap.has(remotePort)) { - const value = portMap.get(remotePort)!; - value.refcount = 0; - await this.tryDisposeTunnel(remoteHost, remotePort, value); - } - } - - private addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise) { - if (!this._tunnels.has(remoteHost)) { - this._tunnels.set(remoteHost, new Map()); - } - this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel }); + ) { + super(environmentService, logService); } - private retainOrCreateTunnel(remoteAuthority: string, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined { + protected retainOrCreateTunnel(remoteAuthority: string, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined { const portMap = this._tunnels.get(remoteHost); const existing = portMap ? portMap.get(remotePort) : undefined; if (existing) { diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 5e2ea4fd604b9..d33ec02d21b79 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -34,6 +34,26 @@ interface IExternalUriResolver { (uri: URI): Promise; } +interface TunnelOptions { + remoteAddress: { port: number, host: string }; + // The desired local port. If this port can't be used, then another will be chosen. + localAddressPort?: number; + label?: string; +} + +interface Tunnel { + remoteAddress: { port: number, host: string }; + //The complete local address(ex. localhost:1234) + localAddress: string; + // Implementers of Tunnel should fire onDidDispose when dispose is called. + onDidDispose: Event; + dispose(): void; +} + +interface ITunnelFactory { + (tunnelOptions: TunnelOptions): Thenable | undefined; +} + interface IWorkbenchConstructionOptions { /** @@ -104,6 +124,11 @@ interface IWorkbenchConstructionOptions { */ readonly resolveExternalUri?: IExternalUriResolver; + /** + * Support for creating tunnels. + */ + readonly tunnelFactory?: ITunnelFactory; + /** * Current logging level. Default is `LogLevel.Info`. */ diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 65b74060fa184..ecd9ff04e173e 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -60,7 +60,7 @@ import { BackupFileService } from 'vs/workbench/services/backup/common/backupFil import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagementService'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; -import { NoOpTunnelService } from 'vs/platform/remote/common/tunnelService'; +import { TunnelService } from 'vs/workbench/services/remote/common/tunnelService'; import { ILoggerService } from 'vs/platform/log/common/log'; import { FileLoggerService } from 'vs/platform/log/common/fileLogService'; import { IAuthTokenService } from 'vs/platform/auth/common/auth'; @@ -75,7 +75,7 @@ registerSingleton(IExtensionManagementService, ExtensionManagementService); registerSingleton(IBackupFileService, BackupFileService); registerSingleton(IAccessibilityService, BrowserAccessibilityService, true); registerSingleton(IContextMenuService, ContextMenuService); -registerSingleton(ITunnelService, NoOpTunnelService, true); +registerSingleton(ITunnelService, TunnelService, true); registerSingleton(ILoggerService, FileLoggerService); registerSingleton(IAuthTokenService, AuthTokenService); registerSingleton(IUserDataSyncLogService, UserDataSyncLogService);