fix(shutdown): kill full child process trees and add synchronous exit handler to force-kill remaining child processes

This commit is contained in:
2026-03-03 22:34:01 +00:00
parent 2e5b26c9cf
commit 1986b3a421
3 changed files with 39 additions and 16 deletions

View File

@@ -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

View File

@@ -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.'
}

View File

@@ -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);
}
});