UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

255 lines (254 loc) 7.55 kB
import { readFileSync } from "node:fs"; import { spawn, spawnSync } from "node:child_process"; //#region packages/agent-core/src/harness/env/kill-tree.ts const DEFAULT_GRACE_MS = 3e3; const MAX_GRACE_MS = 6e4; const TASKKILL_COMPLETION_TIMEOUT_MS = 3e3; /** * Best-effort process-tree termination with graceful shutdown. * - Windows: use taskkill /T to include descendants. Sends SIGTERM-equivalent * first (without /F), then force-kills if taskkill refuses or the process * survives the grace period. * - Unix: send SIGTERM to process group first, wait grace period, then SIGKILL. * * Group kill (`process.kill(-pid, ...)`) is only used when the PID is verified * as its own process group leader, unless `detached: true` is explicitly passed. * This prevents accidentally signaling the gateway's process group when the * child shares its parent's group. * * - `detached: false`: skip group kill unconditionally. * - `detached: true`: use group kill unconditionally (trust caller). * - `detached` omitted: use group kill only when PID is the group leader. */ function killProcessTree(pid, opts) { if (!Number.isFinite(pid) || pid <= 0) return; if (process.platform === "win32") { if (opts?.force === true) { signalProcessTreeWindows(pid, "SIGKILL"); return; } killProcessTreeWindows(pid, normalizeGraceMs(opts?.graceMs)); return; } const useGroupKill = opts?.detached === true || opts?.detached !== false && isProcessGroupLeader(pid); if (opts?.force === true) { signalProcessTreeUnix(pid, "SIGKILL", useGroupKill); return; } const graceMs = normalizeGraceMs(opts?.graceMs); signalProcessTreeUnix(pid, "SIGTERM", useGroupKill); setTimeout(() => { if (!(useGroupKill ? isProcessAlive(-pid) || isProcessAlive(pid) : isProcessAlive(pid))) return; signalProcessTreeUnix(pid, "SIGKILL", useGroupKill); }, graceMs).unref(); } function signalProcessTree(pid, signal, opts) { if (!Number.isFinite(pid) || pid <= 0) { opts?.onComplete?.(); return; } if (process.platform === "win32") { signalProcessTreeWindowsAndWait(pid, signal).then(opts?.onComplete); return; } signalProcessTreeUnix(pid, signal, opts?.detached === true || opts?.detached !== false && isProcessGroupLeader(pid)); opts?.onComplete?.(); } /** Signals every process group and process still owned by one forkpty session. */ function signalPtySessionTree(pid, signal) { if (!Number.isFinite(pid) || pid <= 0) return; if (process.platform === "win32") { signalProcessTreeWindowsAndWait(pid, signal); return; } const darwinTty = process.platform === "darwin" ? readDarwinPtyTty(pid) : void 0; if (process.platform === "darwin" && !darwinTty) { signalProcessTreeUnix(pid, signal, true); return; } const members = readProcessSessionMembers(pid, darwinTty); if (!members) { signalProcessTreeUnix(pid, signal, true); return; } const signalMembers = (snapshot) => { const groups = new Set(snapshot.map((member) => member.pgid)); groups.delete(pid); for (const pgid of groups) signalUnixTarget(-pgid, signal); for (const member of snapshot) if (member.pid !== pid) signalUnixTarget(member.pid, signal); }; signalMembers(members); const remaining = readProcessSessionMembers(pid, darwinTty); if (remaining) signalMembers(remaining); signalUnixTarget(-pid, signal); signalUnixTarget(pid, signal); } function normalizeGraceMs(value) { if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_GRACE_MS; return Math.max(0, Math.min(MAX_GRACE_MS, Math.floor(value))); } function isProcessAlive(pid) { try { process.kill(pid, 0); return true; } catch { return false; } } function parseProcessGroupId(value) { if (typeof value !== "string" || !/^\d+$/.test(value.trim())) return; const pgid = Number(value.trim()); return Number.isSafeInteger(pgid) && pgid > 0 ? pgid : void 0; } function readProcessGroupIdFromPs(pid) { try { const res = spawnSync("ps", [ "-p", String(pid), "-o", "pgid=" ], { encoding: "utf8", timeout: 500 }); if (res.error || res.status !== 0) return; return parseProcessGroupId(res.stdout); } catch { return; } } function readProcessGroupIdFromProc(pid) { try { const stat = readFileSync(`/proc/${pid}/stat`, "utf8"); const commEnd = stat.lastIndexOf(")"); if (commEnd < 0) return; return parseProcessGroupId(stat.slice(commEnd + 1).trim().split(/\s+/)[2]); } catch { return; } } function readDarwinPtyTty(sessionLeaderPid) { try { const leader = spawnSync("ps", [ "-p", String(sessionLeaderPid), "-o", "tty=" ], { encoding: "utf8", timeout: 500 }); const tty = leader.stdout.trim(); if (leader.error || leader.status !== 0 || !tty || tty === "?" || tty === "??") return; return tty; } catch { return; } } function readProcessSessionMembers(sessionId, darwinTty) { try { const expectedSession = darwinTty ?? String(sessionId); const result = spawnSync("ps", darwinTty ? [ "-t", darwinTty, "-o", "pid=,pgid=" ] : ["-axo", "pid=,pgid=,sid="], { encoding: "utf8", timeout: 500 }); if (result.error || result.status !== 0) return; const members = []; for (const line of result.stdout.split("\n")) { const [pidText, pgidText, session] = line.trim().split(/\s+/); const pid = parseProcessGroupId(pidText); const pgid = parseProcessGroupId(pgidText); if (pid && pgid && (darwinTty || session === expectedSession)) members.push({ pid, pgid }); } return members; } catch { return; } } /** Fail closed to direct-PID signaling when group ownership cannot be proved. */ function isProcessGroupLeader(pid) { return ((process.platform === "linux" ? readProcessGroupIdFromProc(pid) : void 0) ?? readProcessGroupIdFromPs(pid)) === pid; } function signalProcessTreeUnix(pid, signal, useGroupKill) { if (useGroupKill) try { process.kill(-pid, signal); return; } catch {} try { process.kill(pid, signal); } catch {} } function signalUnixTarget(pid, signal) { try { process.kill(pid, signal); } catch {} } function runTaskkill(args, onExit) { return new Promise((resolve) => { let settled = false; const finish = (code) => { if (settled) return; settled = true; clearTimeout(completionTimer); onExit?.(code); resolve(); }; const completionTimer = setTimeout(() => finish(null), TASKKILL_COMPLETION_TIMEOUT_MS); completionTimer.unref?.(); try { const child = spawn("taskkill", args, { stdio: "ignore", detached: true, windowsHide: true }); child.once("error", () => finish(null)); child.once("close", (code) => finish(code)); } catch { finish(null); } }); } function killProcessTreeWindows(pid, graceMs) { let forced = false; let graceTimer; const forceKill = () => { if (forced) return; forced = true; if (graceTimer !== void 0) { clearTimeout(graceTimer); graceTimer = void 0; } if (!isProcessAlive(pid)) return; signalProcessTreeWindows(pid, "SIGKILL"); }; signalProcessTreeWindows(pid, "SIGTERM", (code) => { if (code !== null && code !== 0) forceKill(); }); graceTimer = setTimeout(forceKill, graceMs); graceTimer.unref(); } function signalProcessTreeWindows(pid, signal, onExit) { signalProcessTreeWindowsAndWait(pid, signal, onExit); } function signalProcessTreeWindowsAndWait(pid, signal, onExit) { return runTaskkill(signal === "SIGKILL" ? [ "/F", "/T", "/PID", String(pid) ] : [ "/T", "/PID", String(pid) ], onExit); } //#endregion export { signalProcessTree as n, signalPtySessionTree as r, killProcessTree as t };