thedesk/app/node_modules/async-exit-hook/index.js
2019-09-12 23:38:13 +09:00

172 lines
3.6 KiB
JavaScript

'use strict';
const hooks = [];
const errHooks = [];
let called = false;
let waitingFor = 0;
let asyncTimeoutMs = 10000;
const events = {};
const filters = {};
function exit(exit, code, err) {
// Helper functions
let doExitDone = false;
function doExit() {
if (doExitDone) {
return;
}
doExitDone = true;
if (exit === true) {
// All handlers should be called even if the exit-hook handler was registered first
process.nextTick(process.exit.bind(null, code));
}
}
// Async hook callback, decrements waiting counter
function stepTowardExit() {
process.nextTick(() => {
if (--waitingFor === 0) {
doExit();
}
});
}
// Runs a single hook
function runHook(syncArgCount, err, hook) {
// Cannot perform async hooks in `exit` event
if (exit && hook.length > syncArgCount) {
// Hook is async, expects a finish callback
waitingFor++;
if (err) {
// Pass error, calling uncaught exception handlers
return hook(err, stepTowardExit);
}
return hook(stepTowardExit);
}
// Hook is synchronous
if (err) {
// Pass error, calling uncaught exception handlers
return hook(err);
}
return hook();
}
// Only execute hooks once
if (called) {
return;
}
called = true;
// Run hooks
if (err) {
// Uncaught exception, run error hooks
errHooks.map(runHook.bind(null, 1, err));
}
hooks.map(runHook.bind(null, 0, null));
if (waitingFor) {
// Force exit after x ms (10000 by default), even if async hooks in progress
setTimeout(() => {
doExit();
}, asyncTimeoutMs);
} else {
// No asynchronous hooks, exit immediately
doExit();
}
}
// Add a hook
function add(hook) {
hooks.push(hook);
if (hooks.length === 1) {
add.hookEvent('exit');
add.hookEvent('beforeExit', 0);
add.hookEvent('SIGHUP', 128 + 1);
add.hookEvent('SIGINT', 128 + 2);
add.hookEvent('SIGTERM', 128 + 15);
add.hookEvent('SIGBREAK', 128 + 21);
// PM2 Cluster shutdown message. Caught to support async handlers with pm2, needed because
// explicitly calling process.exit() doesn't trigger the beforeExit event, and the exit
// event cannot support async handlers, since the event loop is never called after it.
add.hookEvent('message', 0, function (msg) { // eslint-disable-line prefer-arrow-callback
if (msg !== 'shutdown') {
return true;
}
});
}
}
// New signal / event to hook
add.hookEvent = function (event, code, filter) {
events[event] = function () {
const eventFilters = filters[event];
for (let i = 0; i < eventFilters.length; i++) {
if (eventFilters[i].apply(this, arguments)) {
return;
}
}
exit(code !== undefined && code !== null, code);
};
if (!filters[event]) {
filters[event] = [];
}
if (filter) {
filters[event].push(filter);
}
process.on(event, events[event]);
};
// Unhook signal / event
add.unhookEvent = function (event) {
process.removeListener(event, events[event]);
delete events[event];
delete filters[event];
};
// List hooked events
add.hookedEvents = function () {
const ret = [];
for (const name in events) {
if ({}.hasOwnProperty.call(events, name)) {
ret.push(name);
}
}
return ret;
};
// Add an uncaught exception handler
add.uncaughtExceptionHandler = function (hook) {
errHooks.push(hook);
if (errHooks.length === 1) {
process.once('uncaughtException', exit.bind(null, true, 1));
}
};
// Add an unhandled rejection handler
add.unhandledRejectionHandler = function (hook) {
errHooks.push(hook);
if (errHooks.length === 1) {
process.once('unhandledRejection', exit.bind(null, true, 1));
}
};
// Configure async force exit timeout
add.forceExitTimeout = function (ms) {
asyncTimeoutMs = ms;
};
// Export
module.exports = add;