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