Skip to content

Commit 21dda74

Browse files
authored
Cache auto port properties (microsoft#182081)
* Add session cach for port props and merge * Only cache auto forwarded * Cache properties before closing * Add privacy mismatch log
1 parent d0ca613 commit 21dda74

File tree

5 files changed

+87
-40
lines changed

5 files changed

+87
-40
lines changed

src/vs/platform/tunnel/common/tunnel.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,9 @@ export abstract class AbstractTunnelService implements ITunnelService {
349349
if (tunnel.tunnelRemoteHost !== remoteHost || tunnel.tunnelRemotePort !== remotePort) {
350350
this.logService.warn('ForwardedPorts: (TunnelService) Created tunnel does not match requirements of requested tunnel. Host or port mismatch.');
351351
}
352+
if (privacy && tunnel.privacy !== privacy) {
353+
this.logService.warn('ForwardedPorts: (TunnelService) Created tunnel does not match requirements of requested tunnel. Privacy mismatch.');
354+
}
352355
this._onTunnelOpened.fire(newTunnel);
353356
return newTunnel;
354357
});

src/vs/workbench/api/browser/mainThreadTunnelService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as nls from 'vs/nls';
77
import { MainThreadTunnelServiceShape, MainContext, ExtHostContext, ExtHostTunnelServiceShape, CandidatePortSource, PortAttributesProviderSelector, TunnelDto } from 'vs/workbench/api/common/extHost.protocol';
88
import { TunnelDtoConverter } from 'vs/workbench/api/common/extHostTunnelService';
99
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
10-
import { CandidatePort, IRemoteExplorerService, makeAddress, PORT_AUTO_FORWARD_SETTING, PORT_AUTO_SOURCE_SETTING, PORT_AUTO_SOURCE_SETTING_OUTPUT, TunnelSource } from 'vs/workbench/services/remote/common/remoteExplorerService';
10+
import { CandidatePort, IRemoteExplorerService, makeAddress, PORT_AUTO_FORWARD_SETTING, PORT_AUTO_SOURCE_SETTING, PORT_AUTO_SOURCE_SETTING_OUTPUT, TunnelCloseReason, TunnelSource } from 'vs/workbench/services/remote/common/remoteExplorerService';
1111
import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions, RemoteTunnel, ProvidedPortAttributes, PortAttributesProvider, TunnelProtocol } from 'vs/platform/tunnel/common/tunnel';
1212
import { Disposable } from 'vs/base/common/lifecycle';
1313
import type { TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver';
@@ -129,7 +129,7 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun
129129
label: nls.localize('remote.tunnelsView.elevationButton', "Use Port {0} as Sudo...", tunnel.tunnelRemotePort),
130130
run: async () => {
131131
this.elevateionRetry = true;
132-
await this.remoteExplorerService.close({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort });
132+
await this.remoteExplorerService.close({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }, TunnelCloseReason.Other);
133133
await this.remoteExplorerService.forward({
134134
remote: tunnelOptions.remoteAddress,
135135
local: tunnelOptions.localAddressPort,
@@ -146,7 +146,7 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun
146146
}
147147

148148
async $closeTunnel(remote: { host: string; port: number }): Promise<void> {
149-
return this.remoteExplorerService.close(remote);
149+
return this.remoteExplorerService.close(remote, TunnelCloseReason.Other);
150150
}
151151

152152
async $getTunnels(): Promise<TunnelDescription[]> {

src/vs/workbench/contrib/remote/browser/remoteExplorer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as nls from 'vs/nls';
66
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
77
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
88
import { Extensions, IViewContainersRegistry, IViewsRegistry, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views';
9-
import { Attributes, AutoTunnelSource, IRemoteExplorerService, makeAddress, mapHasAddressLocalhostOrAllInterfaces, OnPortForward, PORT_AUTO_FORWARD_SETTING, PORT_AUTO_SOURCE_SETTING, PORT_AUTO_SOURCE_SETTING_HYBRID, PORT_AUTO_SOURCE_SETTING_OUTPUT, PORT_AUTO_SOURCE_SETTING_PROCESS, Tunnel, TUNNEL_VIEW_CONTAINER_ID, TUNNEL_VIEW_ID, TunnelSource } from 'vs/workbench/services/remote/common/remoteExplorerService';
9+
import { Attributes, AutoTunnelSource, IRemoteExplorerService, makeAddress, mapHasAddressLocalhostOrAllInterfaces, OnPortForward, PORT_AUTO_FORWARD_SETTING, PORT_AUTO_SOURCE_SETTING, PORT_AUTO_SOURCE_SETTING_HYBRID, PORT_AUTO_SOURCE_SETTING_OUTPUT, PORT_AUTO_SOURCE_SETTING_PROCESS, Tunnel, TUNNEL_VIEW_CONTAINER_ID, TUNNEL_VIEW_ID, TunnelCloseReason, TunnelSource } from 'vs/workbench/services/remote/common/remoteExplorerService';
1010
import { forwardedPortsViewEnabled, ForwardPortAction, OpenPortInBrowserAction, TunnelPanel, TunnelPanelDescriptor, TunnelViewModel, OpenPortInPreviewAction, openPreviewEnabledContext } from 'vs/workbench/contrib/remote/browser/tunnelView';
1111
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
1212
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
@@ -380,7 +380,7 @@ class OnAutoForwardedAction extends Disposable {
380380
// Privileged ports are not on Windows, so it's ok to stick to just "sudo".
381381
label: nls.localize('remote.tunnelsView.elevationButton', "Use Port {0} as Sudo...", tunnel.tunnelRemotePort),
382382
run: async () => {
383-
await this.remoteExplorerService.close({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort });
383+
await this.remoteExplorerService.close({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }, TunnelCloseReason.Other);
384384
const newTunnel = await this.remoteExplorerService.forward({
385385
remote: { host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort },
386386
local: tunnel.tunnelRemotePort,
@@ -643,7 +643,7 @@ class ProcAutomaticPortForwarding extends Disposable {
643643
} else {
644644
value = { host: forwardedValue.remoteHost, port: forwardedValue.remotePort };
645645
}
646-
await this.remoteExplorerService.close(value);
646+
await this.remoteExplorerService.close(value, TunnelCloseReason.AutoForwardEnd);
647647
removedPorts.push(value.port);
648648
} else if (this.notifiedOnly.has(key)) {
649649
this.notifiedOnly.delete(key);

src/vs/workbench/contrib/remote/browser/tunnelView.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { ActionRunner, IAction } from 'vs/base/common/actions';
2424
import { IMenuService, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions';
2525
import { ILocalizedString } from 'vs/platform/action/common/action';
2626
import { createAndFillInActionBarActions, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
27-
import { IRemoteExplorerService, TunnelModel, makeAddress, TunnelType, ITunnelItem, Tunnel, TUNNEL_VIEW_ID, parseAddress, CandidatePort, TunnelEditId, mapHasAddressLocalhostOrAllInterfaces, Attributes, TunnelSource } from 'vs/workbench/services/remote/common/remoteExplorerService';
27+
import { IRemoteExplorerService, TunnelModel, makeAddress, TunnelType, ITunnelItem, Tunnel, TUNNEL_VIEW_ID, parseAddress, CandidatePort, TunnelEditId, mapHasAddressLocalhostOrAllInterfaces, Attributes, TunnelSource, TunnelCloseReason } from 'vs/workbench/services/remote/common/remoteExplorerService';
2828
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
2929
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
3030
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
@@ -1252,7 +1252,7 @@ namespace ClosePortAction {
12521252
if (!ports || ports.length === 0) {
12531253
return;
12541254
}
1255-
return Promise.all(ports.map(port => remoteExplorerService.close({ host: port.remoteHost, port: port.remotePort })));
1255+
return Promise.all(ports.map(port => remoteExplorerService.close({ host: port.remoteHost, port: port.remotePort }, TunnelCloseReason.User)));
12561256
};
12571257
}
12581258

@@ -1266,7 +1266,7 @@ namespace ClosePortAction {
12661266
const picks: QuickPickInput<QuickPickTunnel>[] = makeTunnelPicks(Array.from(remoteExplorerService.tunnelModel.forwarded.values()).filter(tunnel => tunnel.closeable), remoteExplorerService, tunnelService);
12671267
const result = await quickInputService.pick(picks, { placeHolder: nls.localize('remote.tunnel.closePlaceholder', "Choose a port to stop forwarding") });
12681268
if (result && result.tunnel) {
1269-
await remoteExplorerService.close({ host: result.tunnel.remoteHost, port: result.tunnel.remotePort });
1269+
await remoteExplorerService.close({ host: result.tunnel.remoteHost, port: result.tunnel.remotePort }, TunnelCloseReason.User);
12701270
} else if (result) {
12711271
await commandService.executeCommand(ForwardPortAction.COMMANDPALETTE_ID);
12721272
}
@@ -1468,7 +1468,7 @@ namespace ChangeLocalPortAction {
14681468
onFinish: async (value, success) => {
14691469
remoteExplorerService.setEditable(tunnelItem, TunnelEditId.LocalPort, null);
14701470
if (success) {
1471-
await remoteExplorerService.close({ host: tunnelItem.remoteHost, port: tunnelItem.remotePort });
1471+
await remoteExplorerService.close({ host: tunnelItem.remoteHost, port: tunnelItem.remotePort }, TunnelCloseReason.Other);
14721472
const numberValue = Number(value);
14731473
const newForward = await remoteExplorerService.forward({
14741474
remote: { host: tunnelItem.remoteHost, port: tunnelItem.remotePort },
@@ -1495,7 +1495,7 @@ namespace ChangeTunnelPrivacyAction {
14951495
return async (accessor, arg) => {
14961496
if (isITunnelItem(arg)) {
14971497
const remoteExplorerService = accessor.get(IRemoteExplorerService);
1498-
await remoteExplorerService.close({ host: arg.remoteHost, port: arg.remotePort });
1498+
await remoteExplorerService.close({ host: arg.remoteHost, port: arg.remotePort }, TunnelCloseReason.Other);
14991499
return remoteExplorerService.forward({
15001500
remote: { host: arg.remoteHost, port: arg.remotePort },
15011501
local: arg.localPort,

src/vs/workbench/services/remote/common/remoteExplorerService.ts

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ export enum TunnelType {
4444
Add = 'Add'
4545
}
4646

47+
export enum TunnelCloseReason {
48+
Other = 'Other',
49+
User = 'User',
50+
AutoForwardEnd = 'AutoForwardEnd',
51+
}
52+
4753
export interface ITunnelItem {
4854
tunnelType: TunnelType;
4955
remoteHost: string;
@@ -426,6 +432,7 @@ export class TunnelModel extends Disposable {
426432
private restoreListener: IDisposable | undefined;
427433
private knownPortsRestoreValue: string | undefined;
428434
private unrestoredExtensionTunnels: Map<string, Tunnel> = new Map();
435+
private sessionCachedProperties: Map<string, Partial<TunnelProperties>> = new Map();
429436

430437
private portAttributesProviders: PortAttributesProvider[] = [];
431438

@@ -500,11 +507,11 @@ export class TunnelModel extends Disposable {
500507
this._onForwardPort.fire(this.forwarded.get(key)!);
501508
}));
502509
this._register(this.tunnelService.onTunnelClosed(address => {
503-
return this.onTunnelClosed(address);
510+
return this.onTunnelClosed(address, TunnelCloseReason.Other);
504511
}));
505512
}
506513

507-
private async onTunnelClosed(address: { host: string; port: number }) {
514+
private async onTunnelClosed(address: { host: string; port: number }, reason: TunnelCloseReason) {
508515
const key = makeAddress(address.host, address.port);
509516
if (this.forwarded.has(key)) {
510517
this.forwarded.delete(key);
@@ -631,7 +638,9 @@ export class TunnelModel extends Disposable {
631638

632639
const key = makeAddress(tunnelProperties.remote.host, tunnelProperties.remote.port);
633640
this.inProgress.set(key, true);
634-
let tunnel = await this.tunnelService.openTunnel(addressProvider, tunnelProperties.remote.host, tunnelProperties.remote.port, undefined, localPort, (!tunnelProperties.elevateIfNeeded) ? attributes?.elevateIfNeeded : tunnelProperties.elevateIfNeeded, tunnelProperties.privacy, attributes?.protocol);
641+
tunnelProperties = this.mergeCachedAndUnrestoredProperties(key, tunnelProperties);
642+
643+
const tunnel = await this.tunnelService.openTunnel(addressProvider, tunnelProperties.remote.host, tunnelProperties.remote.port, undefined, localPort, (!tunnelProperties.elevateIfNeeded) ? attributes?.elevateIfNeeded : tunnelProperties.elevateIfNeeded, tunnelProperties.privacy, attributes?.protocol);
635644
if (tunnel && tunnel.localAddress) {
636645
const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces<CandidatePort>(this._candidates ?? new Map(), tunnelProperties.remote.host, tunnelProperties.remote.port);
637646
const protocol = (tunnel.protocol ?
@@ -658,37 +667,63 @@ export class TunnelModel extends Disposable {
658667
await this.storeForwarded();
659668
await this.showPortMismatchModalIfNeeded(tunnel, localPort, attributes);
660669
this._onForwardPort.fire(newForward);
661-
if (this.unrestoredExtensionTunnels.has(key)) {
662-
const updateProps = this.unrestoredExtensionTunnels.get(key);
663-
this.unrestoredExtensionTunnels.delete(key);
664-
if (updateProps) {
665-
tunnel = await this.forward({
666-
remote: { host: newForward.remoteHost, port: newForward.remotePort },
667-
local: newForward.localPort,
668-
name: newForward.name,
669-
privacy: updateProps.privacy,
670-
elevateIfNeeded: true,
671-
});
672-
}
673-
}
674670
return tunnel;
675671
}
676672
this.inProgress.delete(key);
677673
} else {
678-
const newName = attributes?.label ?? tunnelProperties.name;
679-
if (newName !== existingTunnel.name) {
680-
existingTunnel.name = newName;
674+
return this.mergeAttributesIntoExistingTunnel(existingTunnel, tunnelProperties, attributes);
675+
}
676+
677+
return undefined;
678+
}
679+
680+
private mergeCachedAndUnrestoredProperties(key: string, tunnelProperties: TunnelProperties): TunnelProperties {
681+
const map = this.unrestoredExtensionTunnels.has(key) ? this.unrestoredExtensionTunnels : (this.sessionCachedProperties.has(key) ? this.sessionCachedProperties : undefined);
682+
if (map) {
683+
const updateProps = map.get(key)!;
684+
map.delete(key);
685+
if (updateProps) {
686+
tunnelProperties.name = updateProps.name ?? tunnelProperties.name;
687+
tunnelProperties.local = (('local' in updateProps) ? updateProps.local : (('localPort' in updateProps) ? updateProps.localPort : undefined)) ?? tunnelProperties.local;
688+
tunnelProperties.privacy = updateProps.privacy ?? tunnelProperties.privacy;
689+
}
690+
}
691+
return tunnelProperties;
692+
}
693+
694+
private async mergeAttributesIntoExistingTunnel(existingTunnel: Tunnel, tunnelProperties: TunnelProperties, attributes: Attributes | undefined) {
695+
const newName = attributes?.label ?? tunnelProperties.name;
696+
enum MergedAttributeAction {
697+
None = 0,
698+
Fire = 1,
699+
Reopen = 2
700+
}
701+
let mergedAction = MergedAttributeAction.None;
702+
if (newName !== existingTunnel.name) {
703+
existingTunnel.name = newName;
704+
mergedAction = MergedAttributeAction.Fire;
705+
}
706+
// Source of existing tunnel wins so that original source is maintained
707+
if ((attributes?.protocol || (existingTunnel.protocol !== TunnelProtocol.Http)) && (attributes?.protocol !== existingTunnel.protocol)) {
708+
tunnelProperties.source = existingTunnel.source;
709+
mergedAction = MergedAttributeAction.Reopen;
710+
}
711+
// New privacy value wins
712+
if (tunnelProperties.privacy && (existingTunnel.privacy !== tunnelProperties.privacy)) {
713+
mergedAction = MergedAttributeAction.Reopen;
714+
}
715+
switch (mergedAction) {
716+
case MergedAttributeAction.Fire: {
681717
this._onForwardPort.fire();
718+
break;
682719
}
683-
if ((attributes?.protocol || (existingTunnel.protocol !== TunnelProtocol.Http)) && (attributes?.protocol !== existingTunnel.protocol)) {
684-
await this.close(existingTunnel.remoteHost, existingTunnel.remotePort);
685-
tunnelProperties.source = existingTunnel.source;
720+
case MergedAttributeAction.Reopen: {
721+
await this.close(existingTunnel.remoteHost, existingTunnel.remotePort, TunnelCloseReason.User);
686722
await this.forward(tunnelProperties, attributes);
687723
}
688-
return mapHasAddressLocalhostOrAllInterfaces(this.remoteTunnels, tunnelProperties.remote.host, tunnelProperties.remote.port);
689724
}
690725

691-
return undefined;
726+
return mapHasAddressLocalhostOrAllInterfaces(this.remoteTunnels, tunnelProperties.remote.host, tunnelProperties.remote.port);
692727
}
693728

694729
async name(host: string, port: number, name: string) {
@@ -705,9 +740,18 @@ export class TunnelModel extends Disposable {
705740
}
706741
}
707742

708-
async close(host: string, port: number): Promise<void> {
743+
async close(host: string, port: number, reason: TunnelCloseReason): Promise<void> {
744+
const key = makeAddress(host, port);
745+
const oldTunnel = this.forwarded.get(key)!;
746+
if (reason === TunnelCloseReason.AutoForwardEnd) {
747+
this.sessionCachedProperties.set(key, {
748+
local: oldTunnel.localPort,
749+
name: oldTunnel.name,
750+
privacy: oldTunnel.privacy,
751+
});
752+
}
709753
await this.tunnelService.closeTunnel(host, port);
710-
return this.onTunnelClosed({ host, port });
754+
return this.onTunnelClosed({ host, port }, reason);
711755
}
712756

713757
address(host: string, port: number): string | undefined {
@@ -929,7 +973,7 @@ export interface IRemoteExplorerService {
929973
setEditable(tunnelItem: ITunnelItem | undefined, editId: TunnelEditId, data: IEditableData | null): void;
930974
getEditableData(tunnelItem: ITunnelItem | undefined, editId?: TunnelEditId): IEditableData | undefined;
931975
forward(tunnelProperties: TunnelProperties, attributes?: Attributes | null): Promise<RemoteTunnel | undefined>;
932-
close(remote: { host: string; port: number }): Promise<void>;
976+
close(remote: { host: string; port: number }, reason: TunnelCloseReason): Promise<void>;
933977
setTunnelInformation(tunnelInformation: TunnelInformation | undefined): void;
934978
setCandidateFilter(filter: ((candidates: CandidatePort[]) => Promise<CandidatePort[]>) | undefined): IDisposable;
935979
onFoundNewCandidates(candidates: CandidatePort[]): void;
@@ -991,8 +1035,8 @@ class RemoteExplorerService implements IRemoteExplorerService {
9911035
return this.tunnelModel.forward(tunnelProperties, attributes);
9921036
}
9931037

994-
close(remote: { host: string; port: number }): Promise<void> {
995-
return this.tunnelModel.close(remote.host, remote.port);
1038+
close(remote: { host: string; port: number }, reason: TunnelCloseReason): Promise<void> {
1039+
return this.tunnelModel.close(remote.host, remote.port, reason);
9961040
}
9971041

9981042
setTunnelInformation(tunnelInformation: TunnelInformation | undefined): void {

0 commit comments

Comments
 (0)