diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index 274465c80a5..da102a4e079 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -123,9 +123,9 @@ export async function findPorts(tcp: string, tcp6: string, procSockets: string, const ports: CandidatePort[] = []; connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => { - const command = processMap[socketMap[socket].pid].cmd; - if (!knownExcludeCmdline(command)) { - ports.push({ host: ip, port, detail: processMap[socketMap[socket].pid].cmd, pid: socketMap[socket].pid }); + const command: string | undefined = processMap[socketMap[socket].pid]?.cmd; + if (command && !knownExcludeCmdline(command)) { + ports.push({ host: ip, port, detail: command, pid: socketMap[socket].pid }); } }); return ports; diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index 659e9435593..3e9e9616524 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -150,9 +150,9 @@ Registry.as(ConfigurationExtensions.Configuration) 'remote.portsAttributes': { type: 'object', patternProperties: { - '^\\d+(\\-\\d+)?$': { + '(^\\d+(\\-\\d+)?$)|(.+)': { type: 'object', - description: localize('remote.portsAttributes.port', "A port, or range of ports (ex. \"40000-55000\") that the attributes should apply to"), + description: localize('remote.portsAttributes.port', "A port, range of ports (ex. \"40000-55000\"), or regular expression (ex. \".+\\\\/server.js\"). For a port number or range, the attributes will apply to that port number or range of port numbers. Attributes which use a regular expression will apply to ports whose associated process command line matches the expression."), properties: { 'onAutoForward': { type: 'string', @@ -184,9 +184,9 @@ Registry.as(ConfigurationExtensions.Configuration) } } }, - markdownDescription: localize('remote.portsAttributes', "Set default properties that are applied when a specific port number is forwarded. For example:\n\n```\n\"3000\": {\n \"label\": \"Labeled Port\"\n},\n\"40000-55000\": {\n \"onAutoForward\": \"ignore\"\n}\n```"), - defaultSnippets: [{ body: { '${1:3000}': { label: '${2:My Port}', onAutoForward: 'notify' } } }], - errorMessage: localize('remote.portsAttributes.patternError', "Must be a port number or a range of port numbers"), + markdownDescription: localize('remote.portsAttributes', "Set default properties that are applied when a specific port number is forwarded. For example:\n\n```\n\"3000\": {\n \"label\": \"Labeled Port\"\n},\n\"40000-55000\": {\n \"onAutoForward\": \"ignore\"\n},\n\".+\\\\/server.js\": {\n \"onAutoForward\": \"openPreview\"\n}\n```"), + defaultSnippets: [{ body: { '${1:3000}': { label: '${2:My Port}', onAutoForward: 'openPreview' }, 'others': { onAutoForward: 'notify' } } }], + errorMessage: localize('remote.portsAttributes.patternError', "Must be a port number, range of port numbers, or regular expression."), additionalProperties: false } } diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 25949bb876a..8129d6102af 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -146,14 +146,18 @@ interface Attributes { elevateIfNeeded: boolean | undefined; } +interface PortRange { start: number, end: number } + interface PortAttributes extends Attributes { - port: number | { start: number, end: number }; + key: number | PortRange | RegExp; } export class PortsAttributes extends Disposable { private static SETTING = 'remote.portsAttributes'; private static RANGE = /^(\d+)\-(\d+)$/; + private static OTHERS = 'others'; private portsAttributes: PortAttributes[] = []; + private otherPortAttributes: Attributes | undefined; private _onDidChangeAttributes = new Emitter(); public readonly onDidChangeAttributes = this._onDidChangeAttributes.event; @@ -172,8 +176,8 @@ export class PortsAttributes extends Disposable { this._onDidChangeAttributes.fire(); } - getAttributes(port: number): Attributes | undefined { - let index = this.findNextIndex(port, this.portsAttributes, 0); + getAttributes(port: number, commandLine?: string): Attributes | undefined { + let index = this.findNextIndex(port, commandLine, this.portsAttributes, 0); const attributes: Attributes = { label: undefined, onAutoForward: undefined, @@ -181,31 +185,43 @@ export class PortsAttributes extends Disposable { }; while (index >= 0) { const found = this.portsAttributes[index]; - if (found.port === port) { + if (found.key === port) { attributes.onAutoForward = found.onAutoForward ?? attributes.onAutoForward; attributes.elevateIfNeeded = (found.elevateIfNeeded !== undefined) ? found.elevateIfNeeded : attributes.elevateIfNeeded; attributes.label = found.label ?? attributes.label; } else { - // It's a range, which means that if the attribute is already set, we keep it + // It's a range or regex, which means that if the attribute is already set, we keep it attributes.onAutoForward = attributes.onAutoForward ?? found.onAutoForward; attributes.elevateIfNeeded = (attributes.elevateIfNeeded !== undefined) ? attributes.elevateIfNeeded : found.elevateIfNeeded; attributes.label = attributes.label ?? found.label; } - index = this.findNextIndex(port, this.portsAttributes, index + 1); + index = this.findNextIndex(port, commandLine, this.portsAttributes, index + 1); } if (attributes.onAutoForward !== undefined || attributes.elevateIfNeeded !== undefined || attributes.label !== undefined) { return attributes; } - return undefined; + + // If we find no matches, then use the other port attributes. + return this.getOtherAttributes(); } - private findNextIndex(port: number, attributes: PortAttributes[], fromIndex: number): number { + private hasStartEnd(value: number | PortRange | RegExp): value is PortRange { + return ((value).start !== undefined) && ((value).end !== undefined); + } + + private findNextIndex(port: number, commandLine: string | undefined, attributes: PortAttributes[], fromIndex: number): number { if (fromIndex >= attributes.length) { return -1; } const sliced = attributes.slice(fromIndex); const foundIndex = sliced.findIndex((value) => { - return isNumber(value.port) ? (value.port === port) : (port >= value.port.start && port <= value.port.end); + if (isNumber(value.key)) { + return value.key === port; + } else if (this.hasStartEnd(value.key)) { + return port >= value.key.start && port <= value.key.end; + } else { + return commandLine ? value.key.test(commandLine) : -1; + } }); return foundIndex >= 0 ? foundIndex + fromIndex : -1; } @@ -217,31 +233,63 @@ export class PortsAttributes extends Disposable { } const attributes: PortAttributes[] = []; - for (let portNumber in settingValue) { - const setting = (settingValue)[portNumber]; - let port: number | { start: number, end: number } | undefined = undefined; - if (portNumber !== undefined) { - if (Number(portNumber)) { - port = Number(portNumber); - } else if (isString(portNumber) && PortsAttributes.RANGE.test(portNumber)) { - const match = (portNumber).match(PortsAttributes.RANGE); - port = { start: Number(match![1]), end: Number(match![2]) }; + for (let attributesKey in settingValue) { + if (attributesKey === undefined) { + continue; + } + const setting = (settingValue)[attributesKey]; + let key: number | { start: number, end: number } | RegExp | undefined = undefined; + if (Number(attributesKey)) { + key = Number(attributesKey); + } else if (attributesKey === PortsAttributes.OTHERS) { + this.otherPortAttributes = { + elevateIfNeeded: setting.elevateIfPrivileged, + onAutoForward: setting.onAutoForward, + label: setting.label + }; + } else if (isString(attributesKey)) { + if (PortsAttributes.RANGE.test(attributesKey)) { + const match = (attributesKey).match(PortsAttributes.RANGE); + key = { start: Number(match![1]), end: Number(match![2]) }; + } else { + const regTest: RegExp = RegExp(attributesKey); + if (regTest) { + key = regTest; + } } } - if (!port) { + if (!key) { continue; } attributes.push({ - port, + key: key, elevateIfNeeded: setting.elevateIfPrivileged, onAutoForward: setting.onAutoForward, label: setting.label }); } - attributes.sort((a, b) => { - return (isNumber(a.port) ? a.port : a.port.start) - (isNumber(b.port) ? b.port : b.port.start); + + return this.sortAttributes(attributes); + } + + private sortAttributes(attributes: PortAttributes[]): PortAttributes[] { + function getVal(item: PortAttributes, thisRef: PortsAttributes) { + if (isNumber(item.key)) { + return item.key; + } else if (thisRef.hasStartEnd(item.key)) { + return item.key.start; + } else { + return Number.MAX_VALUE; + } + } + + return attributes.sort((a, b) => { + return getVal(a, this) - getVal(b, this); }); - return attributes; + } + + private getOtherAttributes() { + return this.otherPortAttributes; } static providedActionToAction(providedAction: ProvidedOnAutoForward | undefined) { @@ -565,17 +613,6 @@ export class TunnelModel extends Disposable { } async getAttributes(ports: number[], checkProviders: boolean = true): Promise | undefined> { - const configAttributes: Map = new Map(); - ports.forEach(port => { - const attributes = this.configPortsAttributes.getAttributes(port); - if (attributes) { - configAttributes.set(port, attributes); - } - }); - if ((this.portAttributesProviders.length === 0) || !checkProviders) { - return (configAttributes.size > 0) ? configAttributes : undefined; - } - const matchingCandidates: Map = new Map(); const pidToPortsMapping: Map = new Map(); ports.forEach(port => { @@ -588,6 +625,18 @@ export class TunnelModel extends Disposable { pidToPortsMapping.get(matchingCandidate.pid)?.push(port); } }); + + const configAttributes: Map = new Map(); + ports.forEach(port => { + const attributes = this.configPortsAttributes.getAttributes(port, matchingCandidates.get(port)?.detail); + if (attributes) { + configAttributes.set(port, attributes); + } + }); + if ((this.portAttributesProviders.length === 0) || !checkProviders) { + return (configAttributes.size > 0) ? configAttributes : undefined; + } + // Group calls to provide attributes by pid. const allProviderResults = await Promise.all(flatten(this.portAttributesProviders.map(provider => { return Array.from(pidToPortsMapping.entries()).map(entry => {