From 34b9aa446393d68366d9381dc62ee041dadc606f Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 3 Mar 2026 23:56:52 +0000 Subject: [PATCH] fix(core): track PIDs independently to survive removeProcess() race during shutdown The direct child process may die from terminal SIGINT before ProcessLifecycle runs shutdown, causing removeProcess() to clear it. Now killAll() uses a persistent trackedPids Set that is never cleared by removeProcess(), ensuring grandchild process trees are always killed. --- ts/smartexit.classes.lifecycle.ts | 18 +++++++--------- ts/smartexit.classes.smartexit.ts | 35 ++++++++++++++++++------------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/ts/smartexit.classes.lifecycle.ts b/ts/smartexit.classes.lifecycle.ts index eb38521..d7c0eb9 100644 --- a/ts/smartexit.classes.lifecycle.ts +++ b/ts/smartexit.classes.lifecycle.ts @@ -187,22 +187,18 @@ export class ProcessLifecycle { }); } - /** Synchronous last-resort: SIGKILL any remaining child processes. */ + /** Synchronous last-resort: SIGKILL any remaining tracked PIDs. */ private handleExit(): void { const instances = ProcessLifecycle.getInstances(); let killed = 0; for (const instance of instances) { - const processes = instance.processesToEnd.getArray(); - for (const child of processes) { - const pid = child.pid; - if (pid && !child.killed) { - try { - process.kill(pid, 'SIGKILL'); - killed++; - } catch { - // Process may already be dead - } + for (const pid of instance.trackedPids) { + try { + process.kill(pid, 'SIGKILL'); + killed++; + } catch { + // Process already dead } } } diff --git a/ts/smartexit.classes.smartexit.ts b/ts/smartexit.classes.smartexit.ts index 31f705b..dc37e28 100644 --- a/ts/smartexit.classes.smartexit.ts +++ b/ts/smartexit.classes.smartexit.ts @@ -70,6 +70,8 @@ export class SmartExit { // Instance state public processesToEnd = new plugins.lik.ObjectMap(); public cleanupFunctions = new plugins.lik.ObjectMap<() => Promise>(); + /** PIDs tracked independently — survives removeProcess() so shutdown can still kill the tree. */ + public trackedPids = new Set(); private options: ISmartExitOptions; private log(message: string, isError = false): void { @@ -93,6 +95,9 @@ export class SmartExit { /** Register a child process for cleanup on shutdown. */ public addProcess(childProcessArg: plugins.childProcess.ChildProcess) { this.processesToEnd.add(childProcessArg); + if (childProcessArg.pid) { + this.trackedPids.add(childProcessArg.pid); + } } /** Register an async cleanup function to run on shutdown. */ @@ -126,23 +131,25 @@ export class SmartExit { } } - // Phase 2: Kill child process trees - for (const childProcessArg of processes) { - const pid = childProcessArg.pid; - if (pid && !childProcessArg.killed) { - try { - await SmartExit.killTreeByPid(pid, 'SIGTERM'); - } catch { - // SIGTERM failed — force kill - try { - await SmartExit.killTreeByPid(pid, 'SIGKILL'); - } catch { - // Process may already be dead - } - } + // Phase 2: Kill ALL tracked process trees by PID. + // We use trackedPids (not processesToEnd) because the process object may have + // been removed by smartshell's exit handler before shutdown runs. + // We do NOT check .killed — the direct child may be dead but grandchildren alive. + for (const pid of this.trackedPids) { + try { + await SmartExit.killTreeByPid(pid, 'SIGTERM'); processesKilled++; + } catch { + // SIGTERM failed — force kill + try { + await SmartExit.killTreeByPid(pid, 'SIGKILL'); + processesKilled++; + } catch { + // Process tree already dead — fine + } } } + this.trackedPids.clear(); return { processesKilled, cleanupFunctionsRan }; }