diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index ee35a7d80b0..794d8863dc9 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -178,7 +178,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe const address = row.local_address.split(':'); return { socket: parseInt(row.inode, 10), - ip: address[0], + ip: this.parseIpAddress(address[0]), port: parseInt(address[1], 16) }; }).map(port => [port.port, port]) @@ -186,6 +186,17 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe ]; } + private parseIpAddress(hex: string): string { + let result = ''; + for (let i = hex.length - 2; (i >= 0); i -= 2) { + result += parseInt(hex.substr(i, 2), 16); + if (i !== 0) { + result += '.'; + } + } + return result; + } + private loadConnectionTable(stdout: string): Record[] { const lines = stdout.trim().split('\n'); const names = lines.shift()!.trim().split(/\s+/) diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index daedf64538d..c3274afa90d 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -20,13 +20,13 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { Disposable, IDisposable, toDisposable, MutableDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable, MutableDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { ActionBar, ActionViewItem, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; 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, MakeAddress } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { IRemoteExplorerService, TunnelModel, MakeAddress, TunnelType, ITunnelItem } 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'; @@ -55,6 +55,7 @@ export interface ITunnelViewModel { readonly forwarded: TunnelItem[]; readonly detected: TunnelItem[]; readonly candidates: Promise; + readonly input: ITunnelItem | ITunnelGroup | undefined; groups(): Promise; } @@ -62,6 +63,7 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { private _onForwardedPortsChanged: Emitter = new Emitter(); public onForwardedPortsChanged: Event = this._onForwardedPortsChanged.event; private model: TunnelModel; + private _input: ITunnelItem | ITunnelGroup | undefined; constructor( @IRemoteExplorerService remoteExplorerService: IRemoteExplorerService) { @@ -70,6 +72,7 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { this._register(this.model.onForwardPort(() => this._onForwardedPortsChanged.fire())); this._register(this.model.onClosePort(() => this._onForwardedPortsChanged.fire())); this._register(this.model.onPortName(() => this._onForwardedPortsChanged.fire())); + this._register(this.model.onCandidatesChanged(() => this._onForwardedPortsChanged.fire())); } async groups(): Promise { @@ -96,10 +99,13 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { items: candidates }); } - groups.push({ - label: nls.localize('remote.tunnelsView.add', "Forward a Port..."), - tunnelType: TunnelType.Add, - }); + if (!this._input) { + this._input = { + label: nls.localize('remote.tunnelsView.add', "Forward a Port..."), + tunnelType: TunnelType.Add, + }; + } + groups.push(this._input); return groups; } @@ -128,6 +134,10 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { }); } + get input(): ITunnelItem | ITunnelGroup | undefined { + return this._input; + } + dispose() { super.dispose(); } @@ -197,7 +207,7 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer; } -interface ITunnelItem { - tunnelType: TunnelType; - remoteHost: string; - remotePort: number; - localAddress?: string; - name?: string; - closeable?: boolean; - readonly description?: string; - readonly label: string; -} - class TunnelItem implements ITunnelItem { constructor( public tunnelType: TunnelType, @@ -472,12 +465,12 @@ export class TunnelPanel extends ViewPane { this._register(Event.debounce(navigator.onDidOpenResource, (last, event) => event, 75, true)(e => { if (e.element && (e.element.tunnelType === TunnelType.Add)) { - this.commandService.executeCommand(ForwardPortAction.ID, 'inline add'); + this.commandService.executeCommand(ForwardPortAction.INLINE_ID); } })); this._register(this.remoteExplorerService.onDidChangeEditable(async e => { - const isEditing = !!this.remoteExplorerService.getEditableData(e.host, e.port); + const isEditing = !!this.remoteExplorerService.getEditableData(e); if (!isEditing) { dom.removeClass(treeContainer, 'highlight'); @@ -487,6 +480,7 @@ export class TunnelPanel extends ViewPane { if (isEditing) { dom.addClass(treeContainer, 'highlight'); + this.tree.reveal(e ? e : this.viewModel.input); } else { this.tree.domFocus(); } @@ -494,8 +488,7 @@ export class TunnelPanel extends ViewPane { } private get contributedContextMenu(): IMenu { - const contributedContextMenu = this.menuService.createMenu(MenuId.TunnelContext, this.tree.contextKeyService); - this._register(contributedContextMenu); + const contributedContextMenu = this._register(this.menuService.createMenu(MenuId.TunnelContext, this.tree.contextKeyService)); return contributedContextMenu; } @@ -578,12 +571,12 @@ namespace LabelTunnelAction { return async (accessor, arg) => { if (arg instanceof TunnelItem) { const remoteExplorerService = accessor.get(IRemoteExplorerService); - remoteExplorerService.setEditable(arg.remoteHost, arg.remotePort, { + remoteExplorerService.setEditable(arg, { onFinish: (value, success) => { if (success) { remoteExplorerService.tunnelModel.name(arg.remoteHost, arg.remotePort, value); } - remoteExplorerService.setEditable(arg.remoteHost, arg.remotePort, null); + remoteExplorerService.setEditable(arg, null); }, validationMessage: () => null, placeholder: nls.localize('remote.tunnelsView.labelPlaceholder', "Port label"), @@ -596,7 +589,8 @@ namespace LabelTunnelAction { } namespace ForwardPortAction { - export const ID = 'remote.tunnel.forward'; + export const INLINE_ID = 'remote.tunnel.forwardInline'; + export const COMMANDPALETTE_ID = 'remote.tunnel.forwardCommandPalette'; export const LABEL = nls.localize('remote.tunnel.forward', "Forward a Port"); const forwardPrompt = nls.localize('remote.tunnel.forwardPrompt', "Port number or address (eg. 3000 or 10.10.10.10:2000)."); @@ -615,35 +609,40 @@ namespace ForwardPortAction { return null; } - export function handler(): ICommandHandler { + export function inlineHandler(): ICommandHandler { return async (accessor, arg) => { const remoteExplorerService = accessor.get(IRemoteExplorerService); if (arg instanceof TunnelItem) { remoteExplorerService.forward({ host: arg.remoteHost, port: arg.remotePort }); - } else if (arg) { - remoteExplorerService.setEditable(undefined, undefined, { + } else { + remoteExplorerService.setEditable(undefined, { onFinish: (value, success) => { let parsed: { host: string, port: number } | undefined; if (success && (parsed = parseInput(value))) { remoteExplorerService.forward({ host: parsed.host, port: parsed.port }); } - remoteExplorerService.setEditable(undefined, undefined, null); + remoteExplorerService.setEditable(undefined, null); }, validationMessage: validateInput, placeholder: forwardPrompt }); - } else { - const viewsService = accessor.get(IViewsService); - const quickInputService = accessor.get(IQuickInputService); - await viewsService.openView(TunnelPanel.ID, true); - const value = await quickInputService.input({ - prompt: forwardPrompt, - validateInput: (value) => Promise.resolve(validateInput(value)) - }); - let parsed: { host: string, port: number } | undefined; - if (value && (parsed = parseInput(value))) { - remoteExplorerService.forward({ host: parsed.host, port: parsed.port }); - } + } + }; + } + + export function commandPaletteHandler(): ICommandHandler { + return async (accessor, arg) => { + const remoteExplorerService = accessor.get(IRemoteExplorerService); + const viewsService = accessor.get(IViewsService); + const quickInputService = accessor.get(IQuickInputService); + await viewsService.openView(TunnelPanel.ID, true); + const value = await quickInputService.input({ + prompt: forwardPrompt, + validateInput: (value) => Promise.resolve(validateInput(value)) + }); + let parsed: { host: string, port: number } | undefined; + if (value && (parsed = parseInput(value))) { + remoteExplorerService.forward({ host: parsed.host, port: parsed.port }); } }; } @@ -702,30 +701,51 @@ namespace CopyAddressAction { } } +namespace RefreshTunnelViewAction { + export const ID = 'remote.tunnel.refresh'; + export const LABEL = nls.localize('remote.tunnel.refreshView', "Refresh"); + + export function handler(): ICommandHandler { + return (accessor, arg) => { + const remoteExplorerService = accessor.get(IRemoteExplorerService); + return remoteExplorerService.refresh(); + }; + } +} + CommandsRegistry.registerCommand(LabelTunnelAction.ID, LabelTunnelAction.handler()); -CommandsRegistry.registerCommand(ForwardPortAction.ID, ForwardPortAction.handler()); +CommandsRegistry.registerCommand(ForwardPortAction.INLINE_ID, ForwardPortAction.inlineHandler()); +CommandsRegistry.registerCommand(ForwardPortAction.COMMANDPALETTE_ID, ForwardPortAction.commandPaletteHandler()); CommandsRegistry.registerCommand(ClosePortAction.ID, ClosePortAction.handler()); CommandsRegistry.registerCommand(OpenPortInBrowserAction.ID, OpenPortInBrowserAction.handler()); CommandsRegistry.registerCommand(CopyAddressAction.ID, CopyAddressAction.handler()); +CommandsRegistry.registerCommand(RefreshTunnelViewAction.ID, RefreshTunnelViewAction.handler()); MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({ command: { - id: ForwardPortAction.ID, + id: ForwardPortAction.COMMANDPALETTE_ID, title: ForwardPortAction.LABEL }, when: forwardedPortsViewEnabled })); - - MenuRegistry.appendMenuItem(MenuId.TunnelTitle, ({ group: 'navigation', order: 0, command: { - id: ForwardPortAction.ID, + id: ForwardPortAction.COMMANDPALETTE_ID, title: ForwardPortAction.LABEL, icon: { id: 'codicon/plus' } } })); +MenuRegistry.appendMenuItem(MenuId.TunnelTitle, ({ + group: 'navigation', + order: 1, + command: { + id: RefreshTunnelViewAction.ID, + title: RefreshTunnelViewAction.LABEL, + icon: { id: 'codicon/refresh' } + } +})); MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ group: '0_manage', order: 0, @@ -757,7 +777,7 @@ MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ group: '0_manage', order: 1, command: { - id: ForwardPortAction.ID, + id: ForwardPortAction.INLINE_ID, title: ForwardPortAction.LABEL, }, when: TunnelTypeContextKey.isEqualTo(TunnelType.Candidate) @@ -784,7 +804,7 @@ MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({ MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({ order: 0, command: { - id: ForwardPortAction.ID, + id: ForwardPortAction.INLINE_ID, title: ForwardPortAction.LABEL, icon: { id: 'codicon/plus' } }, diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 7453c0297d4..7654610a629 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -19,6 +19,24 @@ export const IRemoteExplorerService = createDecorator('r export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType'; const TUNNELS_TO_RESTORE = 'remote.tunnels.toRestore'; +export enum TunnelType { + Candidate = 'Candidate', + Detected = 'Detected', + Forwarded = 'Forwarded', + Add = 'Add' +} + +export interface ITunnelItem { + tunnelType: TunnelType; + remoteHost: string; + remotePort: number; + localAddress?: string; + name?: string; + closeable?: boolean; + readonly description?: string; + readonly label: string; +} + export interface Tunnel { remoteHost: string; remotePort: number; @@ -45,7 +63,10 @@ export class TunnelModel extends Disposable { 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 _candidates: { host: string, port: number, detail: string }[] = []; private _candidateFinder: (() => Promise<{ host: string, port: number, detail: string }[]>) | undefined; + private _onCandidatesChanged: Emitter = new Emitter(); + public onCandidatesChanged: Event = this._onCandidatesChanged.event; constructor( @ITunnelService private readonly tunnelService: ITunnelService, @@ -168,10 +189,18 @@ export class TunnelModel extends Disposable { } get candidates(): Promise<{ host: string, port: number, detail: string }[]> { + return this.updateCandidates().then(() => this._candidates); + } + + private async updateCandidates(): Promise { if (this._candidateFinder) { - return this._candidateFinder(); + this._candidates = await this._candidateFinder(); } - return Promise.resolve([]); + } + + async refresh(): Promise { + await this.updateCandidates(); + this._onCandidatesChanged.fire(); } } @@ -181,13 +210,14 @@ export interface IRemoteExplorerService { targetType: string; readonly helpInformation: HelpInformation[]; readonly tunnelModel: TunnelModel; - 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; + onDidChangeEditable: Event; + setEditable(tunnelItem: ITunnelItem | undefined, data: IEditableData | null): void; + getEditableData(tunnelItem: ITunnelItem | undefined): IEditableData | undefined; forward(remote: { host: string, port: number }, localPort?: number, name?: string): Promise; close(remote: { host: string, port: number }): Promise; addEnvironmentTunnels(tunnels: { remoteAddress: { port: number, host: string }, localAddress: string }[] | undefined): void; registerCandidateFinder(finder: () => Promise<{ host: string, port: number, detail: string }[]>): void; + refresh(): Promise; } export interface HelpInformation { @@ -232,9 +262,9 @@ class RemoteExplorerService implements IRemoteExplorerService { public readonly onDidChangeTargetType: Event = this._onDidChangeTargetType.event; private _helpInformation: HelpInformation[] = []; private _tunnelModel: TunnelModel; - 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; + private _editable: { tunnelItem: ITunnelItem | undefined, data: IEditableData } | undefined; + private readonly _onDidChangeEditable: Emitter = new Emitter(); + public readonly onDidChangeEditable: Event = this._onDidChangeEditable.event; constructor( @IStorageService private readonly storageService: IStorageService, @@ -305,17 +335,17 @@ class RemoteExplorerService implements IRemoteExplorerService { } } - setEditable(remoteHost: string, remotePort: number | undefined, data: IEditableData | null): void { + setEditable(tunnelItem: ITunnelItem | undefined, data: IEditableData | null): void { if (!data) { this._editable = undefined; } else { - this._editable = { remoteHost, remotePort, data }; + this._editable = { tunnelItem, data }; } - this._onDidChangeEditable.fire({ host: remoteHost, port: remotePort }); + this._onDidChangeEditable.fire(tunnelItem); } - getEditableData(remoteHost: string | undefined, remotePort: number | undefined): IEditableData | undefined { - return (this._editable && (this._editable.remotePort === remotePort) && this._editable.remoteHost === remoteHost) ? + getEditableData(tunnelItem: ITunnelItem | undefined): IEditableData | undefined { + return (this._editable && (!tunnelItem || (this._editable.tunnelItem?.remotePort === tunnelItem.remotePort) && (this._editable.tunnelItem.remoteHost === tunnelItem.remoteHost))) ? this._editable.data : undefined; } @@ -323,6 +353,9 @@ class RemoteExplorerService implements IRemoteExplorerService { this.tunnelModel.registerCandidateFinder(finder); } + refresh(): Promise { + return this.tunnelModel.refresh(); + } } registerSingleton(IRemoteExplorerService, RemoteExplorerService, true);