index.js 3.63 KB
Newer Older
huahua committed
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 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
'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;