autocannon
Version:
Fast HTTP benchmarking tool written in Node.js
337 lines (297 loc) • 9.21 kB
JavaScript
const crossArgv = require('cross-argv')
const fs = require('fs')
const os = require('os')
const net = require('net')
const path = require('path')
const URL = require('url').URL
const spawn = require('child_process').spawn
const managePath = require('manage-path')
const hasAsyncHooks = require('has-async-hooks')
const subarg = require('@minimistjs/subarg')
const printResult = require('./lib/printResult')
const initJob = require('./lib/init')
const track = require('./lib/progressTracker')
const generateSubArgAliases = require('./lib/subargAliases')
const { checkURL, ofURL } = require('./lib/url')
const { parseHAR } = require('./lib/parseHAR')
const _aggregateResult = require('./lib/aggregateResult')
const validateOpts = require('./lib/validate')
if (typeof URL !== 'function') {
console.error('autocannon requires the WHATWG URL API, but it is not available. Please upgrade to Node 6.13+.')
process.exit(1)
}
module.exports = initJob
module.exports.track = track
module.exports.start = start
module.exports.printResult = printResult
module.exports.parseArguments = parseArguments
module.exports.aggregateResult = function aggregateResult (results, opts = {}) {
if (!Array.isArray(results)) {
throw new Error('"results" must be an array of results')
}
opts = validateOpts(opts, false)
if (opts instanceof Error) {
throw opts
}
return _aggregateResult(results, opts)
}
const alias = {
connections: 'c',
pipelining: 'p',
timeout: 't',
duration: 'd',
sampleInt: 'L',
amount: 'a',
json: 'j',
renderLatencyTable: ['l', 'latency'],
onPort: 'on-port',
method: 'm',
headers: ['H', 'header'],
body: 'b',
form: 'F',
servername: 's',
bailout: 'B',
input: 'i',
maxConnectionRequests: 'M',
maxOverallRequests: 'O',
connectionRate: 'r',
overallRate: 'R',
ignoreCoordinatedOmission: 'C',
reconnectRate: 'D',
renderProgressBar: 'progress',
renderStatusCodes: 'statusCodes',
title: 'T',
verbose: 'V',
version: 'v',
forever: 'f',
idReplacement: 'I',
socketPath: 'S',
excludeErrorStats: 'x',
expectBody: 'E',
workers: 'w',
warmup: 'W',
help: 'h'
}
const defaults = {
connections: 10,
timeout: 10,
pipelining: 1,
duration: 10,
sampleInt: 1000,
reconnectRate: 0,
renderLatencyTable: false,
renderProgressBar: true,
renderStatusCodes: false,
json: false,
forever: false,
method: 'GET',
idReplacement: false,
excludeErrorStats: false,
debug: false,
workers: 0,
verbose: true
}
function parseArguments (argvs) {
let argv = subarg(argvs, {
boolean: ['json', 'n', 'help', 'renderLatencyTable', 'renderProgressBar', 'renderStatusCodes', 'forever', 'idReplacement', 'excludeErrorStats', 'onPort', 'debug', 'ignoreCoordinatedOmission', 'verbose'],
alias,
default: defaults,
'--': true
})
// subarg does not convert aliases in sub arguments
argv = generateSubArgAliases(argv)
argv.url = argv._.length > 1 ? argv._ : argv._[0]
// Assume onPort if `-- node` is provided
if (argv['--'][0] === 'node') {
argv.onPort = true
}
if (argv.onPort) {
argv.spawn = argv['--']
}
// support -n to disable the progress bar and results table
if (argv.n) {
argv.renderProgressBar = false
argv.renderResultsTable = false
argv.renderStatusCodes = false
}
if (argv.version) {
console.log('autocannon', 'v' + require('./package').version)
console.log('node', process.version)
return
}
if (!checkURL(argv.url) || argv.help) {
const help = fs.readFileSync(path.join(__dirname, 'help.txt'), 'utf8')
console.error(help)
return
}
// if PORT is set (like by `0x`), target `localhost:PORT/path` by default.
// this allows doing:
// 0x --on-port 'autocannon /path' -- node server.js
if (process.env.PORT) {
argv.url = ofURL(argv.url).map(url => new URL(url, `http://localhost:${process.env.PORT}`).href)
}
// Add http:// if it's not there and this is not a /path
argv.url = ofURL(argv.url).map(url => {
if (url.indexOf('http') !== 0 && url[0] !== '/') {
url = `http://${url}`
}
return url
})
// check that the URL is valid.
ofURL(argv.url).map(url => {
try {
// If --on-port is given, it's acceptable to not have a hostname
if (argv.onPort) {
new URL(url, 'http://localhost') // eslint-disable-line no-new
} else {
new URL(url) // eslint-disable-line no-new
}
} catch (err) {
console.error(err.message)
console.error('')
console.error('When targeting a path without a hostname, the PORT environment variable must be available.')
console.error('Use a full URL or set the PORT variable.')
process.exit(1)
}
return null // to make linter happy
})
if (argv.input) {
argv.body = fs.readFileSync(argv.input, 'utf8')
}
if (argv.headers) {
if (!Array.isArray(argv.headers)) {
argv.headers = [argv.headers]
}
argv.headers = argv.headers.reduce((obj, header) => {
const colonIndex = header.indexOf(':')
const equalIndex = header.indexOf('=')
const index = Math.min(colonIndex < 0 ? Infinity : colonIndex, equalIndex < 0 ? Infinity : equalIndex)
if (Number.isFinite(index) && index > 0) {
obj[header.slice(0, index)] = header.slice(index + 1)
return obj
} else throw new Error(`An HTTP header was not correctly formatted: ${header}`)
}, {})
}
if (argv.har) {
try {
argv.har = JSON.parse(fs.readFileSync(argv.har))
// warn users about skipped HAR requests
const requestsByOrigin = parseHAR(argv.har)
const allowed = ofURL(argv.url, true).map(url => new URL(url).origin)
for (const [origin] of requestsByOrigin) {
if (!allowed.includes(origin)) {
console.error(`Warning: skipping requests to '${origin}' as the target is ${allowed.join(', ')}`)
}
}
} catch (err) {
throw new Error(`Failed to load HAR file content: ${err.message}`)
}
}
argv.tlsOptions = {}
if (argv.cert) {
try {
argv.tlsOptions.cert = fs.readFileSync(argv.cert)
} catch (err) {
throw new Error(`Failed to load cert file: ${err.message}`)
}
}
if (argv.key) {
try {
argv.tlsOptions.key = fs.readFileSync(argv.key)
} catch (err) {
throw new Error(`Failed to load key file: ${err.message}`)
}
}
if (argv.ca) {
if (typeof argv.ca === 'string') {
argv.ca = [argv.ca]
} else if (Array.isArray(argv.ca._)) {
argv.ca = argv.ca._
}
try {
argv.tlsOptions.ca = argv.ca.map(caPath => fs.readFileSync(caPath))
} catch (err) {
throw new Error(`Failed to load ca file: ${err.message}`)
}
}
// This is to distinguish down the line whether it is
// run via command-line or programmatically
argv[Symbol.for('internal')] = true
return argv
}
function start (argv) {
if (!argv) {
// we are printing the help
return
}
if (argv.onPort) {
if (!hasAsyncHooks()) {
console.error('The --on-port flag requires the async_hooks builtin module, but it is not available. Please upgrade to Node 8.1+.')
process.exit(1)
}
const { socketPath, server } = createChannel((port) => {
const url = new URL(argv.url, `http://localhost:${port}`).href
const opts = Object.assign({}, argv, {
onPort: false,
url
})
const tracker = initJob(opts, () => {
proc.kill('SIGINT')
server.close()
})
process.once('SIGINT', () => {
tracker.stop()
})
})
// manage-path always uses the $PATH variable, but we can pretend
// that it is equal to $NODE_PATH
const alterPath = managePath({ PATH: process.env.NODE_PATH })
alterPath.unshift(path.join(__dirname, 'lib/preload'))
const proc = spawn(argv.spawn[0], argv.spawn.slice(1), {
stdio: ['ignore', 'inherit', 'inherit'],
env: Object.assign({}, process.env, {
NODE_OPTIONS: ['-r', 'autocannonDetectPort'].join(' ') +
(process.env.NODE_OPTIONS ? ` ${process.env.NODE_OPTIONS}` : ''),
NODE_PATH: alterPath.get(),
AUTOCANNON_SOCKET: socketPath
})
})
} else {
// if forever is true then a promise is not returned and we need to try ... catch errors
try {
const tracker = initJob(argv)
if (tracker.then) {
tracker.catch((err) => {
console.error(err.message)
})
}
} catch (err) {
console.error(err.message)
}
}
}
function createChannel (onport) {
const pipeName = `${process.pid}.autocannon`
const socketPath = process.platform === 'win32'
? `\\\\?\\pipe\\${pipeName}`
: path.join(os.tmpdir(), pipeName)
const server = net.createServer((socket) => {
socket.once('data', (chunk) => {
const port = chunk.toString()
onport(port)
})
})
server.listen(socketPath)
server.on('close', () => {
try {
fs.unlinkSync(socketPath)
} catch (err) {}
})
return { socketPath, server }
}
if (require.main === module) {
const argv = crossArgv(process.argv.slice(2))
start(parseArguments(argv))
}