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