openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
159 lines (158 loc) • 6.43 kB
JavaScript
import { o as isPidDefinitelyDead, r as getProcessInstanceStartTime } from "./pid-alive-CPS_shDt.mjs";
import { d as sleepWithAbort } from "./src-D1BHGNTj.mjs";
import "./runtime-worker-url-CFqA9Dvi.mjs";
import { t as hasErrnoCode } from "./errno-FR4CRyya.mjs";
import "./worker-cpu-B8f4ViUW.mjs";
import "./worker-task-pool-r__tVCXa.mjs";
import "./exec-DdJWnONN.mjs";
import "./exec-spawn-rD1j0ZHJ.mjs";
import { t as readDarwinProcessCommand } from "./darwin-process-command-D1hawpX3.mjs";
import "./linux-oom-score-DhS-zF5g.mjs";
import "./spawn-secret-input-44yUg6Fy.mjs";
import "./worker-task-server-sbLPhqBg.mjs";
import "./supervisor-markers-CHrEUTsQ.mjs";
import "./terminal-pty-hp5Q0pag.mjs";
import { realpathSync } from "node:fs";
import { execFileSync } from "node:child_process";
//#region src/process/orphaned-process.ts
function readProcessCwd(pid) {
const paths = execFileSync("/usr/sbin/lsof", [
"-a",
"-p",
String(pid),
"-d",
"cwd",
"-F0n"
], {
encoding: "utf8",
timeout: 5e3,
maxBuffer: 65536,
stdio: [
"ignore",
"pipe",
"ignore"
]
}).split("\0").filter((field) => field.startsWith("n"));
if (paths.length !== 1 || !paths[0]) throw new Error(`Cannot inspect orphaned process ${pid}'s working directory; inspect it before retrying.`);
return paths[0].slice(1);
}
function readProcesses(pid) {
let output;
try {
output = execFileSync("/bin/ps", [
...pid === void 0 ? ["-A"] : ["-p", String(pid)],
"-o",
"pid=,ppid=,uid=,comm="
], {
encoding: "utf8",
timeout: 5e3,
maxBuffer: 4194304,
stdio: [
"ignore",
"pipe",
"ignore"
]
});
} catch (error) {
if (pid !== void 0 && isPidDefinitelyDead(pid)) return [];
throw error;
}
return output.split("\n").flatMap((line) => {
const [, processId, parentPid, uid, command] = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.+)$/.exec(line) ?? [];
return command ? [{
pid: Number(processId),
parentPid: Number(parentPid),
uid: Number(uid),
command: command.trim()
}] : [];
});
}
function captureProcess(row, command, matchesArguments, cwd) {
if (row.uid !== process.getuid?.() || row.command !== command) return;
try {
const startedAt = getProcessInstanceStartTime(row.pid);
const observed = readDarwinProcessCommand(row.pid, row.uid);
if (!observed || !("argv" in observed) || observed.executable !== command || observed.argv[0] !== command || matchesArguments && !matchesArguments(observed.argv)) return;
if (cwd !== void 0 && readProcessCwd(row.pid) !== cwd) return;
if (startedAt === null) throw new Error(`Cannot verify orphaned process ${row.pid}; stop it manually before retrying.`);
if (getProcessInstanceStartTime(row.pid) !== startedAt) return;
return {
...row,
startedAt,
argv: observed.argv
};
} catch (error) {
if (isPidDefinitelyDead(row.pid)) return;
throw error;
}
}
function isCapturedProcessAlive(target) {
if (isPidDefinitelyDead(target.pid)) return false;
const current = getProcessInstanceStartTime(target.pid);
return current === null || current === target.startedAt;
}
/** Reclaim a launchd-adopted tree while its captured executable and birth still match. */
async function reapOrphanedProcesses(params) {
params.signal?.throwIfAborted();
if (process.platform !== "darwin" || getProcessInstanceStartTime(process.pid) === null) return [];
const cwd = params.cwd === void 0 ? void 0 : realpathSync(params.cwd);
const rows = readProcesses();
const roots = rows.filter((row) => row.parentPid === 1 && row.pid !== process.pid).map((row) => captureProcess(row, params.command, params.matchesArguments, cwd)).filter((row) => row !== void 0);
const reaped = [];
for (const root of roots) {
const captured = /* @__PURE__ */ new Map([[root.pid, root]]);
for (let size = 0; size !== captured.size;) {
size = captured.size;
for (const row of rows) {
const parent = captured.get(row.parentPid);
if (captured.has(row.pid) || !parent || !isCapturedProcessAlive(parent)) continue;
const current = readProcesses(row.pid)[0];
const child = current?.parentPid === parent.pid && captureProcess(current, params.command);
if (child && getProcessInstanceStartTime(parent.pid) === parent.startedAt) captured.set(child.pid, child);
}
}
const canSignal = (target) => {
if (!isCapturedProcessAlive(target)) return false;
const row = readProcesses(target.pid)[0];
const current = row && captureProcess(row, params.command, void 0, target.pid === root.pid ? cwd : void 0);
if (!isCapturedProcessAlive(target)) return false;
if (!current || current.startedAt !== target.startedAt || current.argv.length !== target.argv.length || current.argv.some((value, index) => value !== target.argv[index]) || current.parentPid !== target.parentPid && current.parentPid !== 1 || target.pid === root.pid && !params.matchesArguments(current.argv)) throw new Error(`Orphaned process ${target.pid} changed identity; refusing to signal it.`);
return true;
};
const failures = [];
let signalAttempted = false;
const signalCaptured = (target, signal) => {
try {
if (canSignal(target)) {
if (!signalAttempted) params.signal?.throwIfAborted();
signalAttempted = true;
process.kill(target.pid, signal);
}
} catch (error) {
if (!signalAttempted) throw error;
if (!hasErrnoCode(error, "ESRCH")) failures.push(error);
}
};
const alive = () => [...captured.values()].filter(isCapturedProcessAlive);
const waitForExit = async () => {
const deadline = Date.now() + 5e3;
while (alive().length > 0 && Date.now() < deadline) await sleepWithAbort(Math.min(25, deadline - Date.now()));
};
params.signal?.throwIfAborted();
params.onReap?.(root.pid);
params.signal?.throwIfAborted();
signalCaptured(root, "SIGTERM");
if (!signalAttempted) continue;
for (const target of captured.values()) if (target.pid !== root.pid) signalCaptured(target, "SIGTERM");
await waitForExit();
for (const target of alive()) signalCaptured(target, "SIGKILL");
await waitForExit();
if (alive().length > 0) failures.push(/* @__PURE__ */ new Error(`Orphaned process tree ${root.pid} did not stop; stop it manually.`));
if (failures.length > 0) throw new AggregateError(failures, `Could not safely stop orphaned process tree ${root.pid}.`);
reaped.push(root.pid);
params.signal?.throwIfAborted();
}
return reaped;
}
//#endregion
export { reapOrphanedProcesses as t };