fix(processmanager): Improve process lifecycle handling and cleanup in daemon, monitors and wrappers

This commit is contained in:
2025-08-31 07:45:47 +00:00
parent 8f31672a67
commit 0a75c4cf76
6 changed files with 56 additions and 32 deletions

View File

@@ -23,6 +23,26 @@ export class ProcessWrapper extends EventEmitter {
private runId: string = '';
private stdoutRemainder: string = '';
private stderrRemainder: string = '';
// Helper: send a signal to the process and all its children (best-effort)
private async killProcessTree(signal: NodeJS.Signals): Promise<void> {
if (!this.process || !this.process.pid) return;
const rootPid = this.process.pid;
await new Promise<void>((resolve) => {
plugins.psTree(rootPid, (err: any, children: ReadonlyArray<{ PID: string }>) => {
const pids: number[] = [rootPid, ...children.map((c) => Number(c.PID)).filter((n) => Number.isFinite(n))];
for (const pid of pids) {
try {
// Always signal individual PIDs to avoid accidentally targeting unrelated groups
process.kill(pid, signal);
} catch {
// ignore ESRCH/EPERM
}
}
resolve();
});
});
}
constructor(options: IProcessWrapperOptions) {
super();
@@ -193,17 +213,13 @@ export class ProcessWrapper extends EventEmitter {
// First try SIGTERM for graceful shutdown
if (this.process.pid) {
try {
this.logger.debug(`Sending SIGTERM to process ${this.process.pid}`);
try {
// Try to signal the whole process group on POSIX to ensure children get the signal too
if (process.platform !== 'win32') {
process.kill(-Math.abs(this.process.pid), 'SIGTERM');
} else {
process.kill(this.process.pid, 'SIGTERM');
}
} catch {
// Fallback to direct process kill if group kill fails
process.kill(this.process.pid, 'SIGTERM');
this.logger.debug(`Sending SIGTERM to process tree rooted at ${this.process.pid}`);
await this.killProcessTree('SIGTERM');
// If the process already exited, return immediately
if (typeof this.process.exitCode === 'number') {
this.logger.debug('Process already exited, no need to wait');
return;
}
// Wait for exit or escalate
@@ -218,26 +234,15 @@ export class ProcessWrapper extends EventEmitter {
const onExit = () => cleanup();
this.process!.once('exit', onExit);
const killTimer = setTimeout(() => {
const killTimer = setTimeout(async () => {
if (!this.process || !this.process.pid) return cleanup();
this.logger.warn(
`Process ${this.process.pid} did not exit gracefully, force killing...`,
);
this.addSystemLog(
'Process did not exit gracefully, force killing...',
`Process ${this.process.pid} did not exit gracefully, force killing tree...`,
);
this.addSystemLog('Process did not exit gracefully, force killing...');
try {
if (process.platform !== 'win32') {
process.kill(-Math.abs(this.process.pid), 'SIGKILL');
} else {
process.kill(this.process.pid, 'SIGKILL');
}
} catch (error: any) {
this.logger.debug(
`Failed to send SIGKILL, process probably already exited: ${error?.message || String(error)}`,
);
}
await this.killProcessTree('SIGKILL');
} catch {}
// Give a short grace period after SIGKILL
setTimeout(() => cleanup(), 500);
}, 5000);