index.js 4.08 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
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;
};