From 20b19612917853c058632c0127e5170c9fa47bcf Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Mon, 8 Mar 2021 15:20:12 +0100 Subject: [PATCH 1/5] Move proxy resolution to vscode-proxy-agent (#117054) --- package.json | 2 +- remote/package.json | 2 +- remote/yarn.lock | 8 +- .../services/extensions/node/proxyAgent.ts | 477 ------------------ .../services/extensions/node/proxyResolver.ts | 32 +- yarn.lock | 8 +- 6 files changed, 26 insertions(+), 503 deletions(-) delete mode 100644 src/vs/workbench/services/extensions/node/proxyAgent.ts diff --git a/package.json b/package.json index 09b4905965b..4f3f4508f6f 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "tas-client-umd": "0.1.2", "v8-inspect-profiler": "^0.0.20", "vscode-oniguruma": "1.3.1", - "vscode-proxy-agent": "^0.5.2", + "vscode-proxy-agent": "^0.6.0", "vscode-regexpp": "^3.1.0", "vscode-ripgrep": "^1.11.1", "vscode-sqlite3": "4.0.10", diff --git a/remote/package.json b/remote/package.json index 05f9cec1ec5..22ac93ce46d 100644 --- a/remote/package.json +++ b/remote/package.json @@ -18,7 +18,7 @@ "spdlog": "^0.11.1", "tas-client-umd": "0.1.2", "vscode-oniguruma": "1.3.1", - "vscode-proxy-agent": "^0.5.2", + "vscode-proxy-agent": "^0.6.0", "vscode-regexpp": "^3.1.0", "vscode-ripgrep": "^1.11.1", "vscode-textmate": "5.2.0", diff --git a/remote/yarn.lock b/remote/yarn.lock index 295fc4327b9..8bb9d25675a 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -371,10 +371,10 @@ vscode-oniguruma@1.3.1: resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.3.1.tgz#e2383879c3485b19f533ec34efea9d7a2b14be8f" integrity sha512-gz6ZBofA7UXafVA+m2Yt2zHKgXC2qedArprIsHAPKByTkwq9l5y/izAGckqxYml7mSbYxTRTfdRwsFq3cwF4LQ== -vscode-proxy-agent@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/vscode-proxy-agent/-/vscode-proxy-agent-0.5.2.tgz#0c90d24d353957b841d741da7b2701e3f0a044c4" - integrity sha512-1cCNPxrWIrmUwS+1XGaXxkh3G1y7z2fpXl1sT74OZvELaryQWYb3NMxMLJJ4Q/CpPLEyuhp/bAN7nzHxxFcQ5Q== +vscode-proxy-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/vscode-proxy-agent/-/vscode-proxy-agent-0.6.0.tgz#3ea5c3c82f7abe945d690eb34b2c877cf5833f12" + integrity sha512-dvoLmEO/IxkbcNrRHH6ey8ITfvau4wDg01S+iAJ5Pq/FoAl2ZeE4cK5VEnQ2JHqM20kTLhyZfkjdHq6l7/T+xA== dependencies: debug "^3.1.0" http-proxy-agent "^2.1.0" diff --git a/src/vs/workbench/services/extensions/node/proxyAgent.ts b/src/vs/workbench/services/extensions/node/proxyAgent.ts deleted file mode 100644 index 574f94b5071..00000000000 --- a/src/vs/workbench/services/extensions/node/proxyAgent.ts +++ /dev/null @@ -1,477 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as http from 'http'; -import * as https from 'https'; -import * as tls from 'tls'; -import * as nodeurl from 'url'; -import * as os from 'os'; -import * as fs from 'fs'; -import * as cp from 'child_process'; -import { ProxyAgent } from 'vscode-proxy-agent'; - -export enum LogLevel { - Trace = 1, - Debug = 2, - Info = 3, - Warning = 4, - Error = 5, - Critical = 6, - Off = 7 -} - -export type ResolveProxyEvent = { - count: number; - duration: number; - errorCount: number; - cacheCount: number; - cacheSize: number; - cacheRolls: number; - envCount: number; - settingsCount: number; - localhostCount: number; - envNoProxyCount: number; - results: ConnectionResult[]; -}; - -interface ConnectionResult { - proxy: string; - connection: string; - code: string; - count: number; -} - -const maxCacheEntries = 5000; // Cache can grow twice that much due to 'oldCache'. - -export interface ProxyAgentParams { - resolveProxy(url: string): Promise; - getHttpProxySetting(): string | undefined; - log(level: LogLevel, message: string, ...args: any[]): void; - getLogLevel(): LogLevel; - proxyResolverTelemetry(event: ResolveProxyEvent): void; - useHostProxy: boolean; - env: NodeJS.ProcessEnv; -} - -export function setupProxyResolution(params: ProxyAgentParams) { - const { getHttpProxySetting, log, getLogLevel, proxyResolverTelemetry, useHostProxy, env } = params; - let envProxy = proxyFromConfigURL(env.https_proxy || env.HTTPS_PROXY || env.http_proxy || env.HTTP_PROXY); // Not standardized. - - let envNoProxy = noProxyFromEnv(env.no_proxy || env.NO_PROXY); // Not standardized. - - let cacheRolls = 0; - let oldCache = new Map(); - let cache = new Map(); - function getCacheKey(url: nodeurl.UrlWithStringQuery) { - // Expecting proxies to usually be the same per scheme://host:port. Assuming that for performance. - return nodeurl.format({ ...url, ...{ pathname: undefined, search: undefined, hash: undefined } }); - } - function getCachedProxy(key: string) { - let proxy = cache.get(key); - if (proxy) { - return proxy; - } - proxy = oldCache.get(key); - if (proxy) { - oldCache.delete(key); - cacheProxy(key, proxy); - } - return proxy; - } - function cacheProxy(key: string, proxy: string) { - cache.set(key, proxy); - if (cache.size >= maxCacheEntries) { - oldCache = cache; - cache = new Map(); - cacheRolls++; - log(LogLevel.Debug, 'ProxyResolver#cacheProxy cacheRolls', cacheRolls); - } - } - - let timeout: NodeJS.Timer | undefined; - let count = 0; - let duration = 0; - let errorCount = 0; - let cacheCount = 0; - let envCount = 0; - let settingsCount = 0; - let localhostCount = 0; - let envNoProxyCount = 0; - let results: ConnectionResult[] = []; - function logEvent() { - timeout = undefined; - proxyResolverTelemetry({ count, duration, errorCount, cacheCount, cacheSize: cache.size, cacheRolls, envCount, settingsCount, localhostCount, envNoProxyCount, results }); - count = duration = errorCount = cacheCount = envCount = settingsCount = localhostCount = envNoProxyCount = 0; - results = []; - } - - function resolveProxy(flags: { useProxySettings: boolean, useSystemCertificates: boolean }, req: http.ClientRequest, opts: http.RequestOptions, url: string, callback: (proxy?: string) => void) { - if (!timeout) { - timeout = setTimeout(logEvent, 10 * 60 * 1000); - } - - const stackText = getLogLevel() === LogLevel.Trace ? '\n' + new Error('Error for stack trace').stack : ''; - - useSystemCertificates(params, flags.useSystemCertificates, opts, () => { - useProxySettings(useHostProxy, flags.useProxySettings, req, opts, url, stackText, callback); - }); - } - - function useProxySettings(useHostProxy: boolean, useProxySettings: boolean, req: http.ClientRequest, opts: http.RequestOptions, url: string, stackText: string, callback: (proxy?: string) => void) { - - if (!useProxySettings) { - callback('DIRECT'); - return; - } - - const parsedUrl = nodeurl.parse(url); // Coming from Node's URL, sticking with that. - - const hostname = parsedUrl.hostname; - if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '::ffff:127.0.0.1') { - localhostCount++; - callback('DIRECT'); - log(LogLevel.Debug, 'ProxyResolver#resolveProxy localhost', url, 'DIRECT', stackText); - return; - } - - if (typeof hostname === 'string' && envNoProxy(hostname, String(parsedUrl.port || (opts.agent).defaultPort))) { - envNoProxyCount++; - callback('DIRECT'); - log(LogLevel.Debug, 'ProxyResolver#resolveProxy envNoProxy', url, 'DIRECT', stackText); - return; - } - - let settingsProxy = proxyFromConfigURL(getHttpProxySetting()); - if (settingsProxy) { - settingsCount++; - callback(settingsProxy); - log(LogLevel.Debug, 'ProxyResolver#resolveProxy settings', url, settingsProxy, stackText); - return; - } - - if (envProxy) { - envCount++; - callback(envProxy); - log(LogLevel.Debug, 'ProxyResolver#resolveProxy env', url, envProxy, stackText); - return; - } - - const key = getCacheKey(parsedUrl); - const proxy = getCachedProxy(key); - if (proxy) { - cacheCount++; - collectResult(results, proxy, parsedUrl.protocol === 'https:' ? 'HTTPS' : 'HTTP', req); - callback(proxy); - log(LogLevel.Debug, 'ProxyResolver#resolveProxy cached', url, proxy, stackText); - return; - } - - if (!useHostProxy) { - callback('DIRECT'); - log(LogLevel.Debug, 'ProxyResolver#resolveProxy unconfigured', url, 'DIRECT', stackText); - return; - } - - const start = Date.now(); - params.resolveProxy(url) // Use full URL to ensure it is an actually used one. - .then(proxy => { - if (proxy) { - cacheProxy(key, proxy); - collectResult(results, proxy, parsedUrl.protocol === 'https:' ? 'HTTPS' : 'HTTP', req); - } - callback(proxy); - log(LogLevel.Debug, 'ProxyResolver#resolveProxy', url, proxy, stackText); - }).then(() => { - count++; - duration = Date.now() - start + duration; - }, err => { - errorCount++; - callback(); - log(LogLevel.Error, 'ProxyResolver#resolveProxy', toErrorMessage(err), stackText); - }); - } - - return resolveProxy; -} - -function collectResult(results: ConnectionResult[], resolveProxy: string, connection: string, req: http.ClientRequest) { - const proxy = resolveProxy ? String(resolveProxy).trim().split(/\s+/, 1)[0] : 'EMPTY'; - req.on('response', res => { - const code = `HTTP_${res.statusCode}`; - const result = findOrCreateResult(results, proxy, connection, code); - result.count++; - }); - req.on('error', err => { - const code = err && typeof (err).code === 'string' && (err).code || 'UNKNOWN_ERROR'; - const result = findOrCreateResult(results, proxy, connection, code); - result.count++; - }); -} - -function findOrCreateResult(results: ConnectionResult[], proxy: string, connection: string, code: string): ConnectionResult { - for (const result of results) { - if (result.proxy === proxy && result.connection === connection && result.code === code) { - return result; - } - } - const result = { proxy, connection, code, count: 0 }; - results.push(result); - return result; -} - -function proxyFromConfigURL(configURL: string | undefined) { - if (!configURL) { - return undefined; - } - const url = (configURL || '').trim(); - const i = url.indexOf('://'); - if (i === -1) { - return undefined; - } - const scheme = url.substr(0, i).toLowerCase(); - const proxy = url.substr(i + 3); - if (scheme === 'http') { - return 'PROXY ' + proxy; - } else if (scheme === 'https') { - return 'HTTPS ' + proxy; - } else if (scheme === 'socks') { - return 'SOCKS ' + proxy; - } - return undefined; -} - -function noProxyFromEnv(envValue?: string) { - const value = (envValue || '') - .trim() - .toLowerCase(); - - if (value === '*') { - return () => true; - } - - const filters = value - .split(',') - .map(s => s.trim().split(':', 2)) - .map(([name, port]) => ({ name, port })) - .filter(filter => !!filter.name) - .map(({ name, port }) => { - const domain = name[0] === '.' ? name : `.${name}`; - return { domain, port }; - }); - if (!filters.length) { - return () => false; - } - return (hostname: string, port: string) => filters.some(({ domain, port: filterPort }) => { - return `.${hostname.toLowerCase()}`.endsWith(domain) && (!filterPort || port === filterPort); - }); -} - -export function patches(originals: typeof http | typeof https, resolveProxy: ReturnType, proxySetting: { config: string }, certSetting: { config: boolean }, onRequest: boolean) { - return { - get: patch(originals.get), - request: patch(originals.request) - }; - - function patch(original: typeof http.get) { - function patched(url?: string | URL | null, options?: http.RequestOptions | null, callback?: (res: http.IncomingMessage) => void): http.ClientRequest { - if (typeof url !== 'string' && !(url && (url).searchParams)) { - callback = options; - options = url; - url = null; - } - if (typeof options === 'function') { - callback = options; - options = null; - } - options = options || {}; - - if (options.socketPath) { - return original.apply(null, arguments as any); - } - - const originalAgent = options.agent; - if (originalAgent === true) { - throw new Error('Unexpected agent option: true'); - } - const optionsPatched = originalAgent instanceof ProxyAgent; - const config = onRequest && ((options)._vscodeProxySupport || /* LS */ (options)._vscodeSystemProxy) || proxySetting.config; - const useProxySettings = !optionsPatched && (config === 'override' || config === 'on' && originalAgent === undefined); - const useSystemCertificates = !optionsPatched && certSetting.config && originals === https && !(options as https.RequestOptions).ca; - - if (useProxySettings || useSystemCertificates) { - if (url) { - const parsed = typeof url === 'string' ? new nodeurl.URL(url) : url; - const urlOptions = { - protocol: parsed.protocol, - hostname: parsed.hostname.lastIndexOf('[', 0) === 0 ? parsed.hostname.slice(1, -1) : parsed.hostname, - port: parsed.port, - path: `${parsed.pathname}${parsed.search}` - }; - if (parsed.username || parsed.password) { - options.auth = `${parsed.username}:${parsed.password}`; - } - options = { ...urlOptions, ...options }; - } else { - options = { ...options }; - } - options.agent = new ProxyAgent({ - resolveProxy: resolveProxy.bind(undefined, { useProxySettings, useSystemCertificates }), - defaultPort: originals === https ? 443 : 80, - originalAgent - }); - return original(options, callback); - } - - return original.apply(null, arguments as any); - } - return patched; - } -} - -export function tlsPatches(originals: typeof tls) { - return { - createSecureContext: patch(originals.createSecureContext) - }; - - function patch(original: typeof tls.createSecureContext): typeof tls.createSecureContext { - return function (details?: tls.SecureContextOptions): ReturnType { - const context = original.apply(null, arguments as any); - const certs = (details as any)._vscodeAdditionalCaCerts; - if (certs) { - for (const cert of certs) { - context.context.addCACert(cert); - } - } - return context; - }; - } -} - -function useSystemCertificates(params: ProxyAgentParams, useSystemCertificates: boolean, opts: http.RequestOptions, callback: () => void) { - if (useSystemCertificates) { - getCaCertificates(params) - .then(caCertificates => { - if (caCertificates) { - if (caCertificates.append) { - (opts as any)._vscodeAdditionalCaCerts = caCertificates.certs; - } else { - (opts as https.RequestOptions).ca = caCertificates.certs; - } - } - callback(); - }) - .catch(err => { - params.log(LogLevel.Error, 'ProxyResolver#useSystemCertificates', toErrorMessage(err)); - }); - } else { - callback(); - } -} - -let _caCertificates: ReturnType | Promise; -async function getCaCertificates({ log }: ProxyAgentParams) { - if (!_caCertificates) { - _caCertificates = readCaCertificates() - .then(res => { - log(LogLevel.Debug, 'ProxyResolver#getCaCertificates count', res && res.certs.length); - return res && res.certs.length ? res : undefined; - }) - .catch(err => { - log(LogLevel.Error, 'ProxyResolver#getCaCertificates error', toErrorMessage(err)); - return undefined; - }); - } - return _caCertificates; -} - -async function readCaCertificates() { - if (process.platform === 'win32') { - return readWindowsCaCertificates(); - } - if (process.platform === 'darwin') { - return readMacCaCertificates(); - } - if (process.platform === 'linux') { - return readLinuxCaCertificates(); - } - return undefined; -} - -async function readWindowsCaCertificates() { - // @ts-ignore Windows only - const winCA = await import('vscode-windows-ca-certs'); - - let ders: any[] = []; - const store = new winCA.Crypt32(); - try { - let der: any; - while (der = store.next()) { - ders.push(der); - } - } finally { - store.done(); - } - - const certs = new Set(ders.map(derToPem)); - return { - certs: Array.from(certs), - append: true - }; -} - -async function readMacCaCertificates() { - const stdout = await new Promise((resolve, reject) => { - const child = cp.spawn('/usr/bin/security', ['find-certificate', '-a', '-p']); - const stdout: string[] = []; - child.stdout.setEncoding('utf8'); - child.stdout.on('data', str => stdout.push(str)); - child.on('error', reject); - child.on('exit', code => code ? reject(code) : resolve(stdout.join(''))); - }); - const certs = new Set(stdout.split(/(?=-----BEGIN CERTIFICATE-----)/g) - .filter(pem => !!pem.length)); - return { - certs: Array.from(certs), - append: true - }; -} - -const linuxCaCertificatePaths = [ - '/etc/ssl/certs/ca-certificates.crt', - '/etc/ssl/certs/ca-bundle.crt', -]; - -async function readLinuxCaCertificates() { - for (const certPath of linuxCaCertificatePaths) { - try { - const content = await fs.promises.readFile(certPath, { encoding: 'utf8' }); - const certs = new Set(content.split(/(?=-----BEGIN CERTIFICATE-----)/g) - .filter(pem => !!pem.length)); - return { - certs: Array.from(certs), - append: false - }; - } catch (err) { - if (err.code !== 'ENOENT') { - throw err; - } - } - } - return undefined; -} - -function derToPem(blob: Buffer) { - const lines = ['-----BEGIN CERTIFICATE-----']; - const der = blob.toString('base64'); - for (let i = 0; i < der.length; i += 64) { - lines.push(der.substr(i, 64)); - } - lines.push('-----END CERTIFICATE-----', ''); - return lines.join(os.EOL); -} - -function toErrorMessage(err: any) { - return err && (err.stack || err.message) || String(err); -} diff --git a/src/vs/workbench/services/extensions/node/proxyResolver.ts b/src/vs/workbench/services/extensions/node/proxyResolver.ts index 68160c49e15..920bfd4beb7 100644 --- a/src/vs/workbench/services/extensions/node/proxyResolver.ts +++ b/src/vs/workbench/services/extensions/node/proxyResolver.ts @@ -14,7 +14,7 @@ import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionS import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { LogLevel, patches, ResolveProxyEvent, setupProxyResolution, tlsPatches } from 'vs/workbench/services/extensions/node/proxyAgent'; +import { LogLevel, createHttpPatch, ProxyResolveEvent, createProxyResolver, createTlsPatch } from 'vscode-proxy-agent'; export function connectProxyResolver( extHostWorkspace: IExtHostWorkspaceProvider, @@ -26,7 +26,7 @@ export function connectProxyResolver( ) { const useHostProxy = initData.environment.useHostProxy; const doUseHostProxy = typeof useHostProxy === 'boolean' ? useHostProxy : !initData.remote.isRemote; - const resolveProxy = setupProxyResolution({ + const resolveProxy = createProxyResolver({ resolveProxy: url => extHostWorkspace.resolveProxy(url), getHttpProxySetting: () => configProvider.getConfiguration('http').get('proxy'), log: (level, message, ...args) => { @@ -46,7 +46,7 @@ export function connectProxyResolver( } }, getLogLevel: () => extHostLogService.getLevel(), - proxyResolverTelemetry: event => { + proxyResolveTelemetry: event => { type ResolveProxyClassification = { count: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true }; duration: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true }; @@ -60,7 +60,7 @@ export function connectProxyResolver( envNoProxyCount: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true }; results: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; }; - mainThreadTelemetry.$publicLog2('resolveProxy', event); + mainThreadTelemetry.$publicLog2('resolveProxy', event); }, useHostProxy: doUseHostProxy, env: process.env, @@ -69,7 +69,7 @@ export function connectProxyResolver( return configureModuleLoading(extensionService, lookup); } -function createPatchedModules(configProvider: ExtHostConfigProvider, resolveProxy: ReturnType) { +function createPatchedModules(configProvider: ExtHostConfigProvider, resolveProxy: ReturnType) { const proxySetting = { config: configProvider.getConfiguration('http') .get('proxySupport') || 'off' @@ -89,20 +89,20 @@ function createPatchedModules(configProvider: ExtHostConfigProvider, resolveProx return { http: { - off: Object.assign({}, http, patches(http, resolveProxy, { config: 'off' }, certSetting, true)), - on: Object.assign({}, http, patches(http, resolveProxy, { config: 'on' }, certSetting, true)), - override: Object.assign({}, http, patches(http, resolveProxy, { config: 'override' }, certSetting, true)), - onRequest: Object.assign({}, http, patches(http, resolveProxy, proxySetting, certSetting, true)), - default: Object.assign(http, patches(http, resolveProxy, proxySetting, certSetting, false)) // run last + off: Object.assign({}, http, createHttpPatch(http, resolveProxy, { config: 'off' }, certSetting, true)), + on: Object.assign({}, http, createHttpPatch(http, resolveProxy, { config: 'on' }, certSetting, true)), + override: Object.assign({}, http, createHttpPatch(http, resolveProxy, { config: 'override' }, certSetting, true)), + onRequest: Object.assign({}, http, createHttpPatch(http, resolveProxy, proxySetting, certSetting, true)), + default: Object.assign(http, createHttpPatch(http, resolveProxy, proxySetting, certSetting, false)) // run last } as Record, https: { - off: Object.assign({}, https, patches(https, resolveProxy, { config: 'off' }, certSetting, true)), - on: Object.assign({}, https, patches(https, resolveProxy, { config: 'on' }, certSetting, true)), - override: Object.assign({}, https, patches(https, resolveProxy, { config: 'override' }, certSetting, true)), - onRequest: Object.assign({}, https, patches(https, resolveProxy, proxySetting, certSetting, true)), - default: Object.assign(https, patches(https, resolveProxy, proxySetting, certSetting, false)) // run last + off: Object.assign({}, https, createHttpPatch(https, resolveProxy, { config: 'off' }, certSetting, true)), + on: Object.assign({}, https, createHttpPatch(https, resolveProxy, { config: 'on' }, certSetting, true)), + override: Object.assign({}, https, createHttpPatch(https, resolveProxy, { config: 'override' }, certSetting, true)), + onRequest: Object.assign({}, https, createHttpPatch(https, resolveProxy, proxySetting, certSetting, true)), + default: Object.assign(https, createHttpPatch(https, resolveProxy, proxySetting, certSetting, false)) // run last } as Record, - tls: Object.assign(tls, tlsPatches(tls)) + tls: Object.assign(tls, createTlsPatch(tls)) }; } diff --git a/yarn.lock b/yarn.lock index a51a9cd5467..fc81f57ef3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10096,10 +10096,10 @@ vscode-oniguruma@1.3.1: resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.3.1.tgz#e2383879c3485b19f533ec34efea9d7a2b14be8f" integrity sha512-gz6ZBofA7UXafVA+m2Yt2zHKgXC2qedArprIsHAPKByTkwq9l5y/izAGckqxYml7mSbYxTRTfdRwsFq3cwF4LQ== -vscode-proxy-agent@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/vscode-proxy-agent/-/vscode-proxy-agent-0.5.2.tgz#0c90d24d353957b841d741da7b2701e3f0a044c4" - integrity sha512-1cCNPxrWIrmUwS+1XGaXxkh3G1y7z2fpXl1sT74OZvELaryQWYb3NMxMLJJ4Q/CpPLEyuhp/bAN7nzHxxFcQ5Q== +vscode-proxy-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/vscode-proxy-agent/-/vscode-proxy-agent-0.6.0.tgz#3ea5c3c82f7abe945d690eb34b2c877cf5833f12" + integrity sha512-dvoLmEO/IxkbcNrRHH6ey8ITfvau4wDg01S+iAJ5Pq/FoAl2ZeE4cK5VEnQ2JHqM20kTLhyZfkjdHq6l7/T+xA== dependencies: debug "^3.1.0" http-proxy-agent "^2.1.0" From 9c78fa40ca7ab6b58d0ab5cd33858a39a2b7102d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 8 Mar 2021 15:22:18 +0100 Subject: [PATCH 2/5] skip failing test (#118443) --- test/smoke/src/areas/extensions/extensions.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/smoke/src/areas/extensions/extensions.test.ts b/test/smoke/src/areas/extensions/extensions.test.ts index 72a3460d8fe..1abdd6ee13c 100644 --- a/test/smoke/src/areas/extensions/extensions.test.ts +++ b/test/smoke/src/areas/extensions/extensions.test.ts @@ -10,7 +10,7 @@ export function setup() { it(`install and activate vscode-smoketest-check extension`, async function () { const app = this.app as Application; - if (app.quality === Quality.Dev) { + if (app.quality === Quality.Dev || app.web /* https://github.com/microsoft/vscode/issues/118443 */) { this.skip(); return; } From 06044789bfaa48f87904d59cc65ee0011d698a2f Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 8 Mar 2021 15:45:32 +0100 Subject: [PATCH 3/5] API proposal for PortAttributesProvider (#118446) Part of #115616 --- src/vs/platform/remote/common/tunnel.ts | 18 ++++ src/vs/vscode.proposed.d.ts | 33 +++++++ .../api/browser/mainThreadTunnelService.ts | 41 ++++++++- .../workbench/api/common/extHost.api.impl.ts | 5 + .../workbench/api/common/extHost.protocol.ts | 10 +- .../api/common/extHostTunnelService.ts | 11 ++- src/vs/workbench/api/common/extHostTypes.ts | 8 ++ .../api/node/extHostTunnelService.ts | 43 ++++++++- .../contrib/remote/browser/remoteExplorer.ts | 20 ++-- .../remote/common/remoteExplorerService.ts | 91 +++++++++++++++++-- 10 files changed, 255 insertions(+), 25 deletions(-) diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts index 0be0c057d96..080b647f741 100644 --- a/src/vs/platform/remote/common/tunnel.ts +++ b/src/vs/platform/remote/common/tunnel.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { isWindows, OperatingSystem } from 'vs/base/common/platform'; @@ -42,6 +43,23 @@ export interface ITunnelProvider { forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined; } +export enum ProvidedOnAutoForward { + Notify = 1, + OpenBrowser = 2, + OpenPreview = 3, + Silent = 4, + Ignore = 5 +} + +export interface ProvidedPortAttributes { + port: number; + autoForwardAction: ProvidedOnAutoForward; +} + +export interface PortAttributesProvider { + providePortAttributes(ports: number[], pid: number | undefined, commandLine: string | undefined, token: CancellationToken): Promise; +} + export interface ITunnel { remoteAddress: { port: number, host: string }; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 3668fc0cf0b..91c1d4fadb4 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2846,4 +2846,37 @@ declare module 'vscode' { readonly triggerKind: CodeActionTriggerKind; } //#endregion + + //#region https://github.com/microsoft/vscode/issues/115616 @alexr00 + export enum PortAutoForwardAction { + Notify = 1, + OpenBrowser = 2, + OpenPreview = 3, + Silent = 4, + Ignore = 5 + } + + export interface PortAttributes { + port: number; + autoForwardAction: PortAutoForwardAction + } + + export interface PortAttributesProvider { + providePortAttributes(ports: number[], pid: number | undefined, commandLine: string | undefined, token: CancellationToken): ProviderResult; + } + + export namespace workspace { + /** + * If your extension listens on ports, consider registering a PortAttributesProvider to provide information + * about the ports. For example, a debug extension may know about debug ports in it's debuggee. By providing + * this information with a PortAttributesProvider the extension can tell VS Code that these ports should be + * ignored, since they don't need to be user facing. + * + * @param portSelector If registerPortAttributesProvider is called after you start your process then you may already + * know the range of ports or the pid of your process. + * @param provider The PortAttributesProvider + */ + export function registerPortAttributesProvider(portSelector: { pid?: number, portRange?: [number, number] }, provider: PortAttributesProvider): Disposable; + } + //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index e58a00c5f2e..d3a86cfd85e 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -4,22 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { MainThreadTunnelServiceShape, IExtHostContext, MainContext, ExtHostContext, ExtHostTunnelServiceShape, CandidatePortSource } from 'vs/workbench/api/common/extHost.protocol'; +import { MainThreadTunnelServiceShape, IExtHostContext, MainContext, ExtHostContext, ExtHostTunnelServiceShape, CandidatePortSource, PortAttributesProviderSelector } from 'vs/workbench/api/common/extHost.protocol'; import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { CandidatePort, IRemoteExplorerService, makeAddress, PORT_AUTO_FORWARD_SETTING, PORT_AUTO_SOURCE_SETTING, PORT_AUTO_SOURCE_SETTING_OUTPUT } from 'vs/workbench/services/remote/common/remoteExplorerService'; -import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions, RemoteTunnel, isPortPrivileged } from 'vs/platform/remote/common/tunnel'; +import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions, RemoteTunnel, isPortPrivileged, ProvidedPortAttributes, PortAttributesProvider } from 'vs/platform/remote/common/tunnel'; import { Disposable } from 'vs/base/common/lifecycle'; import type { TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { CancellationToken } from 'vs/base/common/cancellation'; @extHostNamedCustomer(MainContext.MainThreadTunnelService) -export class MainThreadTunnelService extends Disposable implements MainThreadTunnelServiceShape { +export class MainThreadTunnelService extends Disposable implements MainThreadTunnelServiceShape, PortAttributesProvider { private readonly _proxy: ExtHostTunnelServiceShape; private elevateionRetry: boolean = false; + private portsAttributesProviders: Map = new Map(); constructor( extHostContext: IExtHostContext, @@ -50,6 +52,39 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun })); } + private _alreadyRegistered: boolean = false; + async $registerPortsAttributesProvider(selector: PortAttributesProviderSelector, providerHandle: number): Promise { + this.portsAttributesProviders.set(providerHandle, selector); + if (!this._alreadyRegistered) { + this.remoteExplorerService.tunnelModel.addAttributesProvider(this); + this._alreadyRegistered = true; + } + } + + async $unregisterPortsAttributesProvider(providerHandle: number): Promise { + this.portsAttributesProviders.delete(providerHandle); + } + + async providePortAttributes(ports: number[], pid: number | undefined, commandLine: string | undefined, token: CancellationToken): Promise { + if (this.portsAttributesProviders.size === 0) { + return []; + } + + // Check all the selectors to make sure it's worth going to the extension host. + const appropriateHandles = Array.from(this.portsAttributesProviders.entries()).filter(entry => { + const selector = entry[1]; + const portRange = selector.portRange; + const portInRange = portRange ? ports.some(port => portRange[0] <= port && port < portRange[1]) : true; + const pidMatches = !selector.pid || (selector.pid === pid); + return portInRange || pidMatches; + }).map(entry => entry[0]); + + if (appropriateHandles.length === 0) { + return []; + } + return this._proxy.$providePortAttributes(appropriateHandles, ports, pid, commandLine, token); + } + async $openTunnel(tunnelOptions: TunnelOptions, source: string): Promise { const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localAddressPort, tunnelOptions.label, source, false); if (tunnel) { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 7a338648351..bc40f98e5b6 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -901,6 +901,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostTunnelService.onDidChangeTunnels(listener, thisArg, disposables); }, + registerPortAttributesProvider: (portSelector: { pid?: number, portRange?: [number, number] }, provider: vscode.PortAttributesProvider) => { + checkProposedApiEnabled(extension); + return extHostTunnelService.registerPortsAttributesProvider(portSelector, provider); + }, registerTimelineProvider: (scheme: string | string[], provider: vscode.TimelineProvider) => { checkProposedApiEnabled(extension); return extHostTimeline.registerTimelineProvider(scheme, provider, extension.identifier, extHostCommands.converter); @@ -1173,6 +1177,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I MarkdownString: extHostTypes.MarkdownString, OverviewRulerLane: OverviewRulerLane, ParameterInformation: extHostTypes.ParameterInformation, + PortAutoForwardAction: extHostTypes.PortAutoForwardAction, Position: extHostTypes.Position, ProcessExecution: extHostTypes.ProcessExecution, ProgressLocation: extHostTypes.ProgressLocation, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index dce6fdc93d8..297d95919fb 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -47,7 +47,7 @@ import * as search from 'vs/workbench/services/search/common/search'; import { EditorGroupColumn, SaveReason } from 'vs/workbench/common/editor'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; -import { TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions } from 'vs/platform/remote/common/tunnel'; +import { TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions, ProvidedPortAttributes } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; import { NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEventDto, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, TransientMetadata, INotebookCellStatusBarEntry, ICellRange, INotebookDecorationRenderOptions, INotebookExclusiveDocumentFilter, IOutputDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -1048,6 +1048,11 @@ export enum CandidatePortSource { Output = 2 } +export interface PortAttributesProviderSelector { + pid?: number; + portRange?: [number, number]; +} + export interface MainThreadTunnelServiceShape extends IDisposable { $openTunnel(tunnelOptions: TunnelOptions, source: string | undefined): Promise; $closeTunnel(remote: { host: string, port: number }): Promise; @@ -1057,6 +1062,8 @@ export interface MainThreadTunnelServiceShape extends IDisposable { $setCandidateFilter(): Promise; $onFoundNewCandidates(candidates: { host: string, port: number, detail: string }[]): Promise; $setCandidatePortSource(source: CandidatePortSource): Promise; + $registerPortsAttributesProvider(selector: PortAttributesProviderSelector, providerHandle: number): Promise; + $unregisterPortsAttributesProvider(providerHandle: number): Promise; } export interface MainThreadTimelineShape extends IDisposable { @@ -1885,6 +1892,7 @@ export interface ExtHostTunnelServiceShape { $onDidTunnelsChange(): Promise; $registerCandidateFinder(enable: boolean): Promise; $applyCandidateFilter(candidates: CandidatePort[]): Promise; + $providePortAttributes(handles: number[], ports: number[], pid: number | undefined, commandline: string | undefined, cancellationToken: CancellationToken): Promise; } export interface ExtHostTimelineShape { diff --git a/src/vs/workbench/api/common/extHostTunnelService.ts b/src/vs/workbench/api/common/extHostTunnelService.ts index 721cbaba449..ae7bad01660 100644 --- a/src/vs/workbench/api/common/extHostTunnelService.ts +++ b/src/vs/workbench/api/common/extHostTunnelService.ts @@ -6,7 +6,7 @@ import { ExtHostTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import * as vscode from 'vscode'; -import { RemoteTunnel, TunnelCreationOptions, TunnelOptions } from 'vs/platform/remote/common/tunnel'; +import { ProvidedPortAttributes, RemoteTunnel, TunnelCreationOptions, TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; @@ -46,6 +46,7 @@ export interface IExtHostTunnelService extends ExtHostTunnelServiceShape { getTunnels(): Promise; onDidChangeTunnels: vscode.Event; setTunnelExtensionFunctions(provider: vscode.RemoteAuthorityResolver | undefined): Promise; + registerPortsAttributesProvider(portSelector: { pid?: number, portRange?: [number, number] }, provider: vscode.PortAttributesProvider): IDisposable; } export const IExtHostTunnelService = createDecorator('IExtHostTunnelService'); @@ -71,6 +72,14 @@ export class ExtHostTunnelService implements IExtHostTunnelService { async setTunnelExtensionFunctions(provider: vscode.RemoteAuthorityResolver | undefined): Promise { return { dispose: () => { } }; } + registerPortsAttributesProvider(portSelector: { pid?: number, portRange?: [number, number] }, provider: vscode.PortAttributesProvider) { + return { dispose: () => { } }; + } + + async $providePortAttributes(handles: number[], ports: number[], pid: number | undefined, commandline: string | undefined, cancellationToken: vscode.CancellationToken): Promise { + return []; + } + async $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise { return undefined; } async $closeTunnel(remote: { host: string, port: number }): Promise { } async $onDidTunnelsChange(): Promise { } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index bff7626143c..418dbea3687 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3369,3 +3369,11 @@ export enum WorkspaceTrustState { Trusted = 1, Unknown = 2 } + +export enum PortAutoForwardAction { + Notify = 1, + OpenBrowser = 2, + OpenPreview = 3, + Silent = 4, + Ignore = 5 +} diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index 4c2be0c2599..274465c80a5 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MainThreadTunnelServiceShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; +import { MainThreadTunnelServiceShape, MainContext, PortAttributesProviderSelector } from 'vs/workbench/api/common/extHost.protocol'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import type * as vscode from 'vscode'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -13,14 +13,16 @@ import { exec } from 'child_process'; import * as resources from 'vs/base/common/resources'; import * as fs from 'fs'; import * as pfs from 'vs/base/node/pfs'; +import * as types from 'vs/workbench/api/common/extHostTypes'; import { isLinux } from 'vs/base/common/platform'; import { IExtHostTunnelService, TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { Event, Emitter } from 'vs/base/common/event'; -import { TunnelOptions, TunnelCreationOptions } from 'vs/platform/remote/common/tunnel'; +import { TunnelOptions, TunnelCreationOptions, ProvidedPortAttributes, ProvidedOnAutoForward } from 'vs/platform/remote/common/tunnel'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { MovingAverage } from 'vs/base/common/numbers'; import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { ILogService } from 'vs/platform/log/common/log'; +import { flatten } from 'vs/base/common/arrays'; class ExtensionTunnel implements vscode.Tunnel { private _onDispose: Emitter = new Emitter(); @@ -139,6 +141,9 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe onDidChangeTunnels: vscode.Event = this._onDidChangeTunnels.event; private _candidateFindingEnabled: boolean = false; + private _providerHandleCounter: number = 0; + private _portAttributesProviders: Map = new Map(); + constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @IExtHostInitDataService initData: IExtHostInitDataService, @@ -173,6 +178,40 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe return Math.max(movingAverage * 20, 2000); } + private nextPortAttributesProviderHandle(): number { + return this._providerHandleCounter++; + } + + registerPortsAttributesProvider(portSelector: PortAttributesProviderSelector, provider: vscode.PortAttributesProvider): vscode.Disposable { + const providerHandle = this.nextPortAttributesProviderHandle(); + this._portAttributesProviders.set(providerHandle, { selector: portSelector, provider }); + + this._proxy.$registerPortsAttributesProvider(portSelector, providerHandle); + return new types.Disposable(() => { + this._portAttributesProviders.delete(providerHandle); + this._proxy.$unregisterPortsAttributesProvider(providerHandle); + }); + } + + async $providePortAttributes(handles: number[], ports: number[], pid: number | undefined, commandline: string | undefined, cancellationToken: vscode.CancellationToken): Promise { + const providedAttributes = await Promise.all(handles.map(handle => { + const provider = this._portAttributesProviders.get(handle); + if (!provider) { + return []; + } + return provider.provider.providePortAttributes(ports, pid, commandline, cancellationToken); + })); + + const allAttributes = providedAttributes.filter(attribute => !!attribute && attribute.length > 0); + + return (allAttributes.length > 0) ? flatten(allAttributes).map(attributes => { + return { + autoForwardAction: attributes.autoForwardAction, + port: attributes.port + }; + }) : []; + } + async $registerCandidateFinder(enable: boolean): Promise { if (enable && this._candidateFindingEnabled) { // already enabled diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index 22690bf27a0..34099db8509 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -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, IViewsService, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; -import { IRemoteExplorerService, makeAddress, mapHasAddressLocalhostOrAllInterfaces, OnPortForward, PortsAttributes, PORT_AUTO_FORWARD_SETTING, PORT_AUTO_SOURCE_SETTING, PORT_AUTO_SOURCE_SETTING_OUTPUT, PORT_AUTO_SOURCE_SETTING_PROCESS, TUNNEL_VIEW_CONTAINER_ID, TUNNEL_VIEW_ID } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { IRemoteExplorerService, makeAddress, mapHasAddressLocalhostOrAllInterfaces, OnPortForward, PORT_AUTO_FORWARD_SETTING, PORT_AUTO_SOURCE_SETTING, PORT_AUTO_SOURCE_SETTING_OUTPUT, PORT_AUTO_SOURCE_SETTING_PROCESS, TUNNEL_VIEW_CONTAINER_ID, TUNNEL_VIEW_ID } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { forwardedPortsViewEnabled, ForwardPortAction, OpenPortInBrowserAction, TunnelPanel, TunnelPanelDescriptor, TunnelViewModel, OpenPortInPreviewAction } from 'vs/workbench/contrib/remote/browser/tunnelView'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -222,8 +222,7 @@ class OnAutoForwardedAction extends Disposable { private readonly openerService: IOpenerService, private readonly externalOpenerService: IExternalUriOpenerService, private readonly tunnelService: ITunnelService, - private readonly hostService: IHostService, - private readonly portsAttributes: PortsAttributes) { + private readonly hostService: IHostService) { super(); this.lastNotifyTime = new Date(); this.lastNotifyTime.setFullYear(this.lastNotifyTime.getFullYear() - 1); @@ -233,7 +232,7 @@ class OnAutoForwardedAction extends Disposable { this.doActionTunnels = tunnels; const tunnel = await this.portNumberHeuristicDelay(); if (tunnel) { - switch (this.portsAttributes.getAttributes(tunnel.tunnelRemotePort)?.onAutoForward) { + switch ((await this.remoteExplorerService.tunnelModel.getAttributes([tunnel.tunnelRemotePort]))?.get(tunnel.tunnelRemotePort)?.onAutoForward) { case OnPortForward.OpenBrowser: { const address = makeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort); await OpenPortInBrowserAction.run(this.remoteExplorerService.tunnelModel, this.openerService, address); @@ -380,7 +379,6 @@ class OutputAutomaticPortForwarding extends Disposable { private portsFeatures?: IDisposable; private urlFinder?: UrlFinder; private notifier: OnAutoForwardedAction; - private portsAttributes: PortsAttributes; constructor( private readonly terminalService: ITerminalService, @@ -396,8 +394,7 @@ class OutputAutomaticPortForwarding extends Disposable { readonly privilegedOnly: boolean ) { super(); - this.portsAttributes = new PortsAttributes(configurationService); - this.notifier = new OnAutoForwardedAction(notificationService, remoteExplorerService, openerService, externalOpenerService, tunnelService, hostService, this.portsAttributes); + this.notifier = new OnAutoForwardedAction(notificationService, remoteExplorerService, openerService, externalOpenerService, tunnelService, hostService); this._register(configurationService.onDidChangeConfiguration((e) => { if (e.affectsConfiguration(PORT_AUTO_FORWARD_SETTING)) { this.tryStartStopUrlFinder(); @@ -430,7 +427,7 @@ class OutputAutomaticPortForwarding extends Disposable { if (mapHasAddressLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.detected, localUrl.host, localUrl.port)) { return; } - if (this.portsAttributes.getAttributes(localUrl.port)?.onAutoForward === OnPortForward.Ignore) { + if ((await this.remoteExplorerService.tunnelModel.getAttributes([localUrl.port]))?.get(localUrl.port)?.onAutoForward === OnPortForward.Ignore) { return; } if (this.privilegedOnly && !isPortPrivileged(localUrl.port, (await this.remoteAgentService.getEnvironment())?.os)) { @@ -458,7 +455,6 @@ class ProcAutomaticPortForwarding extends Disposable { private notifier: OnAutoForwardedAction; private initialCandidates: Set = new Set(); private portsFeatures: IDisposable | undefined; - private portsAttributes: PortsAttributes; constructor( private readonly configurationService: IConfigurationService, @@ -470,8 +466,7 @@ class ProcAutomaticPortForwarding extends Disposable { readonly hostService: IHostService ) { super(); - this.portsAttributes = new PortsAttributes(configurationService); - this.notifier = new OnAutoForwardedAction(notificationService, remoteExplorerService, openerService, externalOpenerService, tunnelService, hostService, this.portsAttributes); + this.notifier = new OnAutoForwardedAction(notificationService, remoteExplorerService, openerService, externalOpenerService, tunnelService, hostService); this._register(configurationService.onDidChangeConfiguration(async (e) => { if (e.affectsConfiguration(PORT_AUTO_FORWARD_SETTING)) { await this.startStopCandidateListener(); @@ -531,6 +526,7 @@ class ProcAutomaticPortForwarding extends Disposable { } private async forwardCandidates(): Promise { + const attributes = await this.remoteExplorerService.tunnelModel.getAttributes(this.remoteExplorerService.tunnelModel.candidates.map(candidate => candidate.port)); const allTunnels = (await Promise.all(this.remoteExplorerService.tunnelModel.candidates.map(async (value) => { const address = makeAddress(value.host, value.port); if (this.initialCandidates.has(address)) { @@ -543,7 +539,7 @@ class ProcAutomaticPortForwarding extends Disposable { if (mapHasAddressLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.detected, value.host, value.port)) { return undefined; } - if (this.portsAttributes.getAttributes(value.port)?.onAutoForward === OnPortForward.Ignore) { + if (attributes?.get(value.port)?.onAutoForward === OnPortForward.Ignore) { return undefined; } const forwarded = await this.remoteExplorerService.forward(value, undefined, undefined, undefined, undefined, undefined, false); diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 717528fa2df..25949bb876a 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -7,7 +7,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { ALL_INTERFACES_ADDRESSES, isAllInterfaces, isLocalhost, ITunnelService, LOCALHOST_ADDRESSES, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { ALL_INTERFACES_ADDRESSES, isAllInterfaces, isLocalhost, ITunnelService, LOCALHOST_ADDRESSES, PortAttributesProvider, ProvidedOnAutoForward, ProvidedPortAttributes, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IEditableData } from 'vs/workbench/common/views'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -19,6 +19,8 @@ import { isNumber, isObject, isString } from 'vs/base/common/types'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { hash } from 'vs/base/common/hash'; import { ILogService } from 'vs/platform/log/common/log'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { flatten } from 'vs/base/common/arrays'; export const IRemoteExplorerService = createDecorator('remoteExplorerService'); export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType'; @@ -241,6 +243,17 @@ export class PortsAttributes extends Disposable { }); return attributes; } + + static providedActionToAction(providedAction: ProvidedOnAutoForward | undefined) { + switch (providedAction) { + case ProvidedOnAutoForward.Notify: return OnPortForward.Notify; + case ProvidedOnAutoForward.OpenBrowser: return OnPortForward.OpenBrowser; + case ProvidedOnAutoForward.OpenPreview: return OnPortForward.OpenPreview; + case ProvidedOnAutoForward.Silent: return OnPortForward.Silent; + case ProvidedOnAutoForward.Ignore: return OnPortForward.Ignore; + default: return undefined; + } + } } export class TunnelModel extends Disposable { @@ -262,7 +275,9 @@ export class TunnelModel extends Disposable { private _onEnvironmentTunnelsSet: Emitter = new Emitter(); public onEnvironmentTunnelsSet: Event = this._onEnvironmentTunnelsSet.event; private _environmentTunnelsSet: boolean = false; - private portsAttributes: PortsAttributes; + private configPortsAttributes: PortsAttributes; + + private portAttributesProviders: PortAttributesProvider[] = []; constructor( @ITunnelService private readonly tunnelService: ITunnelService, @@ -274,7 +289,7 @@ export class TunnelModel extends Disposable { @ILogService private readonly logService: ILogService ) { super(); - this.portsAttributes = new PortsAttributes(configurationService); + this.configPortsAttributes = new PortsAttributes(configurationService); this.tunnelRestoreValue = this.getTunnelRestoreValue(); this.forwarded = new Map(); this.remoteTunnels = new Map(); @@ -328,7 +343,7 @@ export class TunnelModel extends Disposable { } })); - this._register(this.portsAttributes.onDidChangeAttributes(this.updateAttributes, this)); + this._register(this.configPortsAttributes.onDidChangeAttributes(this.updateAttributes, this)); } private makeTunnelPrivacy(isPublic: boolean) { @@ -391,7 +406,8 @@ export class TunnelModel extends Disposable { getAddress: async () => { return (await this.remoteAuthorityResolverService.resolveAuthority(authority)).authority; } } : undefined; - const attributes = this.portsAttributes.getAttributes(local !== undefined ? local : remote.port); + const port = local !== undefined ? local : remote.port; + const attributes = (await this.getAttributes([port]))?.get(port); const tunnel = await this.tunnelService.openTunnel(addressProvider, remote.host, remote.port, local, (!elevateIfNeeded) ? attributes?.elevateIfNeeded : elevateIfNeeded, isPublic); if (tunnel && tunnel.localAddress) { @@ -541,12 +557,75 @@ export class TunnelModel extends Disposable { private async updateAttributes() { // If the label changes in the attributes, we should update it. for (let forwarded of this.forwarded.values()) { - const attributes = this.portsAttributes.getAttributes(forwarded.remotePort); + const attributes = (await this.getAttributes([forwarded.remotePort], false))?.get(forwarded.remotePort); if (attributes && attributes.label && attributes.label !== forwarded.name) { await this.name(forwarded.remoteHost, forwarded.remotePort, attributes.label); } } } + + 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 => { + const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), LOCALHOST_ADDRESSES[0], port); + if (matchingCandidate) { + matchingCandidates.set(port, matchingCandidate); + if (!pidToPortsMapping.has(matchingCandidate.pid)) { + pidToPortsMapping.set(matchingCandidate.pid, []); + } + pidToPortsMapping.get(matchingCandidate.pid)?.push(port); + } + }); + // Group calls to provide attributes by pid. + const allProviderResults = await Promise.all(flatten(this.portAttributesProviders.map(provider => { + return Array.from(pidToPortsMapping.entries()).map(entry => { + const portGroup = entry[1]; + const matchingCandidate = matchingCandidates.get(portGroup[1]); + return provider.providePortAttributes(portGroup, + matchingCandidate?.pid, matchingCandidate?.detail, new CancellationTokenSource().token); + }); + }))); + const providedAttributes: Map = new Map(); + allProviderResults.forEach(attributes => attributes.forEach(attribute => { + if (attribute) { + providedAttributes.set(attribute.port, attribute); + } + })); + + if (!configAttributes && !providedAttributes) { + return undefined; + } + + // Merge. The config wins. + const mergedAttributes: Map = new Map(); + ports.forEach(port => { + const config = configAttributes.get(port); + const provider = providedAttributes.get(port); + mergedAttributes.set(port, { + elevateIfNeeded: config?.elevateIfNeeded, + label: config?.label, + onAutoForward: config?.onAutoForward ?? PortsAttributes.providedActionToAction(provider?.autoForwardAction) + }); + }); + + return mergedAttributes; + } + + addAttributesProvider(provider: PortAttributesProvider) { + this.portAttributesProviders.push(provider); + } } export interface CandidatePort { From 4a82fbab1629596fdaa66ace43180cae715a0177 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 8 Mar 2021 06:48:08 -0800 Subject: [PATCH 4/5] Don't force create a terminal on no reconnect Fixes #118321 --- .../contrib/terminal/browser/terminalService.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 5b072dbf47c..e05c0b45abd 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -183,13 +183,10 @@ export class TerminalService implements ITerminalService { const layoutInfo = await this._localTerminalService.getTerminalLayoutInfo(); if (layoutInfo && layoutInfo.tabs.length > 0) { this._recreateTerminalTabs(layoutInfo); - // now that terminals have been restored, - // attach listeners to update local state when terminals are changed - this.attachProcessLayoutListeners(false); - } else { - this.createTerminal(); - this.attachProcessLayoutListeners(false); } + // now that terminals have been restored, + // attach listeners to update local state when terminals are changed + this.attachProcessLayoutListeners(false); this._connectionState = TerminalConnectionState.Connected; this._onDidChangeConnectionState.fire(); } From f2cfdcf19b89364e93fbe36c6fb09a48856249d4 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Mon, 8 Mar 2021 10:04:34 -0500 Subject: [PATCH 5/5] Bump distro --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4f3f4508f6f..29712325f67 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.55.0", - "distro": "cc514c3c154928491711c88400585105a6afe9c7", + "distro": "2173d6da3776d9f0e894d49e854ba495636e1b5d", "author": { "name": "Microsoft Corporation" },