UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

282 lines (281 loc) • 8.75 kB
import { spawn, spawnSync } from "node:child_process"; import { inheritedHostEnv } from "../../core/secret-env.js"; import { existsSync } from "node:fs"; import { join } from "node:path"; const DEFAULT_GRACE_MS = 3000; const MAX_GRACE_MS = 60_000; import { shutdownDebug } from "../../core/shutdown-debug.js"; export { shutdownDebug }; export function killProcessTree(pid, opts) { if (!Number.isFinite(pid) || pid <= 0) { return; } shutdownDebug("killProcessTree", { pid, force: opts?.force === true, detached: opts?.detached !== false, graceMs: opts?.graceMs }); if (process.platform === "win32") { if (opts?.force === true) { signalProcessTreeWindows(pid, "SIGKILL"); return; } const graceMs = normalizeGraceMs(opts?.graceMs); killProcessTreeWindows(pid, graceMs, undefined); return; } const useGroupKill = opts?.detached !== false; if (opts?.force === true) { signalProcessTreeUnix(pid, "SIGKILL", useGroupKill); return; } const graceMs = normalizeGraceMs(opts?.graceMs); const termDescendants = signalProcessTreeUnix(pid, "SIGTERM", useGroupKill); setTimeout(() => { const rediscovered = enumerateDescendantsSync(pid); const verifiedCarriedForward = new Map(); for (const [p, originalEtime] of termDescendants) { const currentEtime = rediscovered.etimeOf.get(p); if (currentEtime !== undefined && verifyStillSameProcess(originalEtime, currentEtime)) { verifiedCarriedForward.set(p, currentEtime); } } const knownDescendants = new Map([...verifiedCarriedForward, ...rediscovered.descendants]); killEach(knownDescendants.keys(), "SIGKILL"); if (escalationCancelled(opts?.stillRunning)) { if (useGroupKill && isProcessAlive(-pid)) { try { process.kill(-pid, "SIGKILL"); } catch { } } return; } const stillAlive = useGroupKill ? isProcessAlive(-pid) || isProcessAlive(pid) : isProcessAlive(pid); if (!stillAlive) { return; } if (useGroupKill) { try { process.kill(-pid, "SIGKILL"); return; } catch { } } try { process.kill(pid, "SIGKILL"); } catch { } }, graceMs).unref(); } function escalationCancelled(stillRunning) { if (stillRunning === undefined) return false; try { return !stillRunning(); } catch { return false; } } export function signalProcessTree(pid, signal, opts) { if (!Number.isFinite(pid) || pid <= 0) { return; } if (process.platform === "win32") { signalProcessTreeWindows(pid, signal); return; } signalProcessTreeUnix(pid, signal, opts?.detached !== false); } 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 signalProcessTreeUnix(pid, signal, useGroupKill) { const descendants = enumerateDescendantsSync(pid).descendants; if (useGroupKill) { try { process.kill(-pid, signal); } catch { try { process.kill(pid, signal); } catch { } } killEach(descendants.keys(), signal); return descendants; } try { process.kill(pid, signal); } catch { } killEach(descendants.keys(), signal); return descendants; } function killEach(pids, signal) { for (const pid of pids) { try { process.kill(pid, signal); } catch { } } } const PS_ENUMERATION_TIMEOUT_MS = 500; const EMPTY_SNAPSHOT = { descendants: new Map(), etimeOf: new Map() }; function enumerateDescendantsSync(rootPid) { if (process.platform === "win32") return EMPTY_SNAPSHOT; let out; try { const result = spawnSync("ps", ["-A", "-o", "pid=", "-o", "ppid=", "-o", "etime="], { env: inheritedHostEnv("scrub"), timeout: PS_ENUMERATION_TIMEOUT_MS, encoding: "utf8", windowsHide: true, }); if (result.error || typeof result.stdout !== "string") return EMPTY_SNAPSHOT; out = result.stdout; } catch { return EMPTY_SNAPSHOT; } const childrenOf = new Map(); const etimeOf = new Map(); for (const line of out.split("\n")) { const m = /^\s*(\d+)\s+(\d+)\s+(\S+)\s*$/.exec(line); if (!m) continue; const p = Number(m[1]); const pp = Number(m[2]); etimeOf.set(p, m[3]); const list = childrenOf.get(pp); if (list) list.push(p); else childrenOf.set(pp, [p]); } const seen = new Set(); const queue = [rootPid]; while (queue.length > 0) { const cur = queue.shift(); for (const kid of childrenOf.get(cur) ?? []) { if (kid <= 1 || kid === rootPid || seen.has(kid)) continue; seen.add(kid); queue.push(kid); } } const descendants = new Map(); for (const p of seen) { const et = etimeOf.get(p); if (et !== undefined) descendants.set(p, et); } return { descendants, etimeOf }; } function verifyStillSameProcess(originalEtime, currentEtime) { const orig = parseEtimeSeconds(originalEtime); const cur = parseEtimeSeconds(currentEtime); if (orig === undefined || cur === undefined) return false; return cur >= orig; } function parseEtimeSeconds(etime) { const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(etime.trim()); if (!m) return undefined; const days = m[1] !== undefined ? Number(m[1]) : 0; const hours = m[2] !== undefined ? Number(m[2]) : 0; const minutes = Number(m[3]); const seconds = Number(m[4]); return days * 86400 + hours * 3600 + minutes * 60 + seconds; } function runTaskkill(args) { try { const child = spawn("taskkill", args, { env: inheritedHostEnv("scrub"), stdio: "ignore", detached: true, windowsHide: true, }); child.on("error", () => { }); } catch { } } function killProcessTreeWindows(pid, graceMs, stillRunning) { signalProcessTreeWindows(pid, "SIGTERM"); setTimeout(() => { if (escalationCancelled(stillRunning)) { return; } if (!isProcessAlive(pid)) { return; } signalProcessTreeWindows(pid, "SIGKILL"); }, graceMs).unref(); } function signalProcessTreeWindows(pid, signal) { if (signal === "SIGKILL") { const bash = gitBashPath(); if (bash) { const fired = tryMsysGroupKill(bash, pid); if (fired) { setTimeout(() => runTaskkill(["/F", "/T", "/PID", String(pid)]), 1200).unref(); return; } } runTaskkill(["/F", "/T", "/PID", String(pid)]); return; } runTaskkill(["/T", "/PID", String(pid)]); } function gitBashPath() { const candidates = [ process.env.SEMA_GIT_BASH, join(process.env["ProgramFiles"] ?? "C:\\Program Files", "Git", "bin", "bash.exe"), join(process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)", "Git", "bin", "bash.exe"), ]; for (const c of candidates) { try { if (c && existsSync(c)) return c; } catch { } } return undefined; } function tryMsysGroupKill(bashPath, pid) { const script = `kids=$(powershell.exe -NoProfile -Command "Get-CimInstance Win32_Process -Filter 'ParentProcessId=${pid}' | Select-Object -ExpandProperty ProcessId" 2>/dev/null | tr -d "\\r"); ` + `for w in ${pid} $kids; do ` + `g=$(ps -W | awk -v w="$w" '$4==w {print $3; exit}'); ` + `[ -n "$g" ] && [ "$g" != "0" ] && kill -9 -"$g" 2>/dev/null; ` + `done; exit 0`; try { const child = spawn(bashPath, ["-c", script], { env: inheritedHostEnv("scrub"), stdio: "ignore", detached: true, windowsHide: true }); child.on("error", () => { }); return true; } catch { return false; } }