@sema-agent/core
Version:
Stateless, task-oriented AI agent core
282 lines (281 loc) • 8.76 kB
JavaScript
import { spawn, spawnSync } from "node:child_process";
import { scrubSecretEnv } 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: scrubSecretEnv(process.env),
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: scrubSecretEnv(process.env),
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: scrubSecretEnv(process.env), stdio: "ignore", detached: true, windowsHide: true });
child.on("error", () => { });
return true;
}
catch {
return false;
}
}