-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathexit-process.ts
More file actions
126 lines (111 loc) · 3.89 KB
/
exit-process.ts
File metadata and controls
126 lines (111 loc) · 3.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import os from 'node:os';
import process from 'node:process';
// POSIX shells convention: exit status = 128 + signal number
// https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html#:~:text=When%20a%20command%20terminates%20on%20a%20fatal%20signal%20whose%20number%20is%20N%2C%20Bash%20uses%20the%20value%20128%2BN%20as%20the%20exit%20status.
const UNIX_SIGNAL_EXIT_CODE_OFFSET = 128;
const unixSignalExitCode = (signalNumber: number) =>
UNIX_SIGNAL_EXIT_CODE_OFFSET + signalNumber;
const SIGINT_CODE = 2;
const SIGTERM_CODE = 15;
const SIGQUIT_CODE = 3;
export const SIGNAL_EXIT_CODES = (): Record<SignalName, number> => {
const isWindowsRuntime = os.platform() === 'win32';
return {
SIGINT: isWindowsRuntime ? SIGINT_CODE : unixSignalExitCode(SIGINT_CODE),
SIGTERM: unixSignalExitCode(SIGTERM_CODE),
SIGQUIT: unixSignalExitCode(SIGQUIT_CODE),
};
};
export const DEFAULT_FATAL_EXIT_CODE = 1;
export type SignalName = 'SIGINT' | 'SIGTERM' | 'SIGQUIT';
export type FatalKind = 'uncaughtException' | 'unhandledRejection';
export type CloseReason =
| { kind: 'signal'; signal: SignalName }
| { kind: 'fatal'; fatal: FatalKind }
| { kind: 'exit' };
export type ExitHandlerOptions = {
onExit?: (code: number, reason: CloseReason) => void;
onError?: (err: unknown, kind: FatalKind) => void;
exitOnFatal?: boolean;
exitOnSignal?: boolean;
fatalExitCode?: number;
};
/**
*
* @param options - Options for the exit handler
* @param options.onExit - Callback to be called when the process exits
* @param options.onError - Callback to be called when an error occurs
* @param options.exitOnFatal - Whether to exit the process on fatal errors
* @param options.exitOnSignal - Whether to exit the process on signals
* @param options.fatalExitCode - The exit code to use for fatal errors
* @returns A function to unsubscribe from the exit handlers
*/
// eslint-disable-next-line max-lines-per-function
export function subscribeProcessExit(
options: ExitHandlerOptions = {},
): () => void {
// eslint-disable-next-line functional/no-let
let closedReason: CloseReason | undefined;
const {
onExit,
onError,
exitOnFatal = false,
exitOnSignal = false,
fatalExitCode = DEFAULT_FATAL_EXIT_CODE,
} = options;
const close = (code: number, reason: CloseReason) => {
if (closedReason) {
return;
}
closedReason = reason;
onExit?.(code, reason);
};
const uncaughtExceptionHandler = (err: unknown) => {
onError?.(err, 'uncaughtException');
if (exitOnFatal) {
close(fatalExitCode, {
kind: 'fatal',
fatal: 'uncaughtException',
});
}
};
const unhandledRejectionHandler = (reason: unknown) => {
onError?.(reason, 'unhandledRejection');
if (exitOnFatal) {
close(fatalExitCode, {
kind: 'fatal',
fatal: 'unhandledRejection',
});
}
};
const signalHandlers = (['SIGINT', 'SIGTERM', 'SIGQUIT'] as const).map(
signal => {
const handler = () => {
close(SIGNAL_EXIT_CODES()[signal], { kind: 'signal', signal });
if (exitOnSignal) {
// eslint-disable-next-line unicorn/no-process-exit,n/no-process-exit
process.exit(SIGNAL_EXIT_CODES()[signal]);
}
};
process.on(signal, handler);
return { signal, handler };
},
);
const exitHandler = (code: number) => {
if (closedReason) {
return;
}
close(code, { kind: 'exit' });
};
process.on('uncaughtException', uncaughtExceptionHandler);
process.on('unhandledRejection', unhandledRejectionHandler);
process.on('exit', exitHandler);
return () => {
process.removeListener('uncaughtException', uncaughtExceptionHandler);
process.removeListener('unhandledRejection', unhandledRejectionHandler);
process.removeListener('exit', exitHandler);
signalHandlers.forEach(({ signal, handler }) => {
process.removeListener(signal, handler);
});
};
}