diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts index 7693ad596c6..b5fdfa8bff0 100644 --- a/src/vs/platform/remote/common/tunnel.ts +++ b/src/vs/platform/remote/common/tunnel.ts @@ -33,10 +33,10 @@ export interface ITunnelService { readonly tunnels: Promise; readonly onTunnelOpened: Event; - readonly onTunnelClosed: Event; + readonly onTunnelClosed: Event<{ host: string, port: number }>; - openTunnel(remotePort: number, localPort?: number): Promise | undefined; - closeTunnel(remotePort: number): Promise; + openTunnel(remoteHost: string | undefined, remotePort: number, localPort?: number): Promise | undefined; + closeTunnel(remoteHost: string, remotePort: number): Promise; setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable; } diff --git a/src/vs/platform/remote/common/tunnelService.ts b/src/vs/platform/remote/common/tunnelService.ts index 2501ebc90f1..a5fa07ac174 100644 --- a/src/vs/platform/remote/common/tunnelService.ts +++ b/src/vs/platform/remote/common/tunnelService.ts @@ -13,12 +13,12 @@ export class NoOpTunnelService implements ITunnelService { public readonly tunnels: Promise = Promise.resolve([]); private _onTunnelOpened: Emitter = new Emitter(); public onTunnelOpened: Event = this._onTunnelOpened.event; - private _onTunnelClosed: Emitter = new Emitter(); - public onTunnelClosed: Event = this._onTunnelClosed.event; - openTunnel(_remotePort: number): Promise | undefined { + 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(_remotePort: number): Promise { + async closeTunnel(_remoteHost: string, _remotePort: number): Promise { } setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { throw new Error('Method not implemented.'); diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index 482fe0f8808..3ab437aeef4 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -22,15 +22,15 @@ export class MainThreadTunnelService implements MainThreadTunnelServiceShape { } async $openTunnel(tunnelOptions: TunnelOptions): Promise { - const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remote.port, tunnelOptions.localPort, tunnelOptions.name); + const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remote, tunnelOptions.localPort, tunnelOptions.name); if (tunnel) { return TunnelDto.fromServiceTunnel(tunnel); } return undefined; } - async $closeTunnel(remotePort: number): Promise { - return this.remoteExplorerService.close(remotePort); + async $closeTunnel(remote: { host: string, port: number }): Promise { + return this.remoteExplorerService.close(remote); } async $registerCandidateFinder(): Promise { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 2e879ed1eda..b4c6ebbb625 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -774,7 +774,7 @@ export interface MainThreadWindowShape extends IDisposable { export interface MainThreadTunnelServiceShape extends IDisposable { $openTunnel(tunnelOptions: TunnelOptions): Promise; - $closeTunnel(remotePort: number): Promise; + $closeTunnel(remote: { host: string, port: number }): Promise; $registerCandidateFinder(): Promise; $setTunnelProvider(): Promise; } @@ -1395,7 +1395,7 @@ export interface ExtHostStorageShape { export interface ExtHostTunnelServiceShape { - $findCandidatePorts(): Promise<{ port: number, detail: string }[]>; + $findCandidatePorts(): Promise<{ host: string, port: number, detail: string }[]>; $forwardPort(tunnelOptions: TunnelOptions): Promise | undefined; $closeTunnel(remote: { host: string, port: number }): Promise; } diff --git a/src/vs/workbench/api/common/extHostTunnelService.ts b/src/vs/workbench/api/common/extHostTunnelService.ts index 40c37a6c00c..a606b3820f0 100644 --- a/src/vs/workbench/api/common/extHostTunnelService.ts +++ b/src/vs/workbench/api/common/extHostTunnelService.ts @@ -48,7 +48,7 @@ export class ExtHostTunnelService implements IExtHostTunnelService { async makeTunnel(forward: TunnelOptions): Promise { return undefined; } - async $findCandidatePorts(): Promise<{ port: number; detail: string; }[]> { + async $findCandidatePorts(): Promise<{ host: string, port: number; detail: string; }[]> { return []; } async setForwardPortProvider(provider: vscode.RemoteAuthorityResolver | undefined): Promise { return { dispose: () => { } }; } diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index 9dbde0ddabb..93b333980a5 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -52,7 +52,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe const tunnel = await this._proxy.$openTunnel(forward); if (tunnel) { const disposableTunnel: vscode.Tunnel = new ExtensionTunnel(tunnel.remote, tunnel.localAddress, () => { - return this._proxy.$closeTunnel(tunnel.remote.port); + return this._proxy.$closeTunnel(tunnel.remote); }); this._register(disposableTunnel); return disposableTunnel; @@ -95,7 +95,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe 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))); + this._register(tunnel.onDispose(() => this._proxy.$closeTunnel(tunnel.remote))); return Promise.resolve(TunnelDto.fromApiTunnel(tunnel)); }); } @@ -104,12 +104,12 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe } - async $findCandidatePorts(): Promise<{ port: number, detail: string }[]> { + async $findCandidatePorts(): Promise<{ host: string, port: number, detail: string }[]> { if (!isLinux) { return []; } - const ports: { port: number, detail: string }[] = []; + const ports: { host: string, port: number, detail: string }[] = []; const tcp: string = fs.readFileSync('/proc/net/tcp', 'utf8'); const tcp6: string = fs.readFileSync('/proc/net/tcp6', 'utf8'); const procSockets: string = await (new Promise(resolve => { @@ -150,7 +150,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => { const command = processMap[socketMap[socket].pid].cmd; if (!command.match('.*\.vscode\-server\-[a-zA-Z]+\/bin.*') && (command.indexOf('out/vs/server/main.js') === -1)) { - ports.push({ port, detail: processMap[socketMap[socket].pid].cmd }); + ports.push({ host: ip, port, detail: processMap[socketMap[socket].pid].cmd }); } }); diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 1e881877649..4f6ebb31e5e 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -26,7 +26,7 @@ import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { ActionRunner, IAction } from 'vs/base/common/actions'; import { IMenuService, MenuId, IMenu, MenuRegistry, MenuItemAction } from 'vs/platform/actions/common/actions'; import { createAndFillInContextMenuActions, createAndFillInActionBarActions, ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IRemoteExplorerService, TunnelModel } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { IRemoteExplorerService, TunnelModel, MakeAddress } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; @@ -105,13 +105,13 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { get forwarded(): TunnelItem[] { return Array.from(this.model.forwarded.values()).map(tunnel => { - return new TunnelItem(TunnelType.Forwarded, tunnel.remote, tunnel.localAddress, tunnel.closeable, tunnel.name, tunnel.description); + return new TunnelItem(TunnelType.Forwarded, tunnel.remoteHost, tunnel.remotePort, tunnel.localAddress, tunnel.closeable, tunnel.name, tunnel.description); }); } get detected(): TunnelItem[] { return Array.from(this.model.detected.values()).map(tunnel => { - return new TunnelItem(TunnelType.Detected, tunnel.remote, tunnel.localAddress, false, tunnel.name, tunnel.description); + return new TunnelItem(TunnelType.Detected, tunnel.remoteHost, tunnel.remotePort, tunnel.localAddress, false, tunnel.name, tunnel.description); }); } @@ -119,8 +119,9 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { return this.model.candidates.then(values => { const candidates: TunnelItem[] = []; values.forEach(value => { - if (!this.model.forwarded.has(value.port) && !this.model.detected.has(value.port)) { - candidates.push(new TunnelItem(TunnelType.Candidate, value.port, undefined, false, undefined, value.detail)); + const key = MakeAddress(value.host, value.port); + if (!this.model.forwarded.has(key) && !this.model.detected.has(key)) { + candidates.push(new TunnelItem(TunnelType.Candidate, value.host, value.port, undefined, false, undefined, value.detail)); } }); return candidates; @@ -185,7 +186,7 @@ class TunnelTreeRenderer extends Disposable implements ITreeRendereritem).remote); + return !!((item).remotePort); } renderElement(element: ITreeNode, index: number, templateData: ITunnelTemplateData): void { @@ -196,7 +197,7 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer { - const isEditing = !!this.remoteExplorerService.getEditableData(e); + const isEditing = !!this.remoteExplorerService.getEditableData(e.host, e.port); if (!isEditing) { dom.removeClass(treeContainer, 'highlight'); @@ -575,12 +578,12 @@ namespace LabelTunnelAction { return async (accessor, arg) => { if (arg instanceof TunnelItem) { const remoteExplorerService = accessor.get(IRemoteExplorerService); - remoteExplorerService.setEditable(arg.remote, { + remoteExplorerService.setEditable(arg.remoteHost, arg.remotePort, { onFinish: (value, success) => { if (success) { - remoteExplorerService.tunnelModel.name(arg.remote, value); + remoteExplorerService.tunnelModel.name(arg.remoteHost, arg.remotePort, value); } - remoteExplorerService.setEditable(arg.remote, null); + remoteExplorerService.setEditable(arg.remoteHost, arg.remotePort, null); }, validationMessage: () => null, placeholder: nls.localize('remote.tunnelsView.labelPlaceholder', "Port label"), @@ -596,24 +599,32 @@ namespace ForwardPortAction { export const ID = 'remote.tunnel.forward'; export const LABEL = nls.localize('remote.tunnel.forward', "Forward a Port"); + function parseInput(value: string): { host: string, port: number } | undefined { + const matches = value.match(/^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\:|localhost:)?([0-9]+)$/); + if (!matches) { + return undefined; + } + return { host: matches[1]?.substring(0, matches[1].length - 1) || 'localhost', port: Number(matches[2]) }; + } + export function handler(): ICommandHandler { return async (accessor, arg) => { const remoteExplorerService = accessor.get(IRemoteExplorerService); if (arg instanceof TunnelItem) { - remoteExplorerService.tunnelModel.forward(arg.remote); + remoteExplorerService.forward({ host: arg.remoteHost, port: arg.remotePort }); } else { const viewsService = accessor.get(IViewsService); await viewsService.openView(TunnelPanel.ID, true); - remoteExplorerService.setEditable(undefined, { + remoteExplorerService.setEditable(undefined, undefined, { onFinish: (value, success) => { - if (success) { - remoteExplorerService.tunnelModel.forward(Number(value)); + let parsed: { host: string, port: number } | undefined; + if (success && (parsed = parseInput(value))) { + remoteExplorerService.forward({ host: parsed.host, port: parsed.port }); } - remoteExplorerService.setEditable(undefined, null); + remoteExplorerService.setEditable(undefined, undefined, null); }, validationMessage: (value) => { - const asNumber = Number(value); - if ((value === '') || isNaN(asNumber) || (asNumber < 0) || (asNumber > 65535)) { + if (!parseInput(value)) { return nls.localize('remote.tunnelsView.portNumberValid', "Port number is invalid"); } return null; @@ -633,7 +644,7 @@ namespace ClosePortAction { return async (accessor, arg) => { if (arg instanceof TunnelItem) { const remoteExplorerService = accessor.get(IRemoteExplorerService); - await remoteExplorerService.tunnelModel.close(arg.remote); + await remoteExplorerService.close({ host: arg.remoteHost, port: arg.remotePort }); } }; } @@ -648,9 +659,10 @@ namespace OpenPortInBrowserAction { if (arg instanceof TunnelItem) { const model = accessor.get(IRemoteExplorerService).tunnelModel; const openerService = accessor.get(IOpenerService); - const tunnel = model.forwarded.has(arg.remote) ? model.forwarded.get(arg.remote) : model.detected.get(arg.remote); + const key = MakeAddress(arg.remoteHost, arg.remotePort); + const tunnel = model.forwarded.get(key) || model.detected.get(key); let address: string | undefined; - if (tunnel && tunnel.localAddress && (address = model.address(tunnel.remote))) { + if (tunnel && tunnel.localAddress && (address = model.address(tunnel.remoteHost, tunnel.remotePort))) { return openerService.open(URI.parse('http://' + address)); } return Promise.resolve(); @@ -668,7 +680,7 @@ namespace CopyAddressAction { if (arg instanceof TunnelItem) { const model = accessor.get(IRemoteExplorerService).tunnelModel; const clipboard = accessor.get(IClipboardService); - const address = model.address(arg.remote); + const address = model.address(arg.remoteHost, arg.remotePort); if (address) { await clipboard.writeText(address.toString()); } diff --git a/src/vs/workbench/contrib/webview/common/portMapping.ts b/src/vs/workbench/contrib/webview/common/portMapping.ts index 69c216631ba..fe5a6d21962 100644 --- a/src/vs/workbench/contrib/webview/common/portMapping.ts +++ b/src/vs/workbench/contrib/webview/common/portMapping.ts @@ -68,7 +68,7 @@ export class WebviewPortMappingManager extends Disposable { if (existing) { return existing; } - const tunnel = this.tunnelService.openTunnel(remotePort); + const tunnel = this.tunnelService.openTunnel(undefined, remotePort); if (tunnel) { this._tunnels.set(remotePort, tunnel); } diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 5e7ff1184b5..02735f4a472 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -453,7 +453,7 @@ export class ElectronWindow extends Disposable { if (options?.allowTunneling) { const portMappingRequest = extractLocalHostUriMetaDataForPortMapping(uri); if (portMappingRequest) { - const tunnel = await this.tunnelService.openTunnel(portMappingRequest.port); + const tunnel = await this.tunnelService.openTunnel(undefined, portMappingRequest.port); if (tunnel) { return { resolved: uri.with({ authority: `127.0.0.1:${tunnel.tunnelLocalPort}` }), diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 01f41b09a1c..f83efc87dc3 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -18,24 +18,32 @@ export const IRemoteExplorerService = createDecorator('r export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType'; export interface Tunnel { - remote: number; + remoteHost: string; + remotePort: number; localAddress: string; - local?: number; + localPort?: number; name?: string; description?: string; closeable?: boolean; } +export function MakeAddress(host: string, port: number): string { + if (host = '127.0.0.1') { + host = 'localhost'; + } + return host + ':' + port; +} + export class TunnelModel extends Disposable { - readonly forwarded: Map; - readonly detected: Map; + readonly forwarded: Map; + readonly detected: Map; private _onForwardPort: Emitter = new Emitter(); public onForwardPort: Event = this._onForwardPort.event; - private _onClosePort: Emitter = new Emitter(); - public onClosePort: Event = this._onClosePort.event; - private _onPortName: Emitter = new Emitter(); - public onPortName: Event = this._onPortName.event; - private _candidateFinder: (() => Promise<{ port: number, detail: string }[]>) | undefined; + private _onClosePort: Emitter<{ host: string, port: number }> = new Emitter(); + public onClosePort: Event<{ host: string, port: number }> = this._onClosePort.event; + private _onPortName: Emitter<{ host: string, port: number }> = new Emitter(); + public onPortName: Event<{ host: string, port: number }> = this._onPortName.event; + private _candidateFinder: (() => Promise<{ host: string, port: number, detail: string }[]>) | undefined; constructor( @ITunnelService private readonly tunnelService: ITunnelService @@ -45,10 +53,11 @@ export class TunnelModel extends Disposable { this.tunnelService.tunnels.then(tunnels => { tunnels.forEach(tunnel => { if (tunnel.localAddress) { - this.forwarded.set(tunnel.tunnelRemotePort, { - remote: tunnel.tunnelRemotePort, + this.forwarded.set(MakeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort), { + remotePort: tunnel.tunnelRemotePort, + remoteHost: tunnel.tunnelRemoteHost, localAddress: tunnel.localAddress, - local: tunnel.tunnelLocalPort + localPort: tunnel.tunnelLocalPort }); } }); @@ -56,75 +65,83 @@ export class TunnelModel extends Disposable { this.detected = new Map(); this._register(this.tunnelService.onTunnelOpened(tunnel => { - if (!this.forwarded.has(tunnel.tunnelRemotePort) && tunnel.localAddress) { - this.forwarded.set(tunnel.tunnelRemotePort, { - remote: tunnel.tunnelRemotePort, + const key = MakeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort); + if ((!this.forwarded.has(key)) && tunnel.localAddress) { + this.forwarded.set(key, { + remoteHost: tunnel.tunnelRemoteHost, + remotePort: tunnel.tunnelRemotePort, localAddress: tunnel.localAddress, - local: tunnel.tunnelLocalPort, + localPort: tunnel.tunnelLocalPort, closeable: true }); } - this._onForwardPort.fire(this.forwarded.get(tunnel.tunnelRemotePort)!); + this._onForwardPort.fire(this.forwarded.get(key)!); })); - this._register(this.tunnelService.onTunnelClosed(remotePort => { - if (this.forwarded.has(remotePort)) { - this.forwarded.delete(remotePort); - this._onClosePort.fire(remotePort); + this._register(this.tunnelService.onTunnelClosed(address => { + const key = MakeAddress(address.host, address.port); + if (this.forwarded.has(key)) { + this.forwarded.delete(key); + this._onClosePort.fire(address); } })); } - async forward(remote: number, local?: number, name?: string): Promise { - if (!this.forwarded.has(remote)) { - const tunnel = await this.tunnelService.openTunnel(remote, local); + async forward(remote: { host: string, port: number }, local?: number, name?: string): Promise { + const key = MakeAddress(remote.host, remote.port); + if (!this.forwarded.has(key)) { + const tunnel = await this.tunnelService.openTunnel(remote.host, remote.port, local); if (tunnel && tunnel.localAddress) { const newForward: Tunnel = { - remote: tunnel.tunnelRemotePort, - local: tunnel.tunnelLocalPort, + remoteHost: tunnel.tunnelRemoteHost, + remotePort: tunnel.tunnelRemotePort, + localPort: tunnel.tunnelLocalPort, name: name, closeable: true, localAddress: tunnel.localAddress }; - this.forwarded.set(remote, newForward); + this.forwarded.set(key, newForward); this._onForwardPort.fire(newForward); return tunnel; } } } - name(remote: number, name: string) { - if (this.forwarded.has(remote)) { - this.forwarded.get(remote)!.name = name; - this._onPortName.fire(remote); - } else if (this.detected.has(remote)) { - this.detected.get(remote)!.name = name; - this._onPortName.fire(remote); + name(host: string, port: number, name: string) { + const key = MakeAddress(host, port); + if (this.forwarded.has(key)) { + this.forwarded.get(key)!.name = name; + this._onPortName.fire({ host, port }); + } else if (this.detected.has(key)) { + this.detected.get(key)!.name = name; + this._onPortName.fire({ host, port }); } } - async close(remote: number): Promise { - return this.tunnelService.closeTunnel(remote); + async close(host: string, port: number): Promise { + return this.tunnelService.closeTunnel(host, port); } - address(remote: number): string | undefined { - return (this.forwarded.get(remote) || this.detected.get(remote))?.localAddress; + address(host: string, port: number): string | undefined { + const key = MakeAddress(host, port); + return (this.forwarded.get(key) || this.detected.get(key))?.localAddress; } addDetected(tunnels: { remote: { port: number, host: string }, localAddress: string }[]): void { tunnels.forEach(tunnel => { - this.detected.set(tunnel.remote.port, { - remote: tunnel.remote.port, + this.detected.set(MakeAddress(tunnel.remote.host, tunnel.remote.port), { + remoteHost: tunnel.remote.host, + remotePort: tunnel.remote.port, localAddress: tunnel.localAddress, closeable: false }); }); } - registerCandidateFinder(finder: () => Promise<{ port: number, detail: string }[]>): void { + registerCandidateFinder(finder: () => Promise<{ host: string, port: number, detail: string }[]>): void { this._candidateFinder = finder; } - get candidates(): Promise<{ port: number, detail: string }[]> { + get candidates(): Promise<{ host: string, port: number, detail: string }[]> { if (this._candidateFinder) { return this._candidateFinder(); } @@ -138,13 +155,13 @@ export interface IRemoteExplorerService { targetType: string; readonly helpInformation: HelpInformation[]; readonly tunnelModel: TunnelModel; - onDidChangeEditable: Event; - setEditable(remote: number | undefined, data: IEditableData | null): void; - getEditableData(remote: number | undefined): IEditableData | undefined; - forward(remote: number, local?: number, name?: string): Promise; - close(remote: number): Promise; + onDidChangeEditable: Event<{ host: string, port: number | undefined }>; + setEditable(remoteHost: string | undefined, remotePort: number | undefined, data: IEditableData | null): void; + getEditableData(remoteHost: string | undefined, remotePort: number | undefined): IEditableData | undefined; + forward(remote: { host: string, port: number }, localPort?: number, name?: string): Promise; + close(remote: { host: string, port: number }): Promise; addDetected(tunnels: { remote: { port: number, host: string }, localAddress: string }[] | undefined): void; - registerCandidateFinder(finder: () => Promise<{ port: number, detail: string }[]>): void; + registerCandidateFinder(finder: () => Promise<{ host: string, port: number, detail: string }[]>): void; } export interface HelpInformation { @@ -189,9 +206,9 @@ class RemoteExplorerService implements IRemoteExplorerService { public readonly onDidChangeTargetType: Event = this._onDidChangeTargetType.event; private _helpInformation: HelpInformation[] = []; private _tunnelModel: TunnelModel; - private _editable: { remote: number | undefined, data: IEditableData } | undefined; - private readonly _onDidChangeEditable: Emitter = new Emitter(); - public readonly onDidChangeEditable: Event = this._onDidChangeEditable.event; + private _editable: { remoteHost: string, remotePort: number | undefined, data: IEditableData } | undefined; + private readonly _onDidChangeEditable: Emitter<{ host: string, port: number | undefined }> = new Emitter(); + public readonly onDidChangeEditable: Event<{ host: string, port: number | undefined }> = this._onDidChangeEditable.event; constructor( @IStorageService private readonly storageService: IStorageService, @@ -246,12 +263,12 @@ class RemoteExplorerService implements IRemoteExplorerService { return this._tunnelModel; } - forward(remote: number, local?: number, name?: string): Promise { + forward(remote: { host: string, port: number }, local?: number, name?: string): Promise { return this.tunnelModel.forward(remote, local, name); } - close(remote: number): Promise { - return this.tunnelModel.close(remote); + close(remote: { host: string, port: number }): Promise { + return this.tunnelModel.close(remote.host, remote.port); } addDetected(tunnels: { remote: { port: number, host: string }, localAddress: string }[] | undefined): void { @@ -260,20 +277,21 @@ class RemoteExplorerService implements IRemoteExplorerService { } } - setEditable(remote: number | undefined, data: IEditableData | null): void { + setEditable(remoteHost: string, remotePort: number | undefined, data: IEditableData | null): void { if (!data) { this._editable = undefined; } else { - this._editable = { remote, data }; + this._editable = { remoteHost, remotePort, data }; } - this._onDidChangeEditable.fire(remote); + this._onDidChangeEditable.fire({ host: remoteHost, port: remotePort }); } - getEditableData(remote: number | undefined): IEditableData | undefined { - return this._editable && this._editable.remote === remote ? this._editable.data : undefined; + getEditableData(remoteHost: string | undefined, remotePort: number | undefined): IEditableData | undefined { + return (this._editable && (this._editable.remotePort === remotePort) && this._editable.remoteHost === remoteHost) ? + this._editable.data : undefined; } - registerCandidateFinder(finder: () => Promise<{ port: number, detail: string }[]>): void { + registerCandidateFinder(finder: () => Promise<{ host: string, port: number, detail: string }[]>): void { this.tunnelModel.registerCandidateFinder(finder); } diff --git a/src/vs/workbench/services/remote/node/tunnelService.ts b/src/vs/workbench/services/remote/node/tunnelService.ts index 4e60ad01e15..79f473813dc 100644 --- a/src/vs/workbench/services/remote/node/tunnelService.ts +++ b/src/vs/workbench/services/remote/node/tunnelService.ts @@ -103,9 +103,9 @@ export class TunnelService implements ITunnelService { private _onTunnelOpened: Emitter = new Emitter(); public onTunnelOpened: Event = this._onTunnelOpened.event; - private _onTunnelClosed: Emitter = new Emitter(); - public onTunnelClosed: Event = this._onTunnelClosed.event; - private readonly _tunnels = new Map }>(); + 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; public constructor( @@ -130,23 +130,32 @@ export class TunnelService implements ITunnelService { } public get tunnels(): Promise { - return Promise.all(Array.from(this._tunnels.values()).map(x => x.value)); + 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 { value } of this._tunnels.values()) { - value.then(tunnel => tunnel.dispose()); + for (const portMap of this._tunnels.values()) { + for (const { value } of portMap.values()) { + value.then(tunnel => tunnel.dispose()); + } + portMap.clear(); } this._tunnels.clear(); } - openTunnel(remotePort: number, localPort: number): Promise | undefined { + openTunnel(remoteHost: string | undefined, remotePort: number, localPort: number): Promise | undefined { const remoteAuthority = this.environmentService.configuration.remoteAuthority; if (!remoteAuthority) { return undefined; } - const resolvedTunnel = this.retainOrCreateTunnel(remoteAuthority, remotePort, localPort); + if (!remoteHost || (remoteHost === '127.0.0.1')) { + remoteHost = 'localhost'; + } + + const resolvedTunnel = this.retainOrCreateTunnel(remoteAuthority, remoteHost, remotePort, localPort); if (!resolvedTunnel) { return resolvedTunnel; } @@ -165,48 +174,62 @@ export class TunnelService implements ITunnelService { tunnelLocalPort: tunnel.tunnelLocalPort, localAddress: tunnel.localAddress, dispose: () => { - const existing = this._tunnels.get(tunnel.tunnelRemotePort); - if (existing) { - existing.refcount--; - this.tryDisposeTunnel(tunnel.tunnelRemotePort, existing); + 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(remotePort: number, tunnel: { refcount: number, readonly value: Promise }): Promise { + 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(tunnel.tunnelRemotePort); + this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); }); - this._tunnels.delete(remotePort); + if (this._tunnels.has(remoteHost)) { + this._tunnels.get(remoteHost)!.delete(remotePort); + } return disposePromise; } } - async closeTunnel(remotePort: number): Promise { - if (this._tunnels.has(remotePort)) { - const value = this._tunnels.get(remotePort)!; + 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(remotePort, value); + await this.tryDisposeTunnel(remoteHost, remotePort, value); } } - private retainOrCreateTunnel(remoteAuthority: string, remotePort: number, localPort?: number): Promise | undefined { - const existing = this._tunnels.get(remotePort); + 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 }); + } + + private 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) { ++existing.refcount; return existing.value; } if (this._tunnelProvider) { - const tunnel = this._tunnelProvider.forwardPort({ remote: { host: 'localhost', port: remotePort } }); + const tunnel = this._tunnelProvider.forwardPort({ remote: { host: remoteHost, port: remotePort } }); if (tunnel) { - this._tunnels.set(remotePort, { refcount: 1, value: tunnel }); + this.addTunnelToMap(remoteHost, remotePort, tunnel); } return tunnel; - } else { + } else if (remoteHost === 'localhost') { const options: IConnectionOptions = { commit: product.commit, socketFactory: nodeSocketFactory, @@ -221,9 +244,10 @@ export class TunnelService implements ITunnelService { }; const tunnel = createRemoteTunnel(options, remotePort, localPort); - this._tunnels.set(remotePort, { refcount: 1, value: tunnel }); + this.addTunnelToMap(remoteHost, remotePort, tunnel); return tunnel; } + return undefined; } }