/* eslint max-len: ["error", { "code": 100, "ignoreComments": true }]*/
/**
* @module childService
*/
// @flow
const _ = require('lodash');
const Promise = require('bluebird');
const childProcess = require('child_process');
const chalk = require('chalk');
const serviceControl = require('../service_control');
const universe = require('../universe');
const { get, unset } = universe.namespaceFactory('_cukelib');
const killProcWhenOrphaned = function (proc, name) {
const removeListenerList =
'exit SIGHUP SIGINT SIGQUIT SIGILL SIGABRT SIGFPE SIGSEGV SIGPIPE SIGTERM SIGBUS'
.split(/\s+/)
.map((sig) => {
const killFn = () => {
unset(`_services.${name}`);
proc.kill('SIGTERM');
};
process.on(sig, killFn);
return () => process.removeListener(sig, killFn);
});
return () => removeListenerList.map((rmListener) => rmListener());
};
const promiseToResolveOnMatch = (stream, matchTarget) =>
new Promise((resolve) => {
const resolveOnMatch = (data) => {
if (data.toString().match(matchTarget)) {
resolve(data);
stream.removeListener('data', resolveOnMatch);
}
};
stream.on('data', resolveOnMatch);
});
const childService =
module.exports = {
/**
* initialize - Initializes the childService using the serviceControl module
*
* Typically called at the top of cucumber support file that uses the childService using the
* cucumber context that contains `Before`, `After`, `Given`, etc.
* Usage:
*
* - `childService.initialize.call(this); `
*
* @returns {undefined}
*/
initialize() {
return serviceControl.initialize.call(this);
},
/**
* getService - Given a childService name returns the childService object.
*
* @param {string} name
*
* @returns {Object} childService
*/
getService(name: string) {
return serviceControl.getService(`child.${name}`);
},
makeSpawnConfig(spawnArgs: Object) {
if (!spawnArgs.name) throw new Error('name is a required argument');
const spawnDefaultArgs = {
args: [],
options: {},
isReadyMatch: /./,
isReady(proc) {
return Promise.race([
promiseToResolveOnMatch(proc.stdout, spawnArgs.isReadyMatch),
promiseToResolveOnMatch(proc.stderr, spawnArgs.isReadyMatch),
]);
},
stderrHandler(data) {
// eslint-disable-next-line no-console
console.error(chalk.magenta(`${spawnArgs.name}.stderr: ${data}`));
},
stdoutHandler(data) {
// eslint-disable-next-line no-console
console.log(chalk.magenta(`${spawnArgs.name}.stdout: ${data}`));
},
errorHandler(err) {
// eslint-disable-next-line no-console
console.log(chalk.magenta(`${spawnArgs.name} Error:`, err));
}
};
return _.defaults(spawnArgs, spawnDefaultArgs);
},
launch(configOverrides: Object = {}, childLauncher: Function) {
const config = childService.makeSpawnConfig(configOverrides);
const start = () => {
let isProcReady = false;
return childLauncher(config)
.then((proc) => {
proc.stderr.on('data', config.stderrHandler);
proc.stdout.on('data', config.stdoutHandler);
proc.on('error', config.errorHandler);
const exitPromise = new Promise((resolve, reject) => {
proc.once('exit', (code) => {
if (isProcReady) {
// This is the resolution case for the stop function.
const msg = `Server "${config.name}" exited with code ${code}`;
if (get(`_services.${config.name}`)) {
// This happens if the proc exits from something other than the stop.
reject(new Error(msg));
} else {
// This is the normal exit case from the stop.
resolve(msg);
}
} else {
// This is the resolution case for the Promise.race against isReadyPromise
reject(new Error(`Server "${config.name}" exited with code ${code} before ready`));
}
});
});
const removeOrphanProcListeners = killProcWhenOrphaned(proc, config.name);
const isReadyPromise = config.isReady.call(config, proc)
.then(() => {
isProcReady = true;
return {
config,
proc,
stop: () => {
removeOrphanProcListeners();
proc.kill('SIGTERM');
return exitPromise;
},
};
});
return Promise.race([isReadyPromise, exitPromise]);
});
};
return serviceControl.launchService(config.name, start);
},
/**
* spawn - Launches a Node child process from a shell command via
*
* [`require('child_process').spawn(...)`](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options)
*
* The `spawnArgs` parameter allows these options:
*
* - `name: string` The name of the cukelib service (required).
* - `cmd: string` Spawn command argument (required).
* - `args: [string]` Spawn args argument
* - `options: Object` [childProcess.spawn options argument (env, cwd, etc.)](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options)
* - `isReadyMatch: string|RegExp` default: `/./` Pattern that is matched from stdout or stderr to indicate the child process is ready.
* - `isReady: (proc: childProcess) => Promise` the promise is resolved when the child process is ready. The default is to resolve when data from stdout or stderr matches the `isReadyMatch` pattern.
* - `stderrHandler: Function(data: string)` default is to print via `console.error(chalk.magenta(...))`
* - `stdoutHandler: Function(data: string)` default is to print via `console.log(chalk.magenta(...))`. Assign the function `(data) => null` for a "quiet" output.
* - `errorHandler: Function(err: Error)` default is to print the err via `console.error(chalk.magenta(...))`
*
* @param {Object} [spawnArgs={}] see above
*
* @returns {Promise} launchService promise
*/
spawn(spawnArgs: Object = {}) {
if (!spawnArgs.cmd) throw new Error('cmd is a required argument');
return childService.launch(spawnArgs, (config) =>
Promise.resolve(childProcess.spawn(config.cmd, config.args, config.options)));
},
};
module.exports = childService;