From 1986b3a421fe195ea48853a45de9ac17fd6ac4b5 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 3 Mar 2026 22:34:01 +0000 Subject: [PATCH] fix(shutdown): kill full child process trees and add synchronous exit handler to force-kill remaining child processes --- changelog.md | 8 +++++++ ts/00_commitinfo_data.ts | 2 +- ts/index.ts | 45 ++++++++++++++++++++++++++-------------- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/changelog.md b/changelog.md index 604b419..ffce761 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 8f26ac8..f277c0b 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -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.' } diff --git a/ts/index.ts b/ts/index.ts index 49a33d2..c6d0183 100644 --- a/ts/index.ts +++ b/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); } });