UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

356 lines (355 loc) • 12.6 kB
import { opendirSync, 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; const UNIX_PROCESS_TREE_TIMEOUT_MS = 500; const MAX_UNIX_PROCESS_TREE_PIDS = 4096; const MAX_UNIX_PROCESS_TREE_DEPTH = 128; /** * 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 the process group when ownership is known, wait the * grace period, then SIGKILL. Linux attached children are enumerated and * signaled descendants-first because their process group belongs to the gateway. * * 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) { signalProcessTreeWindowsAndWait(pid, "SIGKILL"); return; } killProcessTreeWindows(pid, normalizeGraceMs(opts?.graceMs)); return; } const useGroupKill = opts?.detached === true || opts?.detached !== false && isProcessGroupLeader(pid); const attachedLinuxTree = opts?.detached === false && !useGroupKill && process.platform === "linux"; const processTree = attachedLinuxTree ? collectUnixProcessTree(pid) : void 0; if (attachedLinuxTree && !processTree) { signalProcessTreeUnix(pid, opts?.force === true ? "SIGKILL" : "SIGTERM", false); return; } let forceRequested = false; const force = () => { if (forceRequested) return; forceRequested = true; const liveProcessTree = processTree?.filter(verifiedProcessInstanceAlive) ?? []; if (!(useGroupKill ? isProcessAlive(-pid) : attachedLinuxTree ? liveProcessTree.length > 0 : isProcessAlive(pid))) return; signalProcessTreeUnix(pid, "SIGKILL", useGroupKill, processTree ? liveProcessTree : void 0); }; if (opts?.force === true) { if (processTree) force(); else signalProcessTreeUnix(pid, "SIGKILL", useGroupKill); return; } signalProcessTreeUnix(pid, "SIGTERM", useGroupKill, processTree); const graceMs = normalizeGraceMs(opts?.graceMs); const forceTimer = setTimeout(force, graceMs); if (!attachedLinuxTree) forceTimer.unref(); return { force: () => { clearTimeout(forceTimer); force(); } }; } 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; } const useGroupKill = opts?.detached === true || opts?.detached !== false && isProcessGroupLeader(pid); signalProcessTreeUnix(pid, signal, useGroupKill, opts?.detached === false && !useGroupKill && process.platform === "linux" ? collectUnixProcessTree(pid) : void 0); opts?.onComplete?.(); } /** Capture the group selected by signalProcessTree while its leader still exists. */ function readUnixProcessGroupMembers(pid) { if (process.platform === "win32" || !isProcessGroupLeader(pid)) return [pid]; const output = readPsOutput(["-axo", "pid=,pgid="]); const members = /* @__PURE__ */ new Set([pid]); for (const line of output?.split("\n") ?? []) { const [memberText, groupText] = line.trim().split(/\s+/); const member = parseProcessGroupId(memberText); if (member && parseProcessGroupId(groupText) === pid) members.add(member); } return [...members]; } /** 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) { signalUnixTarget(-pid, signal); return; } const members = readProcessSessionMembers(pid, darwinTty); if (!members) { signalUnixTarget(-pid, signal); 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); } catch (error) { if ((typeof error === "object" && error !== null && "code" in error ? error.code : void 0) !== "EPERM") return false; } if (pid < 0 || process.platform !== "linux") return true; try { const status = readFileSync(`/proc/${pid}/status`, "utf8"); return !(status.match(/^State:\s+(\S)/m)?.[1] === "Z" && /^Threads:[ \t]+1[ \t]*$/m.test(status)); } catch { return true; } } 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 readPsOutput(args) { try { const result = spawnSync("ps", args, { encoding: "utf8", timeout: 500 }); return result.error || result.status !== 0 ? void 0 : result.stdout; } catch { return; } } function readProcStatFields(pid) { try { const stat = readFileSync(`/proc/${pid}/stat`, "utf8"); const commEnd = stat.lastIndexOf(")"); if (commEnd < 0) return; return stat.slice(commEnd + 1).trim().split(/\s+/); } catch { return; } } function readDarwinPtyTty(sessionLeaderPid) { const tty = readPsOutput([ "-p", String(sessionLeaderPid), "-o", "tty=" ])?.trim(); return tty && tty !== "?" && tty !== "??" ? tty : void 0; } function readProcessSessionMembers(sessionId, darwinTty) { const expectedSession = darwinTty ?? String(sessionId); const output = readPsOutput(darwinTty ? [ "-t", darwinTty, "-o", "pid=,pgid=" ] : ["-axo", "pid=,pgid=,sid="]); if (output === void 0) return; const members = []; for (const line of output.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; } /** Fail closed to direct-PID signaling when group ownership cannot be proved. */ function isProcessGroupLeader(pid) { return ((process.platform === "linux" ? parseProcessGroupId(readProcStatFields(pid)?.[2]) : void 0) ?? parseProcessGroupId(readPsOutput([ "-p", String(pid), "-o", "pgid=" ]))) === pid; } function parsePositivePids(value) { if (typeof value !== "string") return []; return value.trim().split(/\s+/u).map((entry) => Number(entry)).filter((entry) => Number.isSafeInteger(entry) && entry > 0); } function readUnixProcessIdentity(pid, deadlineMs) { return readUnixProcessInstance(pid, deadlineMs)?.identity; } /** Capture identity and parent together so a recycled child PID cannot authorize a foreign tree. */ function readUnixProcessInstance(pid, deadlineMs) { if (deadlineMs !== void 0 && Date.now() >= deadlineMs) return; if (process.platform !== "linux") return; const fields = readProcStatFields(pid); const ppid = Number(fields?.[1]); const starttime = fields?.[19]; if (!starttime || !/^\d+$/u.test(starttime) || !Number.isSafeInteger(ppid)) return; return { identity: `${pid}:${starttime}`, ppid }; } /** Missing identities are never eligible for an attached-tree signal. */ function verifiedProcessInstanceAlive(entry) { if (!entry.identity || !isProcessAlive(entry.pid)) return false; return readUnixProcessIdentity(entry.pid) === entry.identity; } function* readUnixProcessChildren(pid, canReadTask) { let tasks; try { tasks = opendirSync(`/proc/${pid}/task`); while (canReadTask()) { const task = tasks.readSync(); if (!task) return; const tid = parseProcessGroupId(task.name); if (!tid) continue; try { yield* parsePositivePids(readFileSync(`/proc/${pid}/task/${tid}/children`, "utf8")); } catch {} } } catch {} finally { try { tasks?.closeSync(); } catch {} } } /** Capture verified descendants before their parents; an unverified subtree is omitted. */ function collectUnixProcessTree(rootPid) { if (process.platform !== "linux") return; const descendants = []; const seen = /* @__PURE__ */ new Set([rootPid]); let taskReads = 0; const deadline = Date.now() + UNIX_PROCESS_TREE_TIMEOUT_MS; const rootIdentity = readUnixProcessIdentity(rootPid, deadline); if (!rootIdentity || Date.now() >= deadline) return; const withinBounds = (depth) => Date.now() < deadline && depth <= MAX_UNIX_PROCESS_TREE_DEPTH; const hasCapacity = () => seen.size < MAX_UNIX_PROCESS_TREE_PIDS; const visit = (parentPid, parentIdentity, depth) => { if (!withinBounds(depth) || !hasCapacity()) return; if (readUnixProcessIdentity(parentPid, deadline) !== parentIdentity) return; const children = readUnixProcessChildren(parentPid, () => withinBounds(depth) && hasCapacity() && taskReads++ < MAX_UNIX_PROCESS_TREE_PIDS && readUnixProcessIdentity(parentPid, deadline) === parentIdentity); for (const childPid of children) { if (!withinBounds(depth + 1) || !hasCapacity()) return; if (seen.has(childPid) || childPid === process.pid) continue; seen.add(childPid); const instance = readUnixProcessInstance(childPid, deadline); if (!instance || instance.ppid !== parentPid || !withinBounds(depth + 1)) continue; visit(childPid, instance.identity, depth + 1); descendants.push({ pid: childPid, identity: instance.identity }); } }; visit(rootPid, rootIdentity, 0); return [...descendants, { pid: rootPid, identity: rootIdentity }]; } function signalProcessTreeUnix(pid, signal, useGroupKill, processTree) { if (useGroupKill) { signalUnixTarget(-pid, signal); return; } const targets = processTree ?? [{ pid }]; for (const entry of targets) { if (processTree && !verifiedProcessInstanceAlive(entry)) continue; signalUnixTarget(entry.pid, signal); } } 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; signalProcessTreeWindowsAndWait(pid, "SIGKILL"); }; signalProcessTreeWindowsAndWait(pid, "SIGTERM", (code) => { if (code !== null && code !== 0) forceKill(); }); graceTimer = setTimeout(forceKill, graceMs); graceTimer.unref(); } function signalProcessTreeWindowsAndWait(pid, signal, onExit) { return runTaskkill(signal === "SIGKILL" ? [ "/F", "/T", "/PID", String(pid) ] : [ "/T", "/PID", String(pid) ], onExit); } //#endregion export { signalPtySessionTree as i, readUnixProcessGroupMembers as n, signalProcessTree as r, killProcessTree as t };