1import * as Log from '../log'; 2import { guardAsync } from './fn'; 3 4type AsyncExitHook = (signal: NodeJS.Signals) => void | Promise<void>; 5 6const PRE_EXIT_SIGNALS: NodeJS.Signals[] = ['SIGHUP', 'SIGINT', 'SIGTERM', 'SIGBREAK']; 7 8// We create a queue since Node.js throws an error if we try to append too many listeners: 9// (node:4405) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 SIGINT listeners added to [process]. Use emitter.setMaxListeners() to increase limit 10const queue: AsyncExitHook[] = []; 11 12let unsubscribe: (() => void) | null = null; 13 14/** Add functions that run before the process exits. Returns a function for removing the listeners. */ 15export function installExitHooks(asyncExitHook: AsyncExitHook): () => void { 16 // We need to instantiate the master listener the first time the queue is used. 17 if (!queue.length) { 18 // Track the master listener so we can remove it later. 19 unsubscribe = attachMasterListener(); 20 } 21 22 queue.push(asyncExitHook); 23 24 return () => { 25 const index = queue.indexOf(asyncExitHook); 26 if (index >= 0) { 27 queue.splice(index, 1); 28 } 29 // Clean up the master listener if we don't need it anymore. 30 if (!queue.length) { 31 unsubscribe?.(); 32 } 33 }; 34} 35 36// Create a function that runs before the process exits and guards against running multiple times. 37function createExitHook(signal: NodeJS.Signals) { 38 return guardAsync(async () => { 39 Log.debug(`pre-exit (signal: ${signal}, queue length: ${queue.length})`); 40 41 for (const [index, hookAsync] of Object.entries(queue)) { 42 try { 43 await hookAsync(signal); 44 } catch (error: any) { 45 Log.debug(`Error in exit hook: %O (queue: ${index})`, error); 46 } 47 } 48 49 Log.debug(`post-exit (code: ${process.exitCode ?? 0})`); 50 51 process.exit(); 52 }); 53} 54 55function attachMasterListener() { 56 const hooks: [NodeJS.Signals, () => any][] = []; 57 for (const signal of PRE_EXIT_SIGNALS) { 58 const hook = createExitHook(signal); 59 hooks.push([signal, hook]); 60 process.on(signal, hook); 61 } 62 return () => { 63 for (const [signal, hook] of hooks) { 64 process.removeListener(signal, hook); 65 } 66 }; 67} 68