Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache auto port properties #182081

Merged
merged 4 commits into from May 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/vs/platform/tunnel/common/tunnel.ts
Expand Up @@ -349,6 +349,9 @@ export abstract class AbstractTunnelService implements ITunnelService {
if (tunnel.tunnelRemoteHost !== remoteHost || tunnel.tunnelRemotePort !== remotePort) {
this.logService.warn('ForwardedPorts: (TunnelService) Created tunnel does not match requirements of requested tunnel. Host or port mismatch.');
}
if (privacy && tunnel.privacy !== privacy) {
this.logService.warn('ForwardedPorts: (TunnelService) Created tunnel does not match requirements of requested tunnel. Privacy mismatch.');
}
this._onTunnelOpened.fire(newTunnel);
return newTunnel;
});
Expand Down
6 changes: 3 additions & 3 deletions src/vs/workbench/api/browser/mainThreadTunnelService.ts
Expand Up @@ -7,7 +7,7 @@ import * as nls from 'vs/nls';
import { MainThreadTunnelServiceShape, MainContext, ExtHostContext, ExtHostTunnelServiceShape, CandidatePortSource, PortAttributesProviderSelector, TunnelDto } from 'vs/workbench/api/common/extHost.protocol';
import { TunnelDtoConverter } from 'vs/workbench/api/common/extHostTunnelService';
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
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';
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';
import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions, RemoteTunnel, ProvidedPortAttributes, PortAttributesProvider, TunnelProtocol } from 'vs/platform/tunnel/common/tunnel';
import { Disposable } from 'vs/base/common/lifecycle';
import type { TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver';
Expand Down Expand Up @@ -129,7 +129,7 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun
label: nls.localize('remote.tunnelsView.elevationButton', "Use Port {0} as Sudo...", tunnel.tunnelRemotePort),
run: async () => {
this.elevateionRetry = true;
await this.remoteExplorerService.close({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort });
await this.remoteExplorerService.close({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }, TunnelCloseReason.Other);
await this.remoteExplorerService.forward({
remote: tunnelOptions.remoteAddress,
local: tunnelOptions.localAddressPort,
Expand All @@ -146,7 +146,7 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun
}

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

async $getTunnels(): Promise<TunnelDescription[]> {
Expand Down
6 changes: 3 additions & 3 deletions src/vs/workbench/contrib/remote/browser/remoteExplorer.ts
Expand Up @@ -6,7 +6,7 @@ import * as nls from 'vs/nls';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { Extensions, IViewContainersRegistry, IViewsRegistry, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views';
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';
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';
import { forwardedPortsViewEnabled, ForwardPortAction, OpenPortInBrowserAction, TunnelPanel, TunnelPanelDescriptor, TunnelViewModel, OpenPortInPreviewAction, openPreviewEnabledContext } from 'vs/workbench/contrib/remote/browser/tunnelView';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
Expand Down Expand Up @@ -380,7 +380,7 @@ class OnAutoForwardedAction extends Disposable {
// Privileged ports are not on Windows, so it's ok to stick to just "sudo".
label: nls.localize('remote.tunnelsView.elevationButton', "Use Port {0} as Sudo...", tunnel.tunnelRemotePort),
run: async () => {
await this.remoteExplorerService.close({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort });
await this.remoteExplorerService.close({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }, TunnelCloseReason.Other);
const newTunnel = await this.remoteExplorerService.forward({
remote: { host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort },
local: tunnel.tunnelRemotePort,
Expand Down Expand Up @@ -643,7 +643,7 @@ class ProcAutomaticPortForwarding extends Disposable {
} else {
value = { host: forwardedValue.remoteHost, port: forwardedValue.remotePort };
}
await this.remoteExplorerService.close(value);
await this.remoteExplorerService.close(value, TunnelCloseReason.AutoForwardEnd);
removedPorts.push(value.port);
} else if (this.notifiedOnly.has(key)) {
this.notifiedOnly.delete(key);
Expand Down
10 changes: 5 additions & 5 deletions src/vs/workbench/contrib/remote/browser/tunnelView.ts
Expand Up @@ -24,7 +24,7 @@ import { ActionRunner, IAction } from 'vs/base/common/actions';
import { IMenuService, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions';
import { ILocalizedString } from 'vs/platform/action/common/action';
import { createAndFillInActionBarActions, createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IRemoteExplorerService, TunnelModel, makeAddress, TunnelType, ITunnelItem, Tunnel, TUNNEL_VIEW_ID, parseAddress, CandidatePort, TunnelEditId, mapHasAddressLocalhostOrAllInterfaces, Attributes, TunnelSource } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { IRemoteExplorerService, TunnelModel, makeAddress, TunnelType, ITunnelItem, Tunnel, TUNNEL_VIEW_ID, parseAddress, CandidatePort, TunnelEditId, mapHasAddressLocalhostOrAllInterfaces, Attributes, TunnelSource, TunnelCloseReason } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
Expand Down Expand Up @@ -1252,7 +1252,7 @@ namespace ClosePortAction {
if (!ports || ports.length === 0) {
return;
}
return Promise.all(ports.map(port => remoteExplorerService.close({ host: port.remoteHost, port: port.remotePort })));
return Promise.all(ports.map(port => remoteExplorerService.close({ host: port.remoteHost, port: port.remotePort }, TunnelCloseReason.User)));
};
}

Expand All @@ -1266,7 +1266,7 @@ namespace ClosePortAction {
const picks: QuickPickInput<QuickPickTunnel>[] = makeTunnelPicks(Array.from(remoteExplorerService.tunnelModel.forwarded.values()).filter(tunnel => tunnel.closeable), remoteExplorerService, tunnelService);
const result = await quickInputService.pick(picks, { placeHolder: nls.localize('remote.tunnel.closePlaceholder', "Choose a port to stop forwarding") });
if (result && result.tunnel) {
await remoteExplorerService.close({ host: result.tunnel.remoteHost, port: result.tunnel.remotePort });
await remoteExplorerService.close({ host: result.tunnel.remoteHost, port: result.tunnel.remotePort }, TunnelCloseReason.User);
} else if (result) {
await commandService.executeCommand(ForwardPortAction.COMMANDPALETTE_ID);
}
Expand Down Expand Up @@ -1468,7 +1468,7 @@ namespace ChangeLocalPortAction {
onFinish: async (value, success) => {
remoteExplorerService.setEditable(tunnelItem, TunnelEditId.LocalPort, null);
if (success) {
await remoteExplorerService.close({ host: tunnelItem.remoteHost, port: tunnelItem.remotePort });
await remoteExplorerService.close({ host: tunnelItem.remoteHost, port: tunnelItem.remotePort }, TunnelCloseReason.Other);
const numberValue = Number(value);
const newForward = await remoteExplorerService.forward({
remote: { host: tunnelItem.remoteHost, port: tunnelItem.remotePort },
Expand All @@ -1495,7 +1495,7 @@ namespace ChangeTunnelPrivacyAction {
return async (accessor, arg) => {
if (isITunnelItem(arg)) {
const remoteExplorerService = accessor.get(IRemoteExplorerService);
await remoteExplorerService.close({ host: arg.remoteHost, port: arg.remotePort });
await remoteExplorerService.close({ host: arg.remoteHost, port: arg.remotePort }, TunnelCloseReason.Other);
return remoteExplorerService.forward({
remote: { host: arg.remoteHost, port: arg.remotePort },
local: arg.localPort,
Expand Down
102 changes: 73 additions & 29 deletions src/vs/workbench/services/remote/common/remoteExplorerService.ts
Expand Up @@ -44,6 +44,12 @@ export enum TunnelType {
Add = 'Add'
}

export enum TunnelCloseReason {
Other = 'Other',
User = 'User',
AutoForwardEnd = 'AutoForwardEnd',
}

export interface ITunnelItem {
tunnelType: TunnelType;
remoteHost: string;
Expand Down Expand Up @@ -426,6 +432,7 @@ export class TunnelModel extends Disposable {
private restoreListener: IDisposable | undefined;
private knownPortsRestoreValue: string | undefined;
private unrestoredExtensionTunnels: Map<string, Tunnel> = new Map();
private sessionCachedProperties: Map<string, Partial<TunnelProperties>> = new Map();

private portAttributesProviders: PortAttributesProvider[] = [];

Expand Down Expand Up @@ -500,11 +507,11 @@ export class TunnelModel extends Disposable {
this._onForwardPort.fire(this.forwarded.get(key)!);
}));
this._register(this.tunnelService.onTunnelClosed(address => {
return this.onTunnelClosed(address);
return this.onTunnelClosed(address, TunnelCloseReason.Other);
}));
}

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

const key = makeAddress(tunnelProperties.remote.host, tunnelProperties.remote.port);
this.inProgress.set(key, true);
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);
tunnelProperties = this.mergeCachedAndUnrestoredProperties(key, tunnelProperties);

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);
if (tunnel && tunnel.localAddress) {
const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces<CandidatePort>(this._candidates ?? new Map(), tunnelProperties.remote.host, tunnelProperties.remote.port);
const protocol = (tunnel.protocol ?
Expand All @@ -658,37 +667,63 @@ export class TunnelModel extends Disposable {
await this.storeForwarded();
await this.showPortMismatchModalIfNeeded(tunnel, localPort, attributes);
this._onForwardPort.fire(newForward);
if (this.unrestoredExtensionTunnels.has(key)) {
const updateProps = this.unrestoredExtensionTunnels.get(key);
this.unrestoredExtensionTunnels.delete(key);
if (updateProps) {
tunnel = await this.forward({
remote: { host: newForward.remoteHost, port: newForward.remotePort },
local: newForward.localPort,
name: newForward.name,
privacy: updateProps.privacy,
elevateIfNeeded: true,
});
}
}
return tunnel;
}
this.inProgress.delete(key);
} else {
const newName = attributes?.label ?? tunnelProperties.name;
if (newName !== existingTunnel.name) {
existingTunnel.name = newName;
return this.mergeAttributesIntoExistingTunnel(existingTunnel, tunnelProperties, attributes);
}

return undefined;
}

private mergeCachedAndUnrestoredProperties(key: string, tunnelProperties: TunnelProperties): TunnelProperties {
const map = this.unrestoredExtensionTunnels.has(key) ? this.unrestoredExtensionTunnels : (this.sessionCachedProperties.has(key) ? this.sessionCachedProperties : undefined);
if (map) {
const updateProps = map.get(key)!;
map.delete(key);
if (updateProps) {
tunnelProperties.name = updateProps.name ?? tunnelProperties.name;
tunnelProperties.local = (('local' in updateProps) ? updateProps.local : (('localPort' in updateProps) ? updateProps.localPort : undefined)) ?? tunnelProperties.local;
tunnelProperties.privacy = updateProps.privacy ?? tunnelProperties.privacy;
}
}
return tunnelProperties;
}

private async mergeAttributesIntoExistingTunnel(existingTunnel: Tunnel, tunnelProperties: TunnelProperties, attributes: Attributes | undefined) {
const newName = attributes?.label ?? tunnelProperties.name;
enum MergedAttributeAction {
None = 0,
Fire = 1,
Reopen = 2
}
let mergedAction = MergedAttributeAction.None;
if (newName !== existingTunnel.name) {
existingTunnel.name = newName;
mergedAction = MergedAttributeAction.Fire;
}
// Source of existing tunnel wins so that original source is maintained
if ((attributes?.protocol || (existingTunnel.protocol !== TunnelProtocol.Http)) && (attributes?.protocol !== existingTunnel.protocol)) {
tunnelProperties.source = existingTunnel.source;
mergedAction = MergedAttributeAction.Reopen;
}
// New privacy value wins
if (tunnelProperties.privacy && (existingTunnel.privacy !== tunnelProperties.privacy)) {
mergedAction = MergedAttributeAction.Reopen;
}
switch (mergedAction) {
case MergedAttributeAction.Fire: {
this._onForwardPort.fire();
break;
}
if ((attributes?.protocol || (existingTunnel.protocol !== TunnelProtocol.Http)) && (attributes?.protocol !== existingTunnel.protocol)) {
await this.close(existingTunnel.remoteHost, existingTunnel.remotePort);
tunnelProperties.source = existingTunnel.source;
case MergedAttributeAction.Reopen: {
await this.close(existingTunnel.remoteHost, existingTunnel.remotePort, TunnelCloseReason.User);
await this.forward(tunnelProperties, attributes);
}
return mapHasAddressLocalhostOrAllInterfaces(this.remoteTunnels, tunnelProperties.remote.host, tunnelProperties.remote.port);
}

return undefined;
return mapHasAddressLocalhostOrAllInterfaces(this.remoteTunnels, tunnelProperties.remote.host, tunnelProperties.remote.port);
}

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

async close(host: string, port: number): Promise<void> {
async close(host: string, port: number, reason: TunnelCloseReason): Promise<void> {
const key = makeAddress(host, port);
const oldTunnel = this.forwarded.get(key)!;
if (reason === TunnelCloseReason.AutoForwardEnd) {
this.sessionCachedProperties.set(key, {
local: oldTunnel.localPort,
name: oldTunnel.name,
privacy: oldTunnel.privacy,
});
}
await this.tunnelService.closeTunnel(host, port);
return this.onTunnelClosed({ host, port });
return this.onTunnelClosed({ host, port }, reason);
}

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

close(remote: { host: string; port: number }): Promise<void> {
return this.tunnelModel.close(remote.host, remote.port);
close(remote: { host: string; port: number }, reason: TunnelCloseReason): Promise<void> {
return this.tunnelModel.close(remote.host, remote.port, reason);
}

setTunnelInformation(tunnelInformation: TunnelInformation | undefined): void {
Expand Down