openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
620 lines (619 loc) • 21.2 kB
JavaScript
import { once } from "node:events";
import fs, { closeSync, createWriteStream, readFileSync, readdirSync } from "node:fs";
import { Socket } from "node:net";
import { pipeline } from "node:stream";
import { spawn, spawnSync } from "node:child_process";
//#region packages/normalization-core/src/record-coerce.ts
/** Type guard for non-array object records at browser-safe boundaries. */
function isRecord(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
//#endregion
//#region src/shared/deferred.ts
const promiseWithResolvers = Promise;
function createDeferredCore() {
return promiseWithResolvers.withResolvers();
}
//#endregion
//#region src/process/linux-oom-score.ts
const OOM_SCORE_WRAP_SHELL = "/bin/sh";
const OOM_SCORE_WRAP_SCRIPT = "echo 1000 > /proc/self/oom_score_adj 2>/dev/null; exec \"$0\" \"$@\"";
[
"echo 1000 > /proc/self/oom_score_adj 2>/dev/null; if [ \"${OC_INTERNAL_OOM_EXEC_BASH_ENV+x}\" = x ]; then BASH_ENV=\"$OC_INTERNAL_OOM_EXEC_BASH_ENV\"; export BASH_ENV; fi; unset OC_INTERNAL_OOM_EXEC_BASH_ENV",
"if [ \"${OC_INTERNAL_OOM_EXEC_ENV+x}\" = x ]; then ENV=\"$OC_INTERNAL_OOM_EXEC_ENV\"; export ENV; fi; unset OC_INTERNAL_OOM_EXEC_ENV",
"if [ \"${OC_INTERNAL_OOM_EXEC_CDPATH+x}\" = x ]; then CDPATH=\"$OC_INTERNAL_OOM_EXEC_CDPATH\"; export CDPATH; fi; unset OC_INTERNAL_OOM_EXEC_CDPATH",
"if [ \"${OC_INTERNAL_OOM_EXEC_PS4+x}\" = x ]; then PS4=\"$OC_INTERNAL_OOM_EXEC_PS4\"; export PS4; fi; unset OC_INTERNAL_OOM_EXEC_PS4; exec \"$0\" \"$@\""
].join("; ");
function isWrapped(command, args) {
return command === OOM_SCORE_WRAP_SHELL && args[0] === "-c" && args[1] === OOM_SCORE_WRAP_SCRIPT;
}
/** Only small spawn owners may temporarily raise their score; never call from the Gateway. */
function spawnWithInheritedOomScore(command, args, options) {
if (process.platform !== "linux" || !isWrapped(command, args) || !args[2] || options.shell || options.argv0 !== void 0) return spawn(command, args, options);
let scoreFd;
let original;
try {
scoreFd = fs.openSync("/proc/self/oom_score_adj", "r+");
original = fs.readFileSync(scoreFd, "utf8").trim();
fs.writeSync(scoreFd, "1000", 0, "utf8");
} catch {
if (scoreFd !== void 0) fs.closeSync(scoreFd);
return spawn(command, args, options);
}
try {
return spawn(args[2], args.slice(3), options);
} finally {
try {
fs.writeSync(scoreFd, original, 0, "utf8");
} catch {
process.emitWarning("Could not restore spawn owner's Linux OOM score");
} finally {
fs.closeSync(scoreFd);
}
}
}
//#endregion
//#region src/process/supervisor/cancellation-policy.ts
const GRACEFUL_CANCEL_TIMEOUT_MS = 5e3;
//#endregion
//#region packages/normalization-core/src/error-coercion.ts
function readProperty(value, key) {
try {
return value[key];
} catch {
return;
}
}
function extractErrorCode(err) {
if (!err || typeof err !== "object") return;
const code = readProperty(err, "code");
if (typeof code === "string") return code;
if (typeof code === "number") return String(code);
}
//#endregion
//#region src/shared/pid-alive.ts
function isValidPid(pid) {
return Number.isInteger(pid) && pid > 0;
}
/**
* Check if every thread has exited by reading Linux /proc/<pid>/status.
* Returns false on non-Linux platforms or if the proc file can't be read.
*/
function isZombieProcess(pid) {
if (process.platform !== "linux") return false;
try {
const status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
return status.match(/^State:\s+(\S)/m)?.[1] === "Z" && /^Threads:[ \t]+1[ \t]*$/m.test(status);
} catch {
return false;
}
}
/** Returns true only when the PID is invalid, missing, or known to be a Linux zombie. */
function isPidDefinitelyDead(pid) {
if (!isValidPid(pid)) return true;
try {
process.kill(pid, 0);
} catch (err) {
return err.code === "ESRCH";
}
return isZombieProcess(pid);
}
//#endregion
//#region src/process/supervisor/service-child-group-ownership.ts
/** The caller supplies native command inspection; the standalone group worker stays dependency-free. */
function* readProcessGroupMembers(timeoutMs, commandInspection) {
const includeCommand = commandInspection !== void 0;
if (process.platform === "linux") {
const deadline = Date.now() + timeoutMs;
for (const name of readdirSync("/proc")) {
if (Date.now() >= deadline) throw new Error("Process group census exceeded its deadline");
if (!/^\d+$/.test(name)) continue;
const pid = Number(name);
let stat;
let argv;
try {
stat = readFileSync(`/proc/${name}/stat`, "utf8");
if (includeCommand) argv = readFileSync(`/proc/${name}/cmdline`, "utf8").split("\0").filter(Boolean);
} catch (error) {
if (pid !== process.pid && ["ENOENT", "ESRCH"].includes(extractErrorCode(error) ?? "")) continue;
throw new Error(`Could not classify PID ${pid}: process command inspection failed (${extractErrorCode(error) ?? "unavailable"}).`, { cause: error });
}
const match = /^(\d+) \([\s\S]*\) (\S) (\d+) (\d+)(?:\s|$)/.exec(stat);
if (!match || Number(match[1]) !== pid || Date.now() >= deadline) throw new Error("Process group census is unavailable");
if (argv?.length === 0) {
const flags = Number(stat.slice(stat.lastIndexOf(")") + 2).split(/\s+/)[6]);
if (!(Number.isInteger(flags) && (flags & 2097152) !== 0) && !isPidDefinitelyDead(pid)) throw new Error(`Could not classify PID ${pid}: live userspace process has no readable arguments.`);
}
yield {
pid,
pgid: Number(match[4]),
state: match[2],
...argv ? { command: {
ppid: Number(match[3]),
argv
} } : {}
};
}
if (Date.now() >= deadline) throw new Error("Process group census exceeded its deadline");
return;
}
if (includeCommand && process.platform !== "darwin") throw new Error(`Exact process command census is unavailable on ${process.platform}.`);
const deadline = Date.now() + timeoutMs;
const census = spawnSync("/bin/ps", [
"-A",
"-o",
includeCommand ? "pid=,pgid=,stat=,ppid=,uid=" : "pid=,pgid=,stat="
], {
encoding: "utf8",
timeout: timeoutMs,
maxBuffer: 4194304
});
if (census.error || census.status !== 0) throw new Error("Process group census is unavailable");
for (const line of census.stdout.split("\n")) {
if (!line.trim()) continue;
const match = includeCommand ? /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(\d+)\s+(-?\d+)\s*$/.exec(line) : /^\s*(\d+)\s+(\d+)\s+(\S+)\s*$/.exec(line);
if (!match) throw new Error("Process group census is unavailable");
const pid = Number(match[1]);
if (pid !== census.pid) {
if (includeCommand && Date.now() >= deadline) throw new Error("Process group census exceeded its deadline");
const command = includeCommand ? match[3].startsWith("Z") || pid === 0 ? { argv: [] } : commandInspection?.readDarwinCommand(pid, Number(match[5]) >>> 0) : void 0;
if (includeCommand && !command) continue;
yield {
pid,
pgid: Number(match[2]),
state: match[3],
...command ? { command: {
ppid: Number(match[4]),
...command
} } : {}
};
}
}
if (includeCommand && Date.now() >= deadline) throw new Error("Process group census exceeded its deadline");
}
/** Advisory retirement timing only; the host owns kernel group-disappearance proof. */
function hasLiveOwnedProcessGroupMembers(timeoutMs = 1e3) {
let observedOwner = false;
try {
for (const { pid, pgid, state } of readProcessGroupMembers(Math.max(1, Math.min(1e3, timeoutMs)))) if (pid === process.pid) {
if (pgid !== process.pid) return;
observedOwner = true;
} else if (pgid === process.pid && (!state.startsWith("Z") || process.platform === "linux" && !isPidDefinitelyDead(pid))) return true;
} catch {
return;
}
return observedOwner ? false : void 0;
}
//#endregion
//#region src/process/supervisor/service-child-protocol.ts
function encodeServiceChildMessage(message) {
return `${JSON.stringify(message)}\n`;
}
//#endregion
//#region src/process/supervisor/service-child-stdio.ts
function setStdioEntry(stdio, fd, value) {
while (stdio.length <= fd) stdio.push("ignore");
stdio[fd] = value;
}
function reserveStdioEntry(stdio, value) {
let fd = 3;
while (stdio[fd] !== void 0 && stdio[fd] !== "ignore") fd += 1;
setStdioEntry(stdio, fd, value);
return fd;
}
//#endregion
//#region src/process/supervisor/service-child-group-anchor.ts
function commandStdio(start) {
const stdio = [
start.stdinMode === "inherit" ? "inherit" : "pipe",
"pipe",
"pipe"
];
if (start.secretFd !== void 0) setStdioEntry(stdio, start.secretFd, start.secretFd);
const lineageFd = reserveStdioEntry(stdio, start.lineageFd ?? "pipe");
const inheritedLineageFds = [lineageFd, ...(start.parentLineageFds ?? []).map((fd) => reserveStdioEntry(stdio, fd))];
if (start.ownedWorker) stdio.push("ipc");
return {
stdio,
lineageFd,
inheritedLineageFds
};
}
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms).unref?.();
});
}
function runServiceChildGroupAnchor() {
let start;
let state = "starting";
let sequence = 0;
let lastHostSequence = 0;
let command;
let lineageCompletion;
let inheritedLineageClosed = false;
let workerStarted = false;
let workerLineageFds = [];
let control;
let rootSettlementStarted = false;
let rootResultDelivery;
let rootExit;
let stdoutDrained = false;
let stderrDrained = false;
let lineageClosed = false;
let lineageObservationFailed = false;
let markHostLineageClosed;
let forceCleanup = false;
const forceCleanupRequested = createDeferredCore();
const lineageDone = createDeferredCore();
const rootExited = createDeferredCore();
const rootSettledDone = createDeferredCore();
const startupErrorAcknowledged = createDeferredCore();
const retirementReady = createDeferredCore();
let closingSequence;
const send = async (message) => {
if (!start || !control || control.destroyed) return;
sequence += 1;
await new Promise((resolve) => {
const framed = {
...message,
generation: start.generation,
sequence
};
control.write(encodeServiceChildMessage(framed), () => resolve());
});
};
const closeAuthority = async (reason, hardKill, deadline = Date.now() + GRACEFUL_CANCEL_TIMEOUT_MS) => {
if (!start || state === "closed") return;
state = "closed";
if (hardKill && start.lineageFd === void 0 && !lineageClosed) {
process.kill(0, "SIGKILL");
return;
}
const requiresAcknowledgement = start.acknowledgeClosing === true;
closingSequence = requiresAcknowledgement ? sequence + 1 : void 0;
const remainingMs = deadline - Date.now();
if (remainingMs > 0) send({
type: "closing",
reason
}).then(() => {
if (!requiresAcknowledgement) retirementReady.resolve(true);
}, () => retirementReady.resolve(false));
if (remainingMs <= 0 || !await Promise.race([retirementReady.promise, delay(remainingMs).then(() => false)]) || Date.now() >= deadline || hardKill) {
process.kill(0, "SIGKILL");
return;
}
control?.end(() => process.exit(0));
};
const closeInheritedLineage = () => {
if (start?.lineageFd !== void 0 && !inheritedLineageClosed) {
inheritedLineageClosed = true;
closeSync(start.lineageFd);
}
};
const reportStartupFailure = async (error, hardKill = false) => {
closeInheritedLineage();
await send({
type: "startup-error",
error
});
await Promise.race([startupErrorAcknowledged.promise, retirementReady.promise]);
await closeAuthority("lineage-lost", hardKill);
};
const requestCleanup = async (reason, signal = "SIGTERM") => {
if (!start || state === "closed") return;
if (state === "closing") {
forceCleanup ||= signal === "SIGKILL";
if (forceCleanup) forceCleanupRequested.resolve();
return;
}
state = "closing";
forceCleanup = signal === "SIGKILL";
if (!command) {
await reportStartupFailure("command startup cancelled before spawn", true);
return;
}
const cleanupDeadline = Date.now() + GRACEFUL_CANCEL_TIMEOUT_MS;
const termGraceDone = delay(GRACEFUL_CANCEL_TIMEOUT_MS);
if (start.ownedWorker) {
if (!forceCleanup) process.kill(0, "SIGTERM");
const settled = Promise.all([rootExited.promise, lineageDone.promise]);
if (!forceCleanup) await Promise.race([
settled,
termGraceDone,
forceCleanupRequested.promise
]);
if (!rootExit) command?.kill("SIGKILL");
await settled;
await rootResultDelivery;
let lineageRecorded = false;
if (rootExit && lineageClosed && lineageCompletion) try {
lineageRecorded = lineageCompletion.recordNodeWorkerLineageSettled(start.cleanupBinding);
} catch {}
if (!lineageRecorded) await send({
type: "output",
stream: "stderr",
chunk: "node worker lineage completion was not recorded; restart recovery will retain capacity until cleanup can be verified\n"
});
if (!forceCleanup) await Promise.race([
rootSettledDone.promise,
termGraceDone,
forceCleanupRequested.promise
]);
await closeAuthority(reason, true);
return;
}
if (!forceCleanup) {
process.kill(0, "SIGTERM");
await Promise.race([
lineageDone.promise,
termGraceDone,
forceCleanupRequested.promise
]);
}
if (state !== "closing" || !start) return;
if (lineageClosed && !rootExit && !forceCleanup) await Promise.race([
rootExited.promise,
termGraceDone,
forceCleanupRequested.promise
]);
if (state !== "closing" || !start) return;
if (rootExit && !forceCleanup) {
await Promise.race([
rootSettledDone.promise,
termGraceDone,
forceCleanupRequested.promise
]);
if (state !== "closing" || !start) return;
}
for (;;) {
if (forceCleanup || !rootExit || !stdoutDrained || !stderrDrained || !lineageClosed) break;
const remainingMs = cleanupDeadline - Date.now();
if (remainingMs <= 0) break;
if (hasLiveOwnedProcessGroupMembers(remainingMs) === false) {
await closeAuthority(reason, false, cleanupDeadline);
return;
}
const nextObservationMs = Math.min(100, cleanupDeadline - Date.now());
if (nextObservationMs <= 0) break;
await Promise.race([delay(nextObservationMs), forceCleanupRequested.promise]);
}
await closeAuthority(reason, true);
};
const onControlMessage = (message) => {
if (!start || message.generation !== start.generation || !Number.isSafeInteger(message.sequence) || message.sequence <= lastHostSequence) return;
if (message.type === "closing-ack") {
if (state === "closed" && closingSequence !== void 0 && message.closingSequence === closingSequence) {
lastHostSequence = message.sequence;
retirementReady.resolve(true);
}
return;
}
if (state === "closed") return;
lastHostSequence = message.sequence;
if (message.type === "startup-error-ack") {
startupErrorAcknowledged.resolve();
return;
}
if (message.type === "lineage-closed") {
markHostLineageClosed?.();
return;
}
if (message.type === "worker-close") {
if (start.ownedWorker && !command) requestCleanup("cancel");
else if (start.ownedWorker && command?.connected) command.disconnect();
return;
}
if (message.type === "worker-start") {
if (!start.ownedWorker || workerStarted || state !== "active") return;
if (!command?.connected) {
requestCleanup("parent-lost");
return;
}
workerStarted = true;
command.send({
type: "openclaw-worker-start-v1",
lineageFds: workerLineageFds
}, (error) => {
if (error) requestCleanup("parent-lost");
});
return;
}
requestCleanup("cancel", message.signal);
};
const startCommand = async (next) => {
const controlFd = next.controlFd;
if (controlFd === void 0) {
process.exitCode = 1;
return;
}
start = next;
const socket = process.versions.bun ? new Socket({
readable: true,
writable: true
}) : new Socket({
fd: controlFd,
readable: true,
writable: true
});
control = socket;
socket.setEncoding("utf8");
let pending = "";
socket.on("data", (chunk) => {
pending += chunk;
for (;;) {
const newline = pending.indexOf("\n");
if (newline < 0) break;
const line = pending.slice(0, newline);
pending = pending.slice(newline + 1);
try {
onControlMessage(JSON.parse(line));
} catch {
requestCleanup("parent-lost");
}
}
});
const onControlLoss = () => {
retirementReady.resolve(false);
if (state !== "closed") requestCleanup("parent-lost");
};
socket.once("end", onControlLoss);
socket.once("close", onControlLoss);
socket.once("error", onControlLoss);
if (process.versions.bun) socket.connect({ fd: controlFd });
lineageCompletion = (next.ownedWorker, void 0);
if (start !== next || control !== socket || state !== "starting" || socket.destroyed || socket.readableEnded || socket.writableEnded || !process.connected) {
await requestCleanup("parent-lost");
return;
}
const { stdio, lineageFd, inheritedLineageFds } = commandStdio(start);
workerLineageFds = inheritedLineageFds;
try {
command = spawnWithInheritedOomScore(start.command, start.args, {
cwd: start.cwd,
env: start.env,
argv0: start.argv0,
stdio,
detached: false,
windowsHide: true
});
await once(command, "spawn");
} catch (error) {
await reportStartupFailure(error instanceof Error ? error.message : String(error));
return;
}
const markLineageClosed = () => {
if (lineageClosed || lineageObservationFailed) return;
lineageClosed = true;
lineageDone.resolve();
if (state === "active") (async () => {
await rootExited.promise;
if (state !== "active") return;
if (rootSettlementStarted) await rootSettledDone.promise;
if (state !== "active") return;
requestCleanup("lineage-lost");
})();
};
if (start.lineageFd !== void 0) {
markHostLineageClosed = markLineageClosed;
closeInheritedLineage();
} else {
const lineage = command.stdio[lineageFd];
if (!lineage) {
await send({
type: "startup-error",
error: "command lineage pipe was not created"
});
await requestCleanup("lineage-lost", "SIGKILL");
return;
}
lineage.once("end", markLineageClosed);
const markLineageFailed = () => {
if (lineageClosed || lineageObservationFailed) return;
lineageObservationFailed = true;
lineageDone.resolve();
requestCleanup("lineage-lost");
};
lineage.once("close", () => {
if (!lineage.readableEnded) markLineageFailed();
});
lineage.once("error", markLineageFailed);
lineage.resume();
}
const settleRoot = async () => {
if (rootSettlementStarted || !rootResultDelivery || !stdoutDrained || !stderrDrained) return;
rootSettlementStarted = true;
await rootResultDelivery;
rootSettledDone.resolve();
if (lineageClosed && state === "active") await requestCleanup("lineage-lost");
};
const stdout = process.versions.bun ? createWriteStream("", {
fd: 1,
autoClose: true
}) : process.stdout;
const stderr = process.versions.bun ? createWriteStream("", {
fd: 2,
autoClose: true
}) : process.stderr;
pipeline(command.stdout, stdout, () => {
stdoutDrained = true;
settleRoot();
});
pipeline(command.stderr, stderr, () => {
stderrDrained = true;
settleRoot();
});
if (start.stdinMode !== "inherit" && command.stdin) {
const input = process.stdin;
const destination = command.stdin;
const endInput = () => destination.end();
let inputClosed = false;
const stopInput = () => {
input.unpipe(destination);
input.off("end", endInput);
input.destroy();
if (!inputClosed) {
inputClosed = true;
send({ type: "stdin-closed" });
}
};
destination.once("error", stopInput);
destination.once("close", stopInput);
input.pipe(destination, { end: false });
if (input.readableEnded) endInput();
else input.once("end", endInput);
}
command.once("error", (error) => {
if (state === "starting") reportStartupFailure(error.message);
});
if (start.ownedWorker) command.on("message", (message) => {
if (state === "active") send({
type: "worker-message",
message
});
});
if (command.pid && state === "starting") {
state = "active";
send({
type: "ready",
commandPid: command.pid,
anchorPid: process.pid
});
}
command.once("exit", (code, signal) => {
rootExit = {
code,
signal
};
rootResultDelivery = send({
type: "root-result",
code,
signal
});
rootExited.resolve();
settleRoot();
});
};
const onParentSignal = () => {
if (state === "active" || start && state === "starting") requestCleanup("parent-lost");
};
process.on("SIGTERM", onParentSignal);
process.on("SIGINT", onParentSignal);
process.once("disconnect", () => {
retirementReady.resolve(false);
if (state !== "closed") requestCleanup("parent-lost");
});
process.on("message", (raw) => {
const message = raw;
if (message.type === "start" && !start && state === "starting") {
if (isRecord(raw) && raw.acknowledgeClosing !== void 0 && raw.acknowledgeClosing !== true) process.exit(1);
startCommand(message);
} else if (message.type === "parent-loss" && message.generation === start?.generation) {
retirementReady.resolve(false);
requestCleanup("parent-lost");
}
});
}
runServiceChildGroupAnchor();
//#endregion
export { runServiceChildGroupAnchor };