From df2ae320fcfc2c6f00011ccad7e98377bf3da46c Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 29 Jan 2018 22:26:05 +0100 Subject: [PATCH] Support Windows Named Pipes for IPC --- README.md | 6 ++- autocannon.js | 6 +-- help.txt | 4 +- lib/httpClient.js | 6 +-- lib/run.js | 6 +-- test/cli-ipc.test.js | 86 +++++++++++++++++++++++++++++++++++++++++ test/run.test.js | 51 +++++++++++++++--------- test/unixSocket.test.js | 48 ----------------------- 8 files changed, 134 insertions(+), 79 deletions(-) create mode 100644 test/cli-ipc.test.js delete mode 100644 test/unixSocket.test.js diff --git a/README.md b/README.md index 07f616be..fa5753f8 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,10 @@ npm i autocannon --save ``` Usage: autocannon [opts] URL -URL is any valid http or https url. Can alternatively be a path to a -Unix Domain socket. +URL is any valid http or https url. + +For IPC, the url can be substituted by a path to a Unix Domain Socket or +a Windows Named Pipe. Available options: diff --git a/autocannon.js b/autocannon.js index 1f3b8530..79fedc07 100755 --- a/autocannon.js +++ b/autocannon.js @@ -58,7 +58,7 @@ function parseArguments (argvs) { } }) - if (isUnixSocket(argv._[0])) { + if (isIPC(argv._[0])) { argv.socketPath = argv._[0] } else { argv.url = argv._[0] @@ -128,8 +128,8 @@ function start (argv) { }) } -function isUnixSocket (path) { - return /\.sock$/.test(path) && fs.existsSync(path) +function isIPC (path) { + return (/\.sock$/.test(path) && fs.existsSync(path)) || /^\\\\[?.]\\pipe/.test(path) } if (require.main === module) { diff --git a/help.txt b/help.txt index 9d3be373..0e86a4c1 100644 --- a/help.txt +++ b/help.txt @@ -1,6 +1,8 @@ Usage: autocannon [opts] URL -URL is any valid http or https url. Can alternatively be a path to a Unix Domain socket. +URL is any valid http or https url. + +For IPC, the url can be substituted by a path to a Unix Domain Socket or a Windows Named Pipe. Available options: diff --git a/lib/httpClient.js b/lib/httpClient.js index c174f5ab..c17bc1fa 100644 --- a/lib/httpClient.js +++ b/lib/httpClient.js @@ -18,7 +18,7 @@ function Client (opts) { this.opts = opts this.timeout = (opts.timeout || 10) * 1000 - this.unixSocket = !!opts.socketPath + this.ipc = !!opts.socketPath this.secure = opts.protocol === 'https:' if (this.secure && this.opts.port === 80) this.opts.port = 443 this.parser = new HTTPParser(HTTPParser.RESPONSE) @@ -103,13 +103,13 @@ inherits(Client, EE) Client.prototype._connect = function () { if (this.secure) { - if (this.unixSocket) { + if (this.ipc) { this.conn = tls.connect(this.opts.socketPath, { rejectUnauthorized: false }) } else { this.conn = tls.connect(this.opts.port, this.opts.hostname, { rejectUnauthorized: false }) } } else { - if (this.unixSocket) { + if (this.ipc) { this.conn = net.connect(this.opts.socketPath) } else { this.conn = net.connect(this.opts.port, this.opts.hostname) diff --git a/lib/run.js b/lib/run.js index 7cc42562..936d5662 100644 --- a/lib/run.js +++ b/lib/run.js @@ -78,10 +78,10 @@ function run (opts, cb) { // is done tracker.opts = opts - const unixSocket = !!opts.socketPath + const ipc = !!opts.socketPath - if (!unixSocket && opts.url.indexOf('http') !== 0) opts.url = 'http://' + opts.url - const url = unixSocket ? {socketPath: opts.socketPath} : URL.parse(opts.url) + if (!ipc && opts.url.indexOf('http') !== 0) opts.url = 'http://' + opts.url + const url = ipc ? {socketPath: opts.socketPath} : URL.parse(opts.url) let counter = 0 let bytes = 0 diff --git a/test/cli-ipc.test.js b/test/cli-ipc.test.js new file mode 100644 index 00000000..783f2b40 --- /dev/null +++ b/test/cli-ipc.test.js @@ -0,0 +1,86 @@ +'use strict' + +const t = require('tap') +const split = require('split2') +const os = require('os') +const path = require('path') +const childProcess = require('child_process') +const helper = require('./helper') + +const win = process.platform === 'win32' + +const lines = [ + /Running 1s test @ .*$/, + /10 connections.*$/, + /$/, + /Stat.*Avg.*Stdev.*Max.*$/, + /Latency \(ms\).*$/, + /Req\/Sec.*$/, + /Bytes\/Sec.*$/, + /$/, + /.* requests in \d+s, .* read/ +] + +if (!win) { + // If not Windows we can predict exactly how many lines there will be. On + // Windows we rely on t.end() being called. + t.plan(lines.length) + t.tearDown(teardown) +} + +const socketPath = win + ? path.join('\\\\?\\pipe', process.cwd(), 'autocannon-' + Date.now()) + : path.join(os.tmpdir(), 'autocannon-' + Date.now() + '.sock') + +helper.startServer({socketPath}) + +const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), '-d', '1', socketPath], { + cwd: __dirname, + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'], + detached: false +}) + +// For handling the last line on Windows +let errorLine = false +let failsafeTimer + +child + .stderr + .pipe(split()) + .on('data', (line) => { + let regexp = lines.shift() + const lastLine = lines.length === 0 + + if (regexp) { + t.ok(regexp.test(line), 'line matches ' + regexp) + + if (lastLine && win) { + // We can't be sure the error line is outputted on Windows, so in case + // this really is the last line, we'll set a timer to auto-end the test + // in case there are no more lines. + failsafeTimer = setTimeout(function () { + t.end() + teardown() + }, 1000) + } + } else if (!errorLine && win) { + // On Windows a few errors are expected. We'll accept a 1% error rate on + // the pipe. + errorLine = true + clearTimeout(failsafeTimer) + regexp = /^(\d+) errors \(0 timeouts\)$/ + const match = line.match(regexp) + t.ok(match, 'line matches ' + regexp) + const errors = Number(match[1]) + t.ok(errors / 15000 < 0.01, `should have less than 1% errors on Windows (had ${errors} errors)`) + t.end() + teardown() + } else { + throw new Error('Unexpected line: ' + JSON.stringify(line)) + } + }) + +function teardown () { + child.kill() +} diff --git a/test/run.test.js b/test/run.test.js index 76a310b0..cd490fd4 100644 --- a/test/run.test.js +++ b/test/run.test.js @@ -224,29 +224,42 @@ test('run should recognise valid urls without http at the start', (t) => { }) }) -if (process.platform !== 'win32') { - test('run should accept a unix socket instead of a url', (t) => { - t.plan(11) +test('run should accept a unix socket/windows pipe instead of a url', (t) => { + t.plan(11) - const socketPath = path.join(os.tmpdir(), 'autocannon-' + Date.now() + '.sock') - helper.startServer({socketPath}) + const socketPath = process.platform === 'win32' + ? path.join('\\\\?\\pipe', process.cwd(), 'autocannon-' + Date.now()) + : path.join(os.tmpdir(), 'autocannon-' + Date.now() + '.sock') - run({socketPath}, (err, result) => { - t.error(err) - t.ok(result, 'results should exist') - t.equal(result.socketPath, socketPath, 'socketPath should be included in result') - t.ok(result.requests.total > 0, 'should make at least one request') + helper.startServer({socketPath}) + + run({ + socketPath, + connections: 2, + duration: 2 + }, (err, result) => { + t.error(err) + t.ok(result, 'results should exist') + t.equal(result.socketPath, socketPath, 'socketPath should be included in result') + t.ok(result.requests.total > 0, 'should make at least one request') + + if (process.platform === 'win32') { + // On Windows a few errors are expected. We'll accept a 1% error rate on + // the pipe. + t.ok(result.errors / result.requests.total < 0.01, `should have less than 1% errors on Windows (had ${result.errors} errors)`) + } else { t.equal(result.errors, 0, 'no errors') - t.equal(result['1xx'], 0, '1xx codes') - t.equal(result['2xx'], result.requests.total, '2xx codes') - t.equal(result['3xx'], 0, '3xx codes') - t.equal(result['4xx'], 0, '4xx codes') - t.equal(result['5xx'], 0, '5xx codes') - t.equal(result.non2xx, 0, 'non 2xx codes') - t.end() - }) + } + + t.equal(result['1xx'], 0, '1xx codes') + t.equal(result['2xx'], result.requests.total, '2xx codes') + t.equal(result['3xx'], 0, '3xx codes') + t.equal(result['4xx'], 0, '4xx codes') + t.equal(result['5xx'], 0, '5xx codes') + t.equal(result.non2xx, 0, 'non 2xx codes') + t.end() }) -} +}) for (let i = 1; i <= 5; i++) { test(`run should count all ${i}xx status codes`, (t) => { diff --git a/test/unixSocket.test.js b/test/unixSocket.test.js deleted file mode 100644 index e9d1d76a..00000000 --- a/test/unixSocket.test.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict' - -// UNIX sockets are not supported by Windows -if (process.platform === 'win32') process.exit(0) - -const t = require('tap') -const split = require('split2') -const os = require('os') -const path = require('path') -const childProcess = require('child_process') -const helper = require('./helper') - -const lines = [ - /Running 1s test @ .*$/, - /10 connections.*$/, - /$/, - /Stat.*Avg.*Stdev.*Max.*$/, - /Latency \(ms\).*$/, - /Req\/Sec.*$/, - /Bytes\/Sec.*$/, - /$/, - /.* requests in \d+s, .* read/ -] - -t.plan(lines.length * 2) - -const socketPath = path.join(os.tmpdir(), 'autocannon-' + Date.now() + '.sock') -helper.startServer({socketPath}) - -const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), '-d', '1', socketPath], { - cwd: __dirname, - env: process.env, - stdio: ['ignore', 'pipe', 'pipe'], - detached: false -}) - -t.tearDown(() => { - child.kill() -}) - -child - .stderr - .pipe(split()) - .on('data', (line) => { - const regexp = lines.shift() - t.ok(regexp, 'we are expecting this line') - t.ok(regexp.test(line), 'line matches ' + regexp) - })