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.
This commit is contained in:
2026-03-03 23:56:52 +00:00
parent da24218bef
commit 34b9aa4463
2 changed files with 28 additions and 25 deletions

View File

@@ -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) {
for (const pid of instance.trackedPids) {
try {
process.kill(pid, 'SIGKILL');
killed++;
} catch {
// Process may already be dead
}
// Process already dead
}
}
}

View File

@@ -70,6 +70,8 @@ export class SmartExit {
// Instance state
public processesToEnd = new plugins.lik.ObjectMap<plugins.childProcess.ChildProcess>();
public cleanupFunctions = new plugins.lik.ObjectMap<() => Promise<any>>();
/** PIDs tracked independently — survives removeProcess() so shutdown can still kill the tree. */
public trackedPids = new Set<number>();
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) {
// 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');
} catch {
// Process may already be dead
}
}
processesKilled++;
} catch {
// Process tree already dead — fine
}
}
}
this.trackedPids.clear();
return { processesKilled, cleanupFunctionsRan };
}