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 };