Skip to content

Commit 422db6f

Browse files
committed
feat: handle onDeath
1 parent f4ddf12 commit 422db6f

File tree

4 files changed

+120
-1
lines changed

4 files changed

+120
-1
lines changed

packages/death/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ onDeath(() => {
1818
})
1919
```
2020

21+
## Note
22+
23+
In [Signal events](https://nodejs.org/dist/latest-v20.x/docs/api/process.html#signal-events), it says that `SIGTERM` and `SIGINT` have default handlers on non-Windows platforms that reset the terminal mode before exiting with code 128 + signal number. **If one of these signals has a listener installed, its default behavior will be removed (Node.js will no longer exit).**
24+
2125
## License
2226

2327
MIT License © 2023 [XLor](https://github.com/yjl9903)

packages/death/scripts/play.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { onDeath } from '../src';
2+
3+
function sleep(timeout: number): Promise<void> {
4+
return new Promise((res) => {
5+
setTimeout(() => res(), timeout);
6+
});
7+
}
8+
9+
onDeath((sig) => {
10+
console.log(`Receive: ${sig}`);
11+
console.log('Process is being killed');
12+
});
13+
14+
console.log('Start sleep');
15+
16+
await sleep(1000 * 1000);

packages/death/src/death.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { EventEmitter } from 'node:events';
2+
3+
export type DeathSignals = 'SIGINT' | 'SIGTERM' | 'SIGQUIT';
4+
5+
export interface OnDeathContext {
6+
// Terminate process by which method or does nothing when callbacks are done
7+
terminate: 'exit' | 'kill' | false;
8+
9+
// Call process.exit when callbacks are done
10+
exit: number | undefined;
11+
12+
// Call process.kill when callbacks are done
13+
kill: NodeJS.Signals | undefined;
14+
}
15+
16+
export type OnDeathCallback = (
17+
signal: DeathSignals,
18+
context: OnDeathContext
19+
) => unknown | Promise<unknown>;
20+
21+
export interface OnDeathOptions {
22+
SIGINT?: boolean;
23+
SIGTERM?: boolean;
24+
SIGQUIT?: boolean;
25+
}
26+
27+
const emitter = new EventEmitter();
28+
29+
const handlers: Record<DeathSignals, NodeJS.SignalsListener> = {
30+
SIGINT: makeHandler('SIGINT'),
31+
SIGTERM: makeHandler('SIGTERM'),
32+
SIGQUIT: makeHandler('SIGQUIT')
33+
};
34+
35+
export function onDeath(
36+
callback: OnDeathCallback,
37+
{ SIGINT = true, SIGTERM = true, SIGQUIT = true }: OnDeathOptions = {}
38+
): () => void {
39+
const cleanUps: Array<() => void> = [];
40+
41+
if (SIGINT) {
42+
registerCallback('SIGINT', handlers.SIGINT);
43+
emitter.addListener('SIGINT', callback);
44+
cleanUps.push(() => emitter.removeListener('SIGINT', callback));
45+
}
46+
if (SIGTERM) {
47+
registerCallback('SIGTERM', handlers.SIGTERM);
48+
emitter.addListener('SIGTERM', callback);
49+
cleanUps.push(() => emitter.removeListener('SIGTERM', callback));
50+
}
51+
if (SIGQUIT) {
52+
registerCallback('SIGQUIT', handlers.SIGQUIT);
53+
emitter.addListener('SIGQUIT', callback);
54+
cleanUps.push(() => emitter.removeListener('SIGQUIT', callback));
55+
}
56+
57+
return () => {
58+
for (const cleanUp of cleanUps) {
59+
cleanUp();
60+
}
61+
};
62+
}
63+
64+
function registerCallback(
65+
signal: DeathSignals,
66+
callback: NodeJS.SignalsListener
67+
) {
68+
process.on(signal, callback);
69+
return () => {
70+
process.off(signal, callback);
71+
};
72+
}
73+
74+
function makeHandler(signal: DeathSignals) {
75+
return async (signal: NodeJS.Signals) => {
76+
const listeners = emitter.listeners(signal);
77+
const context: OnDeathContext = {
78+
terminate: 'kill',
79+
exit: undefined,
80+
kill: signal
81+
};
82+
83+
// Iterate all the listener by reverse
84+
for (const listener of listeners.reverse()) {
85+
await listener(signal, context);
86+
}
87+
88+
if (context.terminate === 'kill' || context.terminate === 'exit') {
89+
process.removeListener('SIGINT', handlers.SIGINT);
90+
process.removeListener('SIGTERM', handlers.SIGTERM);
91+
process.removeListener('SIGQUIT', handlers.SIGQUIT);
92+
if (context.terminate === 'kill') {
93+
process.kill(process.pid, context.kill);
94+
} else {
95+
process.exit(context.exit);
96+
}
97+
}
98+
};
99+
}

packages/death/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export function onDeath() {}
1+
export * from './death';

0 commit comments

Comments
 (0)