fix(shutdown): kill full child process trees and add synchronous exit handler to force-kill remaining child processes
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-03 - 1.1.1 - fix(shutdown)
|
||||
kill full child process trees and add synchronous exit handler to force-kill remaining child processes
|
||||
|
||||
- Use tree-kill via SmartExit.killTreeByPid to terminate entire process trees (attempt SIGTERM, fallback to SIGKILL).
|
||||
- Replace previous delayed/process.kill calls with awaitable tree-kill for more reliable termination.
|
||||
- Add a synchronous process.on('exit') handler that force-kills any remaining child processes as a last-resort safety net.
|
||||
- No public API changes; internal robustness/bugfix to shutdown behavior.
|
||||
|
||||
## 2025-12-15 - 1.1.0 - feat(smartexit)
|
||||
Add silent logging option, structured shutdown logs, and killAll return stats
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartexit',
|
||||
version: '1.1.0',
|
||||
version: '1.1.1',
|
||||
description: 'A library for managing graceful shutdowns of Node.js processes by handling cleanup operations, including terminating child processes.'
|
||||
}
|
||||
|
||||
45
ts/index.ts
45
ts/index.ts
@@ -99,18 +99,22 @@ export class SmartExit {
|
||||
let processesKilled = 0;
|
||||
let cleanupFunctionsRan = 0;
|
||||
|
||||
// Kill child processes
|
||||
// Kill child process trees
|
||||
if (processes.length > 0) {
|
||||
for (const childProcessArg of processes) {
|
||||
const pid = childProcessArg.pid;
|
||||
if (pid) {
|
||||
plugins.smartdelay.delayFor(10000).then(() => {
|
||||
if (childProcessArg.killed) {
|
||||
return;
|
||||
try {
|
||||
// Use tree-kill to kill the entire process tree, not just the direct child
|
||||
await SmartExit.killTreeByPid(pid, 'SIGTERM');
|
||||
} catch (err) {
|
||||
// SIGTERM failed — force kill the tree
|
||||
try {
|
||||
await SmartExit.killTreeByPid(pid, 'SIGKILL');
|
||||
} catch {
|
||||
// Process may already be dead, ignore
|
||||
}
|
||||
}
|
||||
process.kill(pid, 'SIGKILL');
|
||||
});
|
||||
process.kill(pid, 'SIGINT');
|
||||
processesKilled++;
|
||||
}
|
||||
}
|
||||
@@ -130,14 +134,25 @@ export class SmartExit {
|
||||
constructor(optionsArg: ISmartExitOptions = {}) {
|
||||
this.options = optionsArg;
|
||||
|
||||
// do app specific cleaning before exiting
|
||||
process.on('exit', async (code) => {
|
||||
if (code === 0) {
|
||||
const { processesKilled, cleanupFunctionsRan } = await this.killAll();
|
||||
this.log(`Shutdown complete: killed ${processesKilled} child processes, ran ${cleanupFunctionsRan} cleanup functions`);
|
||||
} else {
|
||||
const { processesKilled, cleanupFunctionsRan } = await this.killAll();
|
||||
this.log(`Shutdown complete: killed ${processesKilled} child processes, ran ${cleanupFunctionsRan} cleanup functions (exit code: ${code})`, true);
|
||||
// Last-resort synchronous cleanup on exit — 'exit' event cannot await async work.
|
||||
// By this point, SIGINT handler should have already called killAll().
|
||||
// This is a safety net for any processes still alive.
|
||||
process.on('exit', (code) => {
|
||||
const processes = this.processesToEnd.getArray();
|
||||
let killed = 0;
|
||||
for (const childProcessArg of processes) {
|
||||
const pid = childProcessArg.pid;
|
||||
if (pid && !childProcessArg.killed) {
|
||||
try {
|
||||
process.kill(pid, 'SIGKILL');
|
||||
killed++;
|
||||
} catch {
|
||||
// Process may already be dead, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
if (killed > 0) {
|
||||
this.log(`Exit handler: force-killed ${killed} remaining child processes (exit code: ${code})`, code !== 0);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user