Source: workers/child.js

/*!
 * child.js - child processes for hsd
 * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
 * https://github.com/handshake-org/hsd
 */

'use strict';

const EventEmitter = require('events');
const path = require('path');
const cp = require('child_process');

const children = new Set();

let exitBound = false;

/**
 * Child
 * Represents a child process.
 * @alias module:workers.Child
 * @extends EventEmitter
 * @ignore
 */

class Child extends EventEmitter {
  /**
   * Represents a child process.
   * @constructor
   * @param {String} file
   */

  constructor(file) {
    super();

    bindExit();
    children.add(this);

    this.init(file);
  }

  /**
   * Test whether child process support is available.
   * @returns {Boolean}
   */

  static hasSupport() {
    return true;
  }

  /**
   * Initialize child process (node.js).
   * @private
   * @param {String} file
   */

  init(file) {
    const bin = process.argv[0];
    const filename = path.resolve(__dirname, file);
    const options = { stdio: 'pipe', env: process.env };

    this.child = cp.spawn(bin, [filename], options);

    this.child.unref();
    this.child.stdin.unref();
    this.child.stdout.unref();
    this.child.stderr.unref();

    this.child.on('error', (err) => {
      this.emit('error', err);
    });

    this.child.once('exit', (code, signal) => {
      children.delete(this);
      this.emit('exit', code == null ? -1 : code, signal);
    });

    this.child.stdin.on('error', (err) => {
      this.emit('error', err);
    });

    this.child.stdout.on('error', (err) => {
      this.emit('error', err);
    });

    this.child.stderr.on('error', (err) => {
      this.emit('error', err);
    });

    this.child.stdout.on('data', (data) => {
      this.emit('data', data);
    });
  }

  /**
   * Send data to child process.
   * @param {Buffer} data
   * @returns {Boolean}
   */

  write(data) {
    return this.child.stdin.write(data);
  }

  /**
   * Destroy the child process.
   */

  destroy() {
    this.child.kill('SIGTERM');
  }
}

/**
 * Cleanup all child processes.
 * @private
 */

function bindExit() {
  if (exitBound)
    return;

  exitBound = true;

  listenExit(() => {
    for (const child of children)
      child.destroy();
  });
}

/**
 * Listen for exit.
 * @param {Function} handler
 * @private
 */

function listenExit(handler) {
  const onSighup = () => {
    process.exit(1 | 0x80);
  };

  const onSigint = () => {
    process.exit(2 | 0x80);
  };

  const onSigterm = () => {
    process.exit(15 | 0x80);
  };

  const onError = (err) => {
    if (err && err.stack)
      console.error(String(err.stack));
    else
      console.error(String(err));

    process.exit(1);
  };

  process.once('exit', handler);

  if (process.listenerCount('SIGHUP') === 0)
    process.once('SIGHUP', onSighup);

  if (process.listenerCount('SIGINT') === 0)
    process.once('SIGINT', onSigint);

  if (process.listenerCount('SIGTERM') === 0)
    process.once('SIGTERM', onSigterm);

  if (process.listenerCount('uncaughtException') === 0)
    process.once('uncaughtException', onError);

  process.on('newListener', (name) => {
    switch (name) {
      case 'SIGHUP':
        process.removeListener(name, onSighup);
        break;
      case 'SIGINT':
        process.removeListener(name, onSigint);
        break;
      case 'SIGTERM':
        process.removeListener(name, onSigterm);
        break;
      case 'uncaughtException':
        process.removeListener(name, onError);
        break;
    }
  });
}

/*
 * Expose
 */

module.exports = Child;