Files
cypress/packages/server/lib/util/net_profiler.js
T
Zach Bloomquist c1a345dce2 Improved proxy support (#3531)
* https-proxy: unused file

* server: wrap all https requests that use a proxy

* server: use request lib in ensureUrl if proxy is in use. this makes runs tab work behind a proxy

* electron: pass --proxy-server to app itself, so the embedded github login page works

* cli: first attempt at env vars from windows registry

* cli: api cleanup

* cli: lint

* cli: fix crash on no proxy, add tests

* add desktop-gui watch to terminals.json

* cli: pass along --proxy-source

* electron: pass --proxy-bypass-list too

* server: whitelist proxy* args

* cli: better wording

* desktop-gui: display proxy settings

* extension: force proxy [wip]

* extension: finally, i am victorious over coffeescript

* extension: add -loopback to bypasslist

* extension: revert changes

Revert "extension: force proxy [wip]"

This reverts commit 3ab6ba42a763f25ee65f12eb8b79eb597efc9b11.

* desktop-gui: skip proxysettings if there aren't any

* https-proxy, server: proxy directConnections using https-proxy-agent

* https-agent: pool httpsAgents

* https-proxy: work when they're not on a proxy

* https-proxy: ci - use agent 1.0

* https-proxy: tests

* desktop-gui: hide proxy settings when not using proxy

* https-proxy: pass req through to https-proxy-agent callback

* cli: use get-windows-proxy

* desktop-gui: always show proxy settings

* server: use get-windows-proxy

* electron, server: supply electron proxy config when window launched

* server: fix

* https-proxy: cleanup

* server: clean up ensureUrl

* https-proxy: cleanup

* cli: fix

* cli: fix destructuring

* server: enable ForeverAgent to pool HTTPS/HTTP connections

#3192

* server: updating snapshot

* https-proxy: don't crash, do error if proxy unreachable

* https-proxy:

* get-windows-proxy@1.0.0

* https-proxy: use proxy-from-env to decide on a proxy for a url

* server: fallback to HTTP_PROXY globally if HTTPS_PROXY not set

* server: proxy args test

* cli: add proxy tests

* cli: add test that loadSystemProxySettings is called during download

* cli, server: account for the fact that CI has some proxy vars set

* https-proxy: ""

* cli, https-proxy, server: ""

* desktop-gui: update settings gui

* desktop-gui: cypress tests for proxy settings

* server: strict undefined check

* cli, server: move get-windows-proxy to scope, optionalDeps

* server, cli: use new and improved get-windows-proxy

* cli, server: 1.5.0

* server: re-check for proxy since cli may have failed to load the lib

* server, cli: 1.5.1

* server: NO_PROXY=localhost by default, clean up

* https-proxy: disable Nagle's on proxy sockets

\#3192

* https-proxy: use setNoDelay on upstream, cache https agent

* https-proxy: test basic auth

* https-proxy: add todo: remove this

* server: add custom HTTP(s) Agent implementation w keepalive, tunneling

* server: typescript for agent

* add ts to zunder

* server: more ts

* ts: add missing Agent type declaration

* server: create CombinedAgent

* server: use agent in more places

* ts: more declarations

* server: make script work even if debug port not supplied

* server: begin some testing

* server, ts: agent, tests

* server: test

* server: agent works with websockets now

* server: update snapshot

* server: work out some more bugs with websockets

* server: more websockets

* server: add net_profiler

* https-proxy: fix dangling socket on direct connection

* server: fix potential 'headers have already been sent'

* https-proxy: nab another dangler

* server: update test to expect agent

* https-proxy: fix failing test

* desktop-gui: change on-link

* server: add tests for empty response case

* server: tests

* server: send keep-alive with requests

* server: make net profiler hook on socket.connect

* server: only hook profiler once

* server: update tests, add keep-alive test

* server: only regen headers if needed

* server: move http_overrides into CombinedAgent, make it proxy-proof

for #112

* server: update snapshot

* server: undo

* server: avoid circular dependency

* https-proxy, server: use our Agent instead of https-proxy-agent

* server: add dependency back

* cli: actually use proxy for download

* server, launcher, ts: typescript

* Revert "server, launcher, ts: typescript"

This reverts commit d3f8b8bbb6.

* Revert "Revert "server, launcher, ts: typescript""

This reverts commit 818dfdfd00.

* ts, server: respond to PR

* server, ts: types

* ts: really fix types

* https-proxy, server: export CA from https-proxy

* agent, server, https-proxy: move agent to own package

* agent => networking, move connect into networking

* fix tests

* fix test

* networking: respond to PR changes, add more unit tests

* rename ctx

* networking, ts: add more tests

* server: add ensureUrl tests

* https-proxy: remove https-proxy-agent

* server: use CombinedAgent for API

* server: updates

* add proxy performance tests

* add perf tests to workflow

* circle

* run perf tests with --no-sandbox

* networking, ts: ch-ch-ch-ch-changes

* server, networking: pr changes

* run networking tests in circle

* server: fix performance test

* https-proxy: test that sockets are being closed

* https-proxy: write, not emit

* networking: fix test

* networking: bubble err in connect

* networking: style

* networking: clean p connect error handling

* networking => network

* server: make perf tests really work

* server: really report

* server: use args from browser

* server: use AI to determine max run time

* server: load electron only when needed


Co-authored-by: Brian Mann <brian@cypress.io>
2019-03-31 23:39:10 -04:00

265 lines
6.7 KiB
JavaScript

const fs = require('fs')
const debug = require('debug')('net-profiler')
function getCaller (level = 5) {
try {
return new Error().stack.split('\n')[level].slice(7)
} catch (e) {
return 'unknown'
}
}
function getLogPath (logPath) {
if (!logPath) {
const os = require('os')
const dirName = fs.mkdtempSync(`${os.tmpdir()}/net-profiler-`)
logPath = `${dirName}/timeline.txt`
}
return logPath
}
function Connection (host, port, type = 'connection', toHost, toPort) {
this.type = type
this.host = host || 'localhost'
this.port = port
this.toHost = toHost || 'localhost'
this.toPort = toPort
}
Connection.prototype.beginning = function () {
switch (this.type) {
case 'server':
return `O server began listening on ${this.host}:${this.port} at ${getCaller()}`
case 'client':
return `C client connected from ${this.host}:${this.port} to server on ${this.toHost}:${this.toPort}`
default:
return `X connection opened to ${this.host}:${this.port} by ${getCaller()}`
}
}
Connection.prototype.ending = function () {
switch (this.type) {
case 'server':
return 'O server closed'
case 'client':
return 'C client disconnected'
default:
return 'X connection closed'
}
}
/**
* Tracks all incoming and outgoing network connections and logs a timeline of network traffic to a file.
*
* @param options.net the `net` object to stub, default: nodejs net object
* @param options.tickMs the number of milliseconds between ticks in the profile, default: 1000
* @param options.tickWhenNoneActive should ticks be recorded when no connections are active, default: false
* @param options.logPath path to the file to append to, default: new file in your temp directory
*/
function NetProfiler (options = {}) {
if (!(this instanceof NetProfiler)) return new NetProfiler(options)
if (!options.net) {
options.net = require('net')
}
this.net = options.net
this.proxies = {}
this.activeConnections = []
this.startTs = new Date() / 1000
this.tickMs = options.tickMs || 1000
this.tickWhenNoneActive = options.tickWhenNoneActive || false
this.logPath = getLogPath(options.logPath)
debug('logging to ', this.logPath)
this.startProfiling()
}
NetProfiler.prototype.install = function () {
const net = this.net
const self = this
function netSocketPrototypeConnectApply (target, thisArg, args) {
const client = target.bind(thisArg)(...args)
let options = self.net._normalizeArgs(args)[0]
if (Array.isArray(options)) {
options = options[0]
}
options.host = options.host || 'localhost'
const connection = new Connection(options.host, options.port)
client.on('close', () => {
self.removeActiveConnection(connection)
})
self.addActiveConnection(connection)
return client
}
function netServerPrototypeListenApply (target, thisArg, args) {
const server = thisArg
server.on('listening', () => {
const { host, port } = server.address()
const connection = new Connection(host, port, 'server')
self.addActiveConnection(connection)
server.on('close', () => {
self.removeActiveConnection(connection)
})
server.on('connection', (client) => {
const clientConn = new Connection(client.remoteAddress, client.remotePort, 'client', host, port)
self.addActiveConnection(clientConn)
client.on('close', () => {
self.removeActiveConnection(clientConn)
})
})
})
const listener = target.bind(thisArg)(...args)
return listener
}
this.proxies['net.Socket.prototype.connect'] = Proxy.revocable(net.Socket.prototype.connect, {
apply: netSocketPrototypeConnectApply,
})
this.proxies['net.Server.prototype.listen'] = Proxy.revocable(net.Server.prototype.listen, {
apply: netServerPrototypeListenApply,
})
net.Socket.prototype.connect = this.proxies['net.Socket.prototype.connect'].proxy
net.Server.prototype.listen = this.proxies['net.Server.prototype.listen'].proxy
}
NetProfiler.prototype.uninstall = function () {
const net = this.net
net.Socket.prototype.connect = this.proxies['net.Socket.prototype.connect'].proxy['[[Target]]']
net.Server.prototype.listen = this.proxies['net.Server.prototype.listen'].proxy['[[Target]]']
this.proxies.forEach((proxy) => {
proxy.revoke()
})
}
NetProfiler.prototype.startProfiling = function () {
this.install()
debug('profiling started')
this.logStream = fs.openSync(this.logPath, 'a')
this.writeTimeline('Profiling started!')
this.startTimer()
}
NetProfiler.prototype.startTimer = function () {
if (!this.tickMs) {
return
}
this.timer = setInterval(() => {
const tick = this.tickWhenNoneActive || this.activeConnections.find((x) => {
return !!x
})
if (tick) {
this.writeTimeline()
}
}, this.tickMs)
}
NetProfiler.prototype.stopTimer = function () {
clearInterval(this.timer)
}
NetProfiler.prototype.stopProfiling = function () {
this.writeTimeline('Profiling stopped!')
this.stopTimer()
fs.closeSync(this.logStream)
debug('profiling ended')
this.uninstall()
}
NetProfiler.prototype.addActiveConnection = function (connection) {
let index = this.activeConnections.findIndex((x) => {
return typeof x === 'undefined'
})
if (index === -1) {
index = this.activeConnections.length
this.activeConnections.push(connection)
} else {
this.activeConnections[index] = connection
}
this.writeTimeline(index, connection.beginning())
}
NetProfiler.prototype.removeActiveConnection = function (connection) {
let index = this.activeConnections.findIndex((x) => {
return x === connection
})
this.writeTimeline(index, connection.ending())
this.activeConnections[index] = undefined
}
NetProfiler.prototype.getTimestamp = function () {
let elapsed = (new Date() / 1000 - this.startTs).toString()
const parts = elapsed.split('.', 2)
if (!parts[1]) {
parts[1] = '000'
}
while (parts[1].length < 3) {
parts[1] += '0'
}
elapsed = `${parts[0]}.${parts[1] ? parts[1].slice(0, 3) : '000'}`
while (elapsed.length < 11) {
elapsed = ` ${elapsed}`
}
return elapsed
}
NetProfiler.prototype.writeTimeline = function (index, message) {
if (!message) {
message = index || ''
index = this.activeConnections.length
}
let row = ` ${this.activeConnections.map((conn, i) => {
if (conn) {
return ['|', '1', 'l', ':'][i % 4]
}
return ' '
}).join(' ')}`
if (message) {
const column = 3 + index * 4
row = `${row.substring(0, column - 2)}[ ${message} ]${row.substring(2 + column + message.length)}`
}
row = `${this.getTimestamp()}${row.replace(/\s+$/, '')}\n`
fs.writeSync(this.logStream, row)
}
module.exports = NetProfiler