diff --git a/lib/processes.js b/lib/processes.js index 1661e29..dbd9e30 100644 --- a/lib/processes.js +++ b/lib/processes.js @@ -45,6 +45,7 @@ const Proxy = require('./processes/proxy'); const App = require('./processes/application'); const Selenium = require('./processes/selenium'); const ChromeDriver = require('./processes/chromedriver'); +const { checkExtraProcs } = require('./processes/extra'); const CHROME = 'chrome'; @@ -87,7 +88,7 @@ function unrefAll(procs) { function launchAllProcesses(config) { const launchApp = config.getBool('launch', false); - function launchWithAppPort(appPort) { + async function launchWithAppPort(appPort) { config.set('app.port', appPort); const browserName = config.get('browser'); @@ -115,6 +116,11 @@ function launchAllProcesses(config) { debug('Using existing selenium server', seleniumUrl); } + const extraProcs = config.get('processes', {}); + if (extraProcs) { + servers.push(...(await checkExtraProcs(extraProcs))); + } + if (browserName === CHROME) { const chromePath = config.get('chrome.command', null); if (chromePath) { @@ -149,19 +155,19 @@ function launchAllProcesses(config) { if (process.getuid() === 0) args.push('--no-sandbox'); - return isChromeInDocker(browserName) - .then(() => { - debug('Running in docker env'); - args.push('--disable-dev-shm-usage'); - }) - .catch(() => { - debug('Not running in docker env'); - }) - .then(() => { - config.set('desiredCapabilities.chromeOptions.args', args); - return spawnServers(config, servers); - }) - .then(unrefAll); + if ( + await isChromeInDocker(browserName).then( + () => true, + () => false + ) + ) { + debug('Running in docker env'); + args.push('--disable-dev-shm-usage'); + } else { + debug('Not running in docker env'); + } + + config.set('desiredCapabilities.chromeOptions.args', args); } return spawnServers(config, servers).then(unrefAll); diff --git a/lib/processes/extra.js b/lib/processes/extra.js new file mode 100644 index 0000000..ae9acab --- /dev/null +++ b/lib/processes/extra.js @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2015, Groupon, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of GROUPON nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +'use strict'; + +/* + Given configuration like: + { + // ... + processes: { + memcached: { + port: 11211, + command: 'memcached', + commandArgs: ['-u', 'memcached', '-d'], + reuseExisting: true, + }, + // ... + } + } + + This will turn them into server configuration bits and start things up + for you; skipping them if something's already listening on that port +*/ + +const { isAvailable } = require('find-open-port'); +const debug = require('debug')('testium-core:processes:extra'); + +/** + * @typedef Server + * @property {string} name + * @property {() => SubprocessOpts | Promise} getOptions + * + * @typedef SubprocessOpts + * @property {number} port + * ...and a bunch more options; see subprocess/README.md + * + * @typedef {SubprocessOpts & { reuseExisting?: boolean }} ExtraProcOpts + */ + +/** @param {Record} extraProcs */ +async function checkExtraProcs(extraProcs) { + /** @type {Server[]} */ + const servers = []; + + /** @type {[ string, SubprocessOpts & { port: number } ]} */ + const toCheck = []; + + for (const [name, opts] of Object.entries(extraProcs)) { + const { reuseExisting = false, ...subPOpts } = opts; + + if (!reuseExisting) { + servers.push({ name, getOptions: () => subPOpts }); + continue; + } + + if (!subPOpts.port) { + throw new Error( + `process.* with reuseExisting=true must include static port` + ); + } + + toCheck.push([name, subPOpts]); + } + + if (toCheck.length > 0) { + await Promise.all( + toCheck.map(async ([name, opts]) => { + const { port } = opts; + if (await isAvailable(port)) { + servers.push({ name, getOptions: () => opts }); + } else { + debug( + `Not starting reuseExisting process ${name}: port ${port} is in use` + ); + } + }) + ); + } + + return servers; +} +exports.checkExtraProcs = checkExtraProcs; diff --git a/package-lock.json b/package-lock.json index faf136f..1b572bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "gofer": "^5.1.0", "memfs": "^2.17.1", "mocha": "^9.0.1", - "nlm": "^5.5.1", + "nlm": "^5.6.1", "npm-run-all": "^4.1.5", "prettier": "^2.3.1", "sinon": "^7.5.0", diff --git a/package.json b/package.json index 7dbcd8b..3168318 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "gofer": "^5.1.0", "memfs": "^2.17.1", "mocha": "^9.0.1", - "nlm": "^5.5.1", + "nlm": "^5.6.1", "npm-run-all": "^4.1.5", "prettier": "^2.3.1", "sinon": "^7.5.0", diff --git a/test/processes.test.js b/test/processes.test.js index 1949f06..7560b88 100644 --- a/test/processes.test.js +++ b/test/processes.test.js @@ -6,11 +6,11 @@ const { promisify } = require('util'); const execFile = promisify(require('child_process').execFile); const assert = require('assertive'); -const { each } = require('lodash'); const { patchFs } = require('fs-monkey'); const { ufs } = require('unionfs'); const { Volume } = require('memfs'); const sinon = require('sinon'); +const debug = require('debug')('testium-core:test:processes'); const Config = require('../lib/config'); const launchAllProcesses = require('../lib/processes'); @@ -18,7 +18,10 @@ const launchAllProcesses = require('../lib/processes'); const HELLO_WORLD = path.resolve(__dirname, '../examples/hello-world'); function killProcs(procs) { - each(procs, ({ rawProcess }) => rawProcess.kill()); + for (const [name, { rawProcess }] of Object.entries(procs)) { + debug(`killing ${name} process`); + rawProcess.kill(); + } } describe('Launch all processes PhantomJS', () => { @@ -74,18 +77,21 @@ describe('Launch all processes Chrome', () => { procs = undefined; }); - afterEach(() => { + afterEach(async () => { config = null; process.env.DISPLAY = display; if (procs) killProcs(procs); }); it('launches all the processes', async () => { + config.set('processes', { + nc: { command: 'nc', commandArgs: ['-l', '%port%'] }, + }); procs = await launchAllProcesses(config); const procNames = Object.keys(procs).sort(); assert.deepEqual( 'Spawns app, chrome, and proxy', - ['application', 'chromedriver', 'proxy'], + ['application', 'chromedriver', 'nc', 'proxy'], procNames ); }); diff --git a/test/processes/extra.test.js b/test/processes/extra.test.js new file mode 100644 index 0000000..8431ec5 --- /dev/null +++ b/test/processes/extra.test.js @@ -0,0 +1,79 @@ +'use strict'; + +const assert = require('assert'); +const { createServer } = require('net'); + +const findOpenPort = require('find-open-port'); + +const { checkExtraProcs } = require('../../lib/processes/extra'); + +describe('extra processes in config', () => { + /** @type {import('net').Server | undefined} */ + let server; + afterEach(async () => { + if (server) { + await new Promise(r => server.close(r)); + server = undefined; + } + }); + + it('returns server objects for extra procs', async () => { + // don't parallelize to avoid collisions + const freePort1 = await findOpenPort.findPort(); + const freePort2 = await findOpenPort.findPort(); + + server = createServer(); + await new Promise(r => server.listen({ port: 0, host: '127.0.0.1' }, r)); + const usedPort = server.address().port; + + const processes = { + reuse1: { + command: 'nc', + commandArgs: ['-l', '%port%'], + reuseExisting: true, + port: freePort1, + }, + reuse2: { + command: 'nc', + commandArgs: ['-l', '%port%'], + reuseExisting: true, + port: usedPort, + }, + start1: { + command: 'nc', + commandArgs: ['-l', '%port%'], + port: freePort2, + }, + start2: { + command: 'nc', + commandArgs: ['-l', '%port%'], + }, + }; + + const servers = await checkExtraProcs(processes); + + assert.ok( + servers.every(s => s.name !== 'reuse2'), + 'reuseExisting with port in use should not be in servers list' + ); + + assert.strictEqual(servers.length, 3); + assert.deepStrictEqual( + servers.find(s => s.name === 'reuse1').getOptions(), + { + command: 'nc', + commandArgs: ['-l', '%port%'], + port: freePort1, + } + ); + }); + + it('errors if configs are incorrect', async () => { + await assert.rejects( + checkExtraProcs({ + kaboom: { reuseExisting: true }, + }), + { message: /reuseExisting.+static port/ } + ); + }); +});