Add status bar entry for forwarded ports

Fixes microsoft/vscode-remote-release#3825
This commit is contained in:
Alex Ross
2020-10-13 16:27:33 +02:00
parent 198b5e7bce
commit a583bdd151
2 changed files with 132 additions and 101 deletions
@@ -17,9 +17,9 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { FilterViewPaneContainer } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { ForwardedPortsView, VIEWLET_ID } from 'vs/workbench/contrib/remote/browser/remoteExplorer';
import { AutomaticPortForwarding, ForwardedPortsView, VIEWLET_ID } from 'vs/workbench/contrib/remote/browser/remoteExplorer';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IViewDescriptor, IViewsRegistry, Extensions, ViewContainerLocation, IViewContainersRegistry, IViewDescriptorService, IViewsService } from 'vs/workbench/common/views';
import { IViewDescriptor, IViewsRegistry, Extensions, ViewContainerLocation, IViewContainersRegistry, IViewDescriptorService } from 'vs/workbench/common/views';
import { Registry } from 'vs/platform/registry/common/platform';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { IOpenerService } from 'vs/platform/opener/common/opener';
@@ -37,14 +37,13 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { ReconnectionWaitEvent, PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection';
import Severity from 'vs/base/common/severity';
import { ReloadWindowAction } from 'vs/workbench/browser/actions/windowActions';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { IDisposable } from 'vs/base/common/lifecycle';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { SwitchRemoteViewItem, SwitchRemoteAction } from 'vs/workbench/contrib/remote/browser/explorerViewItems';
import { Action, IActionViewItem, IAction } from 'vs/base/common/actions';
import { isStringArray } from 'vs/base/common/types';
import { IRemoteExplorerService, MakeAddress, mapHasTunnelLocalhostOrAllInterfaces } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { forwardedPortsViewEnabled, OpenPortInBrowserAction } from 'vs/workbench/contrib/remote/browser/tunnelView';
import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { ITreeRenderer, ITreeNode, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree';
@@ -56,9 +55,6 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { RemoteStatusIndicator } from 'vs/workbench/contrib/remote/browser/remoteIndicator';
import { inQuickPickContextKeyValue } from 'vs/workbench/browser/quickaccess';
import { Codicon, registerIcon } from 'vs/base/common/codicons';
import { INotificationService, IPromptChoice } from 'vs/platform/notification/common/notification';
import { UrlFinder } from 'vs/workbench/contrib/remote/browser/urlFinder';
import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
export interface HelpInformation {
extensionDescription: IExtensionDescription;
@@ -827,91 +823,6 @@ class RemoteAgentConnectionStatusListener implements IWorkbenchContribution {
}
}
class AutomaticPortForwarding extends Disposable implements IWorkbenchContribution {
private contextServiceListener?: IDisposable;
private urlFinder?: UrlFinder;
private static AUTO_FORWARD_SETTING = 'remote.autoForwardPorts';
constructor(
@ITerminalService private readonly terminalService: ITerminalService,
@INotificationService private readonly notificationService: INotificationService,
@IOpenerService private readonly openerService: IOpenerService,
@IViewsService private readonly viewsService: IViewsService,
@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IConfigurationService private readonly configurationService: IConfigurationService
) {
super();
this._register(configurationService.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration(AutomaticPortForwarding.AUTO_FORWARD_SETTING)) {
this.tryStartStopUrlFinder();
}
}));
if (this.environmentService.remoteAuthority) {
this.contextServiceListener = this._register(this.contextKeyService.onDidChangeContext(e => {
if (e.affectsSome(new Set(forwardedPortsViewEnabled.keys()))) {
this.tryStartStopUrlFinder();
}
}));
this.tryStartStopUrlFinder();
}
}
private tryStartStopUrlFinder() {
if (this.configurationService.getValue(AutomaticPortForwarding.AUTO_FORWARD_SETTING)) {
this.startUrlFinder();
} else {
this.stopUrlFinder();
}
}
private startUrlFinder() {
if (!this.urlFinder && !forwardedPortsViewEnabled.getValue(this.contextKeyService)) {
return;
}
if (this.contextServiceListener) {
this.contextServiceListener.dispose();
}
this.urlFinder = this._register(new UrlFinder(this.terminalService));
this._register(this.urlFinder.onDidMatchLocalUrl(async (localUrl) => {
if (mapHasTunnelLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.forwarded, localUrl.host, localUrl.port)) {
return;
}
const forwarded = await this.remoteExplorerService.forward(localUrl);
if (forwarded) {
const address = MakeAddress(forwarded.tunnelRemoteHost, forwarded.tunnelRemotePort);
const message = nls.localize('remote.tunnelsView.automaticForward', "{0} from the remote has been forwarded to {1} locally.",
address, forwarded.localAddress);
const browserChoice: IPromptChoice = {
label: OpenPortInBrowserAction.LABEL,
run: () => OpenPortInBrowserAction.run(this.remoteExplorerService.tunnelModel, this.openerService, address)
};
const showChoice: IPromptChoice = {
label: nls.localize('remote.tunnelsView.showView', "Show Forwarded Ports"),
run: () => {
const remoteAuthority = this.environmentService.remoteAuthority;
const explorerType: string[] | undefined = remoteAuthority ? [remoteAuthority.split('+')[0]] : undefined;
if (explorerType) {
this.remoteExplorerService.targetType = explorerType;
}
this.viewsService.openViewContainer(VIEWLET_ID);
}
};
this.notificationService.prompt(Severity.Info, message, [browserChoice, showChoice], { neverShowAgain: { id: 'remote.tunnelsView.autoForwardNeverShow', isSecondary: true } });
}
}));
}
private stopUrlFinder() {
if (this.urlFinder) {
this.urlFinder.dispose();
this.urlFinder = undefined;
}
}
}
const workbenchContributionsRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentConnectionStatusListener, LifecyclePhase.Eventually);
workbenchContributionsRegistry.registerWorkbenchContribution(RemoteStatusIndicator, LifecyclePhase.Starting);
@@ -5,33 +5,42 @@
import * as nls from 'vs/nls';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { Extensions, IViewDescriptorService, IViewsRegistry } from 'vs/workbench/common/views';
import { Extensions, IViewDescriptorService, IViewsRegistry, IViewsService } from 'vs/workbench/common/views';
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
import { IRemoteExplorerService, TUNNEL_VIEW_ID } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { forwardedPortsViewEnabled, ForwardPortAction, TunnelPanelDescriptor, TunnelViewModel } from 'vs/workbench/contrib/remote/browser/tunnelView';
import { IRemoteExplorerService, MakeAddress, mapHasTunnelLocalhostOrAllInterfaces, TUNNEL_VIEW_ID } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { forwardedPortsViewEnabled, ForwardPortAction, OpenPortInBrowserAction, TunnelPanelDescriptor, TunnelViewModel } from 'vs/workbench/contrib/remote/browser/tunnelView';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { Registry } from 'vs/platform/registry/common/platform';
import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/statusbar';
import { UrlFinder } from 'vs/workbench/contrib/remote/browser/urlFinder';
import Severity from 'vs/base/common/severity';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { INotificationService, IPromptChoice } from 'vs/platform/notification/common/notification';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
export const VIEWLET_ID = 'workbench.view.remote';
export class ForwardedPortsView extends Disposable implements IWorkbenchContribution {
private contextKeyListener?: IDisposable;
private _activityBadge?: IDisposable;
private entryAccessor: IStatusbarEntryAccessor | undefined;
constructor(
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService,
@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,
@IActivityService private readonly activityService: IActivityService
@IActivityService private readonly activityService: IActivityService,
@IStatusbarService private readonly statusbarService: IStatusbarService
) {
super();
this._register(Registry.as<IViewsRegistry>(Extensions.ViewsRegistry).registerViewWelcomeContent(TUNNEL_VIEW_ID, {
content: `Forwarded ports allow you to access your running applications locally.\n[Forward a Port](command:${ForwardPortAction.INLINE_ID})`,
}));
this.enableBadgeAndStatusBar();
this.enableForwardedPortsView();
this.enableBadge();
}
private enableForwardedPortsView() {
@@ -57,12 +66,19 @@ export class ForwardedPortsView extends Disposable implements IWorkbenchContribu
}
}
private enableBadge() {
this._register(this.remoteExplorerService.tunnelModel.onForwardPort(() => this.updateActivityBadge()));
this._register(this.remoteExplorerService.tunnelModel.onClosePort(() => this.updateActivityBadge()));
private enableBadgeAndStatusBar() {
this._register(this.remoteExplorerService.tunnelModel.onForwardPort(() => {
this.updateActivityBadge();
this.updateStatusBar();
}));
this._register(this.remoteExplorerService.tunnelModel.onClosePort(() => {
this.updateActivityBadge();
this.updateStatusBar();
}));
const disposable = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry).onViewsRegistered(e => {
if (e.find(view => view.views.find(viewDescriptor => viewDescriptor.id === TUNNEL_VIEW_ID))) {
this.updateActivityBadge();
this.updateStatusBar();
disposable.dispose();
}
});
@@ -81,4 +97,108 @@ export class ForwardedPortsView extends Disposable implements IWorkbenchContribu
}
}
}
private updateStatusBar() {
if (!this.entryAccessor && this.remoteExplorerService.tunnelModel.forwarded.size > 0) {
this._register(this.entryAccessor = this.statusbarService.addEntry(this.entry, 'status.forwardedPorts', nls.localize('status.forwardedPorts', "Forwarded Ports"), StatusbarAlignment.LEFT, 40));
} else if (this.entryAccessor && this.remoteExplorerService.tunnelModel.forwarded.size === 0) {
this.entryAccessor.dispose();
this.entryAccessor = undefined;
}
}
private get entry(): IStatusbarEntry {
return {
text: '$(radio-tower) ' + nls.localize('remote.forwardedPorts.statusbarText', "Application running"),
ariaLabel: nls.localize('remote.forwardedPorts.statusbarAria', "Application running and available locally through port forwarding"),
tooltip: nls.localize('remote.forwardedPorts.statusbarTooltip', "Application available locally (port forwarded)"),
command: `${TUNNEL_VIEW_ID}.focus`
};
}
}
export class AutomaticPortForwarding extends Disposable implements IWorkbenchContribution {
private contextServiceListener?: IDisposable;
private urlFinder?: UrlFinder;
private static AUTO_FORWARD_SETTING = 'remote.autoForwardPorts';
constructor(
@ITerminalService private readonly terminalService: ITerminalService,
@INotificationService private readonly notificationService: INotificationService,
@IOpenerService private readonly openerService: IOpenerService,
@IViewsService private readonly viewsService: IViewsService,
@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IConfigurationService private readonly configurationService: IConfigurationService
) {
super();
this._register(configurationService.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration(AutomaticPortForwarding.AUTO_FORWARD_SETTING)) {
this.tryStartStopUrlFinder();
}
}));
if (this.environmentService.remoteAuthority) {
this.contextServiceListener = this._register(this.contextKeyService.onDidChangeContext(e => {
if (e.affectsSome(new Set(forwardedPortsViewEnabled.keys()))) {
this.tryStartStopUrlFinder();
}
}));
this.tryStartStopUrlFinder();
}
}
private tryStartStopUrlFinder() {
if (this.configurationService.getValue(AutomaticPortForwarding.AUTO_FORWARD_SETTING)) {
this.startUrlFinder();
} else {
this.stopUrlFinder();
}
}
private startUrlFinder() {
if (!this.urlFinder && !forwardedPortsViewEnabled.getValue(this.contextKeyService)) {
return;
}
if (this.contextServiceListener) {
this.contextServiceListener.dispose();
}
this.urlFinder = this._register(new UrlFinder(this.terminalService));
this._register(this.urlFinder.onDidMatchLocalUrl(async (localUrl) => {
if (mapHasTunnelLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.forwarded, localUrl.host, localUrl.port)) {
return;
}
const forwarded = await this.remoteExplorerService.forward(localUrl);
if (forwarded) {
const address = MakeAddress(forwarded.tunnelRemoteHost, forwarded.tunnelRemotePort);
const message = nls.localize('remote.tunnelsView.automaticForward', "{0} from the remote has been forwarded to {1} locally.",
address, forwarded.localAddress);
const browserChoice: IPromptChoice = {
label: OpenPortInBrowserAction.LABEL,
run: () => OpenPortInBrowserAction.run(this.remoteExplorerService.tunnelModel, this.openerService, address)
};
const showChoice: IPromptChoice = {
label: nls.localize('remote.tunnelsView.showView', "Show Forwarded Ports"),
run: () => {
const remoteAuthority = this.environmentService.remoteAuthority;
const explorerType: string[] | undefined = remoteAuthority ? [remoteAuthority.split('+')[0]] : undefined;
if (explorerType) {
this.remoteExplorerService.targetType = explorerType;
}
this.viewsService.openViewContainer(VIEWLET_ID);
}
};
this.notificationService.prompt(Severity.Info, message, [browserChoice, showChoice], { neverShowAgain: { id: 'remote.tunnelsView.autoForwardNeverShow', isSecondary: true } });
}
}));
}
private stopUrlFinder() {
if (this.urlFinder) {
this.urlFinder.dispose();
this.urlFinder = undefined;
}
}
}