lib/utilities/will-interrupt-process.js
'use strict';
// Allows to setup process interruption handlers.
// The process can be interrupted when ges SIGINT, SIGTERM signal,
// something called process.exit(exitCode) or CTRL+C was pressed.
//
// Node.js doesn't allow to perform async tasks when the process is exiting.
// Also there are some work arounds for exit in the node.js ecosystem.
//
// In order to supply reliable process exit phase, `will-interrupt-process`
// is tightly integrated with `capture-exit` which allows us to perform async cleanup
// on `process.exit()` and control the final exit code.
const captureExit = require('capture-exit');
const EventEmitter = require('events');
const handlers = [];
let _process, _processCapturedLocation, windowsCtrlCTrap, originalIsRaw;
module.exports = {
  capture(outerProcess) {
    if (_process) {
      throw new Error(`process already captured at: \n\n${_processCapturedLocation.stack}`);
    }
    if (outerProcess instanceof EventEmitter === false) {
      throw new Error('attempt to capture bad process instance');
    }
    _process = outerProcess;
    _processCapturedLocation = new Error();
    // ember-cli and user apps have many dependencies, many of which require
    // process.addListener('exit', ....) for cleanup, by default this limit for
    // such listeners is 10, recently users have been increasing this and not to
    // their fault, rather they are including large and more diverse sets of
    // node_modules.
    //
    // https://github.com/babel/ember-cli-babel/issues/76
    _process.setMaxListeners(1000);
    // work around misbehaving libraries, so we can correctly cleanup before actually exiting.
    captureExit.captureExit();
  },
  /**
   * Drops all the interruption handlers and disables an ability to add new one
   *
   * Note: We don't call `captureExit.releaseExit() here.
   * In some rare scenarios it can lead to the hard to debug issues.
   * see: https://github.com/ember-cli/ember-cli/issues/6779#issuecomment-280940358
   *
   * We can more or less feel comfortable with a captured exit because it behaves very
   * similar to the original `exit` except of cases when we need to do cleanup before exit.
   *
   * @private
   * @method release
   */
  release() {
    while (handlers.length > 0) {
      this.removeHandler(handlers[0]);
    }
    _process = null;
    _processCapturedLocation = null;
  },
  /**
   * Add process interruption handler
   *
   * When the first handler is added then automatically
   * sets up process interruption signals listeners
   *
   * @private
   * @method addHandler
   * @param {function} cb   Callback to be called when process interruption fired
   */
  addHandler(cb) {
    if (!_process) {
      throw new Error('process is not captured');
    }
    let index = handlers.indexOf(cb);
    if (index > -1) {
      return;
    }
    if (handlers.length === 0) {
      setupSignalsTrap();
    }
    handlers.push(cb);
    captureExit.onExit(cb);
  },
  /**
   * Remove process interruption handler
   *
   * If there are no remaining handlers after removal
   * then clean up all the process interruption signal listeners
   *
   * @private
   * @method removeHandler
   * @param {function} cb   Callback to be removed
   */
  removeHandler(cb) {
    let index = handlers.indexOf(cb);
    if (index < 0) {
      return;
    }
    handlers.splice(index, 1);
    captureExit.offExit(cb);
    if (handlers.length === 0) {
      teardownSignalsTrap();
    }
  },
};
/**
 * Sets up listeners for interruption signals
 *
 * When one of these signals is caught than raise process.exit()
 * which enforces `capture-exit` to run registered interruption handlers
 *
 * @method setupSignalsTrap
 */
function setupSignalsTrap() {
  _process.on('SIGINT', exit);
  _process.on('SIGTERM', exit);
  _process.on('message', onMessage);
  if (isWindowsTTY(_process)) {
    trapWindowsSignals(_process);
  }
}
/**
 * Removes interruption signal listeners and tears down capture-exit
 *
 * @method teardownSignalsTrap
 */
function teardownSignalsTrap() {
  _process.removeListener('SIGINT', exit);
  _process.removeListener('SIGTERM', exit);
  _process.removeListener('message', onMessage);
  if (isWindowsTTY(_process)) {
    cleanupWindowsSignals(_process);
  }
}
/**
 * Suppresses "Terminate batch job (Y/N)" confirmation on Windows
 *
 * @method trapWindowsSignals
 */
function trapWindowsSignals(_process) {
  const stdin = _process.stdin;
  originalIsRaw = stdin.isRaw;
  // This is required to capture Ctrl + C on Windows
  stdin.setRawMode(true);
  windowsCtrlCTrap = function (data) {
    if (data.length === 1 && data[0] === 0x03) {
      _process.emit('SIGINT');
    }
  };
  stdin.on('data', windowsCtrlCTrap);
}
function cleanupWindowsSignals(_process) {
  const stdin = _process.stdin;
  stdin.setRawMode(originalIsRaw);
  stdin.removeListener('data', windowsCtrlCTrap);
}
function isWindowsTTY(_process) {
  return /^win/.test(_process.platform) && _process.stdin && _process.stdin.isTTY;
}
function exit() {
  _process.exit();
}
function onMessage(message) {
  if (message.kill) {
    exit();
  }
}