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:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user