xref: /expo/packages/@expo/cli/src/utils/exit.ts (revision af644ed4)
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