index.js
  1  var RSVP = require('rsvp');
  2  
  3  var exit;
  4  var handlers = [];
  5  var lastTime;
  6  var isExiting = false;
  7  
  8  process.on('beforeExit', function (code) {
  9    if (handlers.length === 0) { return; }
 10  
 11    var own = lastTime = module.exports._flush(lastTime, code)
 12      .finally(function () {
 13        // if an onExit handler has called process.exit, do not disturb
 14        // `lastTime`.
 15        //
 16        // Otherwise, clear `lastTime` so that we know to synchronously call the
 17        // real `process.exit` with the given exit code, when our captured
 18        // `process.exit` is called during a `process.on('exit')` handler
 19        //
 20        // This is impossible to reason about, don't feel bad.  Just look at
 21        // test-natural-exit-subprocess-error.js
 22        if (own === lastTime) {
 23          lastTime = undefined;
 24        }
 25      });
 26  });
 27  
 28  // This exists only for testing
 29  module.exports._reset = function () {
 30    module.exports.releaseExit();
 31    handlers = [];
 32    lastTime = undefined;
 33    isExiting = false;
 34    firstExitCode = undefined;
 35  }
 36  
 37  /*
 38   * To allow cooperative async exit handlers, we unfortunately must hijack
 39   * process.exit.
 40   *
 41   * It allows a handler to ensure exit, without that exit handler impeding other
 42   * similar handlers
 43   *
 44   * for example, see: https://github.com/sindresorhus/ora/issues/27
 45   *
 46   */
 47  module.exports.releaseExit = function() {
 48    if (exit) {
 49      process.exit = exit;
 50      exit = null;
 51    }
 52  };
 53  
 54  var firstExitCode;
 55  
 56  module.exports.captureExit = function() {
 57    if (exit) {
 58      // already captured, no need to do more work
 59      return;
 60    }
 61    exit = process.exit;
 62  
 63    process.exit = function(code) {
 64      if (handlers.length === 0 && lastTime === undefined) {
 65        // synchronously exit.
 66        //
 67        // We do this brecause either
 68        //
 69        //  1.  The process exited due to a call to `process.exit` but we have no
 70        //      async work to do because no handlers had been attached.  It
 71        //      doesn't really matter whether we take this branch or not in this
 72        //      case.
 73        //
 74        //  2.  The process exited naturally.  We did our async work during
 75        //      `beforeExit` and are in this function because someone else has
 76        //      called `process.exit` during an `on('exit')` hook.  The only way
 77        //      for us to preserve the exit code in this case is to exit
 78        //      synchronously.
 79        //
 80        return exit.call(process, code);
 81      }
 82  
 83      if (firstExitCode === undefined) {
 84        firstExitCode = code;
 85      }
 86      var own = lastTime = module.exports._flush(lastTime, firstExitCode)
 87        .then(function() {
 88          // if another chain has started, let it exit
 89          if (own !== lastTime) { return; }
 90          exit.call(process, firstExitCode);
 91        })
 92        .catch(function(error) {
 93          // if another chain has started, let it exit
 94          if (own !== lastTime) {
 95            throw error;
 96          }
 97          console.error(error);
 98          exit.call(process, 1);
 99        });
100    };
101  };
102  
103  module.exports._handlers = handlers;
104  module.exports._flush = function(lastTime, code) {
105    isExiting = true;
106    var work = handlers.splice(0, handlers.length);
107  
108    return RSVP.Promise.resolve(lastTime).
109      then(function() {
110        var firstRejected;
111        return RSVP.allSettled(work.map(function(handler) {
112          return RSVP.resolve(handler.call(null, code)).catch(function(e) {
113            if (!firstRejected) {
114              firstRejected = e;
115            }
116            throw e;
117          });
118        })).then(function(results) {
119          if (firstRejected) {
120            throw firstRejected;
121          }
122        });
123      });
124  };
125  
126  module.exports.onExit = function(cb) {
127    if (!exit) {
128      throw new Error('Cannot install handler when exit is not captured.  Call `captureExit()` first');
129    }
130    if (isExiting) {
131      throw new Error('Cannot install handler while `onExit` handlers are running.');
132    }
133    var index = handlers.indexOf(cb);
134  
135    if (index > -1) { return; }
136    handlers.push(cb);
137  };
138  
139  module.exports.offExit = function(cb) {
140    var index = handlers.indexOf(cb);
141  
142    if (index < 0) { return; }
143  
144    handlers.splice(index, 1);
145  };
146  
147  module.exports.exit  = function() {
148    exit.apply(process, arguments);
149  };
150  
151  module.exports.listenerCount = function() {
152    return handlers.length;
153  };