mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 23:58:25 +00:00
fix(process): graceful process tree termination with SIGTERM before SIGKILL
Process trees (pty sessions, tool exec) were being SIGKILL'd immediately without any grace period for cleanup. This prevented child processes from: - Flushing buffers and closing files cleanly - Closing network connections - Terminating their own child processes - Removing temporary files Changes: - Send SIGTERM to process group first (Unix) - Wait configurable grace period (default 3s) - Then SIGKILL if process still alive - Windows: taskkill without /F first, then with /F after grace period - Use unref() on timeout to not block event loop exit Fixes #18619 Co-authored-by: James <james@openclaw.ai>
This commit is contained in:
committed by
Peter Steinberger
parent
19ae7a4e17
commit
20957efa46
@@ -1,34 +1,94 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Best-effort process-tree termination.
|
* Best-effort process-tree termination with graceful shutdown.
|
||||||
* - Windows: use taskkill /T to include descendants.
|
* - Windows: use taskkill /T to include descendants. Sends SIGTERM-equivalent
|
||||||
* - Unix: try process-group kill first, then direct pid kill.
|
* first (without /F), then force-kills if process survives.
|
||||||
|
* - Unix: send SIGTERM to process group first, wait grace period, then SIGKILL.
|
||||||
|
*
|
||||||
|
* This gives child processes a chance to clean up (close connections, remove
|
||||||
|
* temp files, terminate their own children) before being hard-killed.
|
||||||
*/
|
*/
|
||||||
export function killProcessTree(pid: number): void {
|
export function killProcessTree(pid: number, opts?: { graceMs?: number }): void {
|
||||||
if (!Number.isFinite(pid) || pid <= 0) {
|
if (!Number.isFinite(pid) || pid <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const graceMs = opts?.graceMs ?? 3000;
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
|
killProcessTreeWindows(pid, graceMs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
killProcessTreeUnix(pid, graceMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function killProcessTreeUnix(pid: number, graceMs: number): void {
|
||||||
|
// Step 1: Try graceful SIGTERM to process group
|
||||||
|
try {
|
||||||
|
process.kill(-pid, "SIGTERM");
|
||||||
|
} catch {
|
||||||
|
// Process group doesn't exist or we lack permission - try direct
|
||||||
|
try {
|
||||||
|
process.kill(pid, "SIGTERM");
|
||||||
|
} catch {
|
||||||
|
// Already gone
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Wait grace period, then SIGKILL if still alive
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
// Check if still alive by sending signal 0
|
||||||
|
process.kill(-pid, 0);
|
||||||
|
// Still alive - hard kill
|
||||||
|
try {
|
||||||
|
process.kill(-pid, "SIGKILL");
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
process.kill(pid, "SIGKILL");
|
||||||
|
} catch {
|
||||||
|
// Gone now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Process group gone - check direct
|
||||||
|
try {
|
||||||
|
process.kill(pid, 0);
|
||||||
|
try {
|
||||||
|
process.kill(pid, "SIGKILL");
|
||||||
|
} catch {
|
||||||
|
// Gone
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Already terminated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, graceMs).unref(); // Don't block event loop exit
|
||||||
|
}
|
||||||
|
|
||||||
|
function killProcessTreeWindows(pid: number, graceMs: number): void {
|
||||||
|
// Step 1: Try graceful termination (taskkill without /F)
|
||||||
|
try {
|
||||||
|
spawn("taskkill", ["/T", "/PID", String(pid)], {
|
||||||
|
stdio: "ignore",
|
||||||
|
detached: true,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Ignore spawn failures
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Wait grace period, then force kill if still alive
|
||||||
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
spawn("taskkill", ["/F", "/T", "/PID", String(pid)], {
|
spawn("taskkill", ["/F", "/T", "/PID", String(pid)], {
|
||||||
stdio: "ignore",
|
stdio: "ignore",
|
||||||
detached: true,
|
detached: true,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// ignore taskkill failures
|
// Ignore taskkill failures
|
||||||
}
|
}
|
||||||
return;
|
}, graceMs).unref(); // Don't block event loop exit
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
process.kill(-pid, "SIGKILL");
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
process.kill(pid, "SIGKILL");
|
|
||||||
} catch {
|
|
||||||
// process already gone or inaccessible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user