-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.ts
252 lines (226 loc) · 8.33 KB
/
index.ts
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
import {createHook} from 'async_hooks';
import {writeSync} from 'fs';
import {signalsByName} from 'human-signals';
/**
* Add a callback function to be called upon process exit or death.
*
* @param callback The callback function with signature: (signal: CatchSignals, exitCode?: number,
* error?: Error) => undefined | void
*
* Typed to block async functions. Async functions will not work for 'exit' events, triggered from
* process.exit(), but will work with other events this catches. If you wish to perform async
* functions have this callback call an async function but remember it won't be awaited if the
* signal is 'exit'.
* @returns The callback itself for chaining purposes.
*/
export function addExitCallback(callback: ExitCallback): ExitCallback {
// setup the exit handling once a callback has actually been added
setupProcessExitHandling();
callbacks.push(callback);
return callback;
}
/**
* Remove the given callback function from the list of callbacks to be called on process exit or death.
*
* @param callback The exact callback (exact by reference) added at some earlier point by addExitCallback.
* @returns The callback removed or undefined if the given callback was not found.
*/
export function removeExitCallback(callback: ExitCallback): ExitCallback | undefined {
// assume that at this point the user wants the handler to be setup
setupProcessExitHandling();
const index = callbacks.indexOf(callback);
return index > -1 ? callbacks.splice(index, 1)[0] : undefined;
}
/**
* This callback cannot be async because on process exit async calls can't be awaited and won't
* finish. See documentation on addExitCallback for details.
*/
export type ExitCallback = (
signal: CatchSignals,
exitCode?: number,
error?: Error,
) => void | undefined; // return type set to prevent passing in functions with Promise<void> return type
/**
* Various different signals that can be passed to ExitCallback as "signal". "unhandledRejection" is
* not a part of this because these are all turned into "uncaughtException" errors
*/
export type CatchSignals = InterceptedSignals | 'exit' | 'uncaughtException';
type InterceptedSignals = 'SIGINT' | 'SIGHUP' | 'SIGTERM' | 'SIGQUIT';
// used in the listener far setup below
const signals: InterceptedSignals[] = [
'SIGHUP',
// catches ctrl+c event
'SIGINT',
// catches "kill pid"
'SIGTERM',
'SIGQUIT',
];
/** The different signal types that can be passed to the exit callback. */
export const catchSignalStrings: CatchSignals[] = [
...signals,
'exit',
'uncaughtException',
];
function stringifyError(error: unknown): string {
if (customStringifyError) {
return customStringifyError(error);
}
if (error instanceof Error) {
return (error.stack || error.toString()) + '\n';
} else {
return String(error);
}
}
/**
* Allow customization of error message printing. Defaults to just printing the stack trace.
*
* @param errorStringifyFunction Function that accepts an error and returns a string
*/
export function registerStringifyError(errorStringifyFunction?: ErrorStringifyFunction): void {
// assume that at this point the user wants the handler to be setup
setupProcessExitHandling();
customStringifyError = errorStringifyFunction;
}
/**
* Used to create custom error logging.
*
* @param error Error that was thrown that can be used in the string
* @returns A string that will be printed as the error's stderr message
*/
export type ErrorStringifyFunction = (error: unknown) => string;
let customStringifyError: undefined | ErrorStringifyFunction;
/** Options to be configured immediately on setup instead of set later by their respective functions. */
export type SetupOptions = {
/** Enables logging immediately on setup */
loggingEnabled?: boolean;
/** Defines a custom error stringify function immediately on setup */
customErrorStringify?: ErrorStringifyFunction;
};
/**
* Setup process exit or death handlers without adding any callbacks
*
* @param options Setup options, see SetupOptions type for details
*/
export function setupCatchExit(options?: SetupOptions): void {
setupProcessExitHandling();
if (options) {
const {loggingEnabled, customErrorStringify} = options;
if (customErrorStringify) {
registerStringifyError(customErrorStringify);
}
if (loggingEnabled) {
enableLogging();
}
}
}
let loggingEnabled = false;
/**
* Enable logging of this package's methods.
*
* @param enable True (default) to enable, false to disable
* @returns The value of the passed or defaulted "enable" argument
*/
export function enableLogging(enable = true): boolean {
// assume that at this point the user wants the handler to be setup
setupProcessExitHandling();
loggingEnabled = enable;
return enable;
}
// console.log is async and these log functions must be sync
function log(value: string): void {
if (loggingEnabled) {
writeSync(1, value + '\n');
}
}
function logError(value: string): void {
writeSync(2, value);
}
const callbacks: ExitCallback[] = [];
// not sure what all the different async types mean but I seem to not care about at least these
const ignoredAsyncTypes = [
'TTYWRAP',
'SIGNALWRAP',
'PIPEWRAP',
];
const asyncHook = createHook({
init(id, type) {
if (!ignoredAsyncTypes.includes(type)) {
writeSync(
2,
`\nERROR: Async operation of type "${type}" was created in "process.exit" callback. This will not run to completion as "process.exit" will not complete async tasks.\n`,
);
}
},
});
let alreadySetup = false;
/**
* This is used to prevent double clean up (since the process.exit in exitHandler gets caught the
* first time, firing exitHandler again).
*/
let alreadyExiting = false;
function setupProcessExitHandling(): void {
if (alreadySetup) {
return;
}
// so the program will not close instantly
// process.stdin.resume();
function exitHandler(signal: CatchSignals, exitCode?: number, inputError?: Error): void {
log(`handling signal: ${signal} with code ${exitCode}`);
if (!alreadyExiting) {
log('setting alreadyExiting');
alreadyExiting = true;
try {
log(`Firing ${callbacks.length} callbacks`);
// only exit prevents async callbacks from completing
if (signal === 'exit') {
asyncHook.enable();
}
callbacks.forEach((callback) => callback(signal, exitCode, inputError));
asyncHook.disable();
} catch (callbackError) {
log('Error in callback');
// 7 here means there was an error in the exit handler, which there was if we got to this point
exitWithError(callbackError, 7);
}
if (inputError instanceof Error) {
exitWithError(inputError, exitCode);
} else {
process.exit(exitCode);
}
} else {
log('Already exiting, not doing anything');
return;
}
}
// prevents all exit codes from being 7 when they shouldn't be
function exitWithError(error: unknown, code?: number) {
log(`Exiting with error and code ${code}`);
logError(stringifyError(error));
process.exit(code);
}
signals.forEach((signal) =>
process.on(signal, () => {
const signalNumber = signalsByName[signal]?.number;
if (signalNumber == undefined) {
throw new Error(`Failed to find number for signal "${signal}"`);
}
exitHandler(signal, 128 + signalNumber);
}),
);
process.on('exit', (code) => {
log(`exit listener with code ${code}`);
exitHandler('exit', code);
});
process.on('unhandledRejection', (reason) => {
log('unhandledRejection listener');
const error = reason instanceof Error ? reason : new Error(reason ? `${reason}` : '');
error.name = 'UnhandledRejection';
throw error;
});
// catches uncaught exceptions
process.on('uncaughtException', (error) => {
log('uncaughtException listener');
exitHandler('uncaughtException', 1, error);
});
alreadySetup = true;
}