Source: child_service/index.js

/* 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;