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 # 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) ## 2025-12-15 - 1.1.0 - feat(smartexit)
Add silent logging option, structured shutdown logs, and killAll return stats Add silent logging option, structured shutdown logs, and killAll return stats

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartexit', 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.' 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 processesKilled = 0;
let cleanupFunctionsRan = 0; let cleanupFunctionsRan = 0;
// Kill child processes // Kill child process trees
if (processes.length > 0) { if (processes.length > 0) {
for (const childProcessArg of processes) { for (const childProcessArg of processes) {
const pid = childProcessArg.pid; const pid = childProcessArg.pid;
if (pid) { if (pid) {
plugins.smartdelay.delayFor(10000).then(() => { try {
if (childProcessArg.killed) { // Use tree-kill to kill the entire process tree, not just the direct child
return; 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++; processesKilled++;
} }
} }
@@ -130,14 +134,25 @@ export class SmartExit {
constructor(optionsArg: ISmartExitOptions = {}) { constructor(optionsArg: ISmartExitOptions = {}) {
this.options = optionsArg; this.options = optionsArg;
// do app specific cleaning before exiting // Last-resort synchronous cleanup on exit — 'exit' event cannot await async work.
process.on('exit', async (code) => { // By this point, SIGINT handler should have already called killAll().
if (code === 0) { // This is a safety net for any processes still alive.
const { processesKilled, cleanupFunctionsRan } = await this.killAll(); process.on('exit', (code) => {
this.log(`Shutdown complete: killed ${processesKilled} child processes, ran ${cleanupFunctionsRan} cleanup functions`); const processes = this.processesToEnd.getArray();
} else { let killed = 0;
const { processesKilled, cleanupFunctionsRan } = await this.killAll(); for (const childProcessArg of processes) {
this.log(`Shutdown complete: killed ${processesKilled} child processes, ran ${cleanupFunctionsRan} cleanup functions (exit code: ${code})`, true); 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);
} }
}); });