blob: c940db4dcf887be7a38c79827060e2de0336d67e [file] [log] [blame] [raw]
// +----------------------------------------------------------------------+
// | node-graceful v3 (https://github.com/mrbar42/node-graceful) |
// | Graceful process exit manager. |
// |----------------------------------------------------------------------|
export type GracefulListener = (
signal: string,
details?: object,
) => void | any | Promise<any> | Promise<void> | Promise<Error>;
export type GracefulSubscription = () => void;
export class Graceful {
private static DEADLY_SIGNALS = ['SIGTERM', 'SIGINT', 'SIGBREAK', 'SIGHUP'];
public static exitOnDouble = true;
public static timeout = 30000;
private static _captureExceptions = false;
private static _captureRejections = false;
private static listeners: GracefulListener[] = [];
private static isRegistered = false;
private static isExiting = false;
private static exceptionListener = (event: any) => {
process.exitCode = 1;
Graceful.onDeadlyEvent('uncaughtException', event);
};
private static rejectionListener = (event: any) => {
process.exitCode = 1;
Graceful.onDeadlyEvent('unhandledRejection', event);
};
private static signalsListeners: {[signal: string]: (event: any) => void} = {};
public static get captureExceptions() {
return Graceful._captureExceptions;
}
public static set captureExceptions(newValue: boolean) {
if (Graceful._captureExceptions === newValue) return;
Graceful._captureExceptions = newValue;
if (Graceful._captureExceptions) {
process.on('uncaughtException' as any, Graceful.exceptionListener);
} else {
process.removeListener('uncaughtException', Graceful.exceptionListener);
}
}
public static get captureRejections() {
return Graceful._captureRejections;
}
public static set captureRejections(newValue: boolean) {
if (Graceful._captureRejections === newValue) return;
Graceful._captureRejections = newValue;
if (Graceful._captureRejections) {
process.on('unhandledRejection' as any, Graceful.rejectionListener);
} else {
process.removeListener('unhandledRejection', Graceful.rejectionListener);
}
}
public static on(signal: 'exit', listener: GracefulListener): GracefulSubscription {
if (signal !== 'exit') throw new Error("Only supports 'exit' signal");
Graceful.listeners.push(listener);
Graceful.updateRegistration();
return () => Graceful.off('exit', listener);
}
public static off(signal: 'exit', listener: GracefulListener) {
if (signal !== 'exit') throw new Error("Only supports 'exit' signal");
const index = Graceful.listeners.indexOf(listener);
if (index !== -1) Graceful.listeners.splice(index, 1);
Graceful.updateRegistration();
}
public static clear() {
Graceful.listeners.splice(0, Infinity);
Graceful.updateRegistration();
}
public static exit(code?: number | string, signal = 'SIGTERM') {
const exitSignal = typeof code === 'string' ? code : signal;
if (typeof code === 'number') {
process.exitCode = code;
}
Graceful.onDeadlyEvent(exitSignal, {reason: 'Manual call to Graceful.exit()'});
}
private static onDeadlyEvent(signal: string, details?: object) {
// console.log(signal, details);
if (Graceful.isExiting) {
if (Graceful.exitOnDouble) Graceful.killProcess(true);
return;
}
const listeners = Graceful.listeners.slice(0);
Graceful.isExiting = true;
let completedListeners = 0;
const done = () => {
completedListeners++;
if (completedListeners === listeners.length) {
Graceful.killProcess(false);
}
};
if (Number(Graceful.timeout)) {
const timeoutRef = setTimeout(() => Graceful.killProcess(true), Graceful.timeout);
if (timeoutRef && timeoutRef.unref) timeoutRef.unref();
}
for (const listener of listeners) {
Graceful.invokeListener(listener, done, signal, details);
}
}
private static invokeListener(listener: GracefulListener, done: () => void, signal: string, details?: object) {
let invoked = false;
const listenerDone = () => {
if (!invoked) {
invoked = true;
done();
}
};
const retVal: any = listener(signal, details);
// allow returning a promise
if (retVal && typeof retVal.then === 'function') {
retVal.then(listenerDone, listenerDone);
} else {
listenerDone();
}
}
private static updateRegistration() {
if (Graceful.listeners.length && !Graceful.isRegistered) {
for (const deadlySignal of Graceful.DEADLY_SIGNALS) {
const listener = () => Graceful.onDeadlyEvent(deadlySignal);
Graceful.signalsListeners[deadlySignal] = listener;
process.on(deadlySignal as any, listener);
}
Graceful.isRegistered = true;
} else if (!Graceful.listeners.length && Graceful.isRegistered) {
for (const deadlySignal of Graceful.DEADLY_SIGNALS) {
const listener = Graceful.signalsListeners[deadlySignal];
if (listener) {
process.removeListener(deadlySignal, listener);
delete Graceful.signalsListeners[deadlySignal];
}
}
Graceful.isRegistered = false;
}
}
private static killProcess(force: boolean) {
process.exit(process.exitCode || (force ? 1 : 0));
}
}