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