index.js 4.1 KB

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