index.js 4.08 KB
var RSVP = require('rsvp');

var exit;
var handlers = [];
var lastTime;
var isExiting = false;

process.on('beforeExit', function (code) {
  if (handlers.length === 0) { return; }

  var own = lastTime = module.exports._flush(lastTime, code)
    .finally(function () {
      // if an onExit handler has called process.exit, do not disturb
      // `lastTime`.
      //
      // Otherwise, clear `lastTime` so that we know to synchronously call the
      // real `process.exit` with the given exit code, when our captured
      // `process.exit` is called during a `process.on('exit')` handler
      //
      // This is impossible to reason about, don't feel bad.  Just look at
      // test-natural-exit-subprocess-error.js
      if (own === lastTime) {
        lastTime = undefined;
      }
    });
});

// This exists only for testing
module.exports._reset = function () {
  module.exports.releaseExit();
  handlers = [];
  lastTime = undefined;
  isExiting = false;
  firstExitCode = undefined;
}

/*
 * To allow cooperative async exit handlers, we unfortunately must hijack
 * process.exit.
 *
 * It allows a handler to ensure exit, without that exit handler impeding other
 * similar handlers
 *
 * for example, see: https://github.com/sindresorhus/ora/issues/27
 *
 */
module.exports.releaseExit = function() {
  if (exit) {
    process.exit = exit;
    exit = null;
  }
};

var firstExitCode;

module.exports.captureExit = function() {
  if (exit) {
    // already captured, no need to do more work
    return;
  }
  exit = process.exit;

  process.exit = function(code) {
    if (handlers.length === 0 && lastTime === undefined) {
      // synchronously exit.
      //
      // We do this brecause either
      //
      //  1.  The process exited due to a call to `process.exit` but we have no
      //      async work to do because no handlers had been attached.  It
      //      doesn't really matter whether we take this branch or not in this
      //      case.
      //
      //  2.  The process exited naturally.  We did our async work during
      //      `beforeExit` and are in this function because someone else has
      //      called `process.exit` during an `on('exit')` hook.  The only way
      //      for us to preserve the exit code in this case is to exit
      //      synchronously.
      //
      return exit.call(process, code);
    }

    if (firstExitCode === undefined) {
      firstExitCode = code;
    }
    var own = lastTime = module.exports._flush(lastTime, firstExitCode)
      .then(function() {
        // if another chain has started, let it exit
        if (own !== lastTime) { return; }
        exit.call(process, firstExitCode);
      })
      .catch(function(error) {
        // if another chain has started, let it exit
        if (own !== lastTime) {
          throw error;
        }
        console.error(error);
        exit.call(process, 1);
      });
  };
};

module.exports._handlers = handlers;
module.exports._flush = function(lastTime, code) {
  isExiting = true;
  var work = handlers.splice(0, handlers.length);

  return RSVP.Promise.resolve(lastTime).
    then(function() {
      var firstRejected;
      return RSVP.allSettled(work.map(function(handler) {
        return RSVP.resolve(handler.call(null, code)).catch(function(e) {
          if (!firstRejected) {
            firstRejected = e;
          }
          throw e;
        });
      })).then(function(results) {
        if (firstRejected) {
          throw firstRejected;
        }
      });
    });
};

module.exports.onExit = function(cb) {
  if (!exit) {
    throw new Error('Cannot install handler when exit is not captured.  Call `captureExit()` first');
  }
  if (isExiting) {
    throw new Error('Cannot install handler while `onExit` handlers are running.');
  }
  var index = handlers.indexOf(cb);

  if (index > -1) { return; }
  handlers.push(cb);
};

module.exports.offExit = function(cb) {
  var index = handlers.indexOf(cb);

  if (index < 0) { return; }

  handlers.splice(index, 1);
};

module.exports.exit  = function() {
  exit.apply(process, arguments);
};

module.exports.listenerCount = function() {
  return handlers.length;
};