openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
217 lines (216 loc) • 9.98 kB
JavaScript
import { i as extractErrorCode } from "./error-coercion-CwIUUFnu.mjs";
import { i as isPidDefinitelyDead } from "./pid-alive-C45jDwYk.mjs";
import { n as runtimeProcessEntrypoints } from "./runtime-process-entrypoints-Bh4reDp0.mjs";
import { t as createSubsystemLogger } from "./subsystem-EYGCijzb.mjs";
import { n as getRootOptionAwareCommandPath, t as classifyOpenClawArgv } from "./gateway-process-argv-DHt7guiS.mjs";
import { createRequire } from "node:module";
import { spawnSync } from "node:child_process";
import fsSync, { readFileSync, readdirSync } from "node:fs";
import path from "node:path";
//#region src/infra/container-environment.ts
let containerEnvironmentCache;
function isContainerEnvironment() {
return containerEnvironmentCache ??= detectContainerEnvironment();
}
function detectContainerEnvironment() {
if (process.env.FLY_MACHINE_ID?.trim() && process.env.FLY_APP_NAME?.trim()) return true;
for (const sentinelPath of [
"/.dockerenv",
"/run/.containerenv",
"/var/run/.containerenv"
]) try {
fsSync.accessSync(sentinelPath, fsSync.constants.F_OK);
return true;
} catch {}
try {
const cgroup = fsSync.readFileSync("/proc/1/cgroup", "utf8");
if (/\/docker\/|cri-containerd-[0-9a-f]|containerd\/[0-9a-f]{64}|\/kubepods[/.]|\blxc\b/.test(cgroup)) return true;
} catch {}
return false;
}
//#endregion
//#region src/process/supervisor/darwin-process-command.ts
let native;
let reportedForeignArguments = false;
function loadNative() {
const koffi = createRequire(import.meta.url)("koffi");
const sysctl = koffi.load("/usr/lib/libSystem.B.dylib").func("int sysctl(const int *name, unsigned int namelen, void *oldp, size_t *oldlenp, const void *newp, size_t newlen)");
const size = Buffer.alloc(8);
const argMax = Buffer.alloc(4);
size.writeBigUInt64LE(4n);
if (sysctl(new Int32Array([1, 8]), 2, argMax, size, null, 0) !== 0 || argMax.readInt32LE() <= 4) throw new Error("Darwin process argument limit is unavailable");
const buffer = Buffer.alloc(argMax.readInt32LE());
return { readArguments(pid) {
size.writeBigUInt64LE(BigInt(buffer.length));
if (sysctl(new Int32Array([
1,
49,
pid
]), 3, buffer, size, null, 0) !== 0) return { errno: koffi.errno() };
const length = Number(size.readBigUInt64LE());
if (length < 4 || length > buffer.length) throw new Error(`Incomplete Darwin process arguments for PID ${pid}`);
return { bytes: buffer.subarray(0, length) };
} };
}
function parseArguments(bytes, pid) {
const argc = bytes.readInt32LE();
let offset = bytes.indexOf(0, 4);
if (argc <= 0 || argc > bytes.length || offset <= 4) throw new Error(`Invalid Darwin process arguments for PID ${pid}`);
while (offset < bytes.length && bytes[offset] === 0) offset++;
const argv = [];
for (let index = 0; index < argc; index++) {
const end = bytes.indexOf(0, offset);
if (end < 0) throw new Error(`Truncated Darwin process arguments for PID ${pid}`);
argv.push(bytes.toString("utf8", offset, end));
offset = end + 1;
}
const marker = bytes.toString("utf8", offset).split("\0").find((entry) => entry.startsWith("OPENCLAW_SERVICE_MARKER="));
return {
argv,
...marker ? { serviceMarker: marker.slice(24) } : {}
};
}
/** Exact argv, or observed foreign ownership when Darwin denies argument inspection. */
function readDarwinProcessCommand(pid, uid) {
native ??= loadNative();
const result = native.readArguments(pid);
if ("bytes" in result) return parseArguments(result.bytes, pid);
if (isPidDefinitelyDead(pid)) return;
const currentUid = process.getuid?.();
if (uid !== void 0 && currentUid !== void 0 && uid !== currentUid) {
if (!reportedForeignArguments) {
reportedForeignArguments = true;
createSubsystemLogger("process/census").debug("Unreadable Darwin arguments for another UID do not establish capture custody.", {
pid,
uid,
errno: result.errno
});
}
return {
argvUnavailable: true,
uid
};
}
throw new Error(`Could not classify PID ${pid}: cannot inspect Darwin arguments (errno ${result.errno}).`);
}
//#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");
}
//#endregion
//#region src/infra/openclaw-process-census.ts
const workerEntrypoints = Object.values(runtimeProcessEntrypoints).flatMap((entry) => [path.posix.normalize(`src/infra/${entry.sourceWorkerName}.ts`), `dist/${entry.distWorkerPath}`]);
/** Incomplete process inspection never authorizes reclamation of unowned scratch. */
function inspectOtherOpenClawProcesses() {
try {
if (process.platform === "linux" && isContainerEnvironment()) throw new Error("Host process visibility cannot be established from this container. Run Doctor on the host after stopping OpenClaw containers that share its temporary directory.");
const processes = [...readProcessGroupMembers(1e3, { readDarwinCommand: readDarwinProcessCommand })];
const byPid = new Map(processes.map((entry) => [entry.pid, entry]));
const current = byPid.get(process.pid);
if (!current?.command || processes.some((entry) => !entry.command)) throw new Error("OpenClaw process census is incomplete.");
const launchers = /* @__PURE__ */ new Set();
const ancestors = /* @__PURE__ */ new Set([process.pid]);
let parentPid = current.command.ppid;
while (parentPid > 0) {
const parent = byPid.get(parentPid);
if (!parent?.command || ancestors.has(parentPid)) throw new Error("OpenClaw process ancestry is incomplete.");
ancestors.add(parentPid);
if ("argv" in parent.command) {
const { argv, serviceMarker } = parent.command;
const identity = classifyOpenClawArgv(argv, {
pid: parentPid,
serviceMarker,
additionalEntrypoints: workerEntrypoints
});
if (identity.kind === "openclaw" && identity.entryIndex !== void 0 && getRootOptionAwareCommandPath(["node", ...argv.slice(identity.entryIndex)], 1)[0] === "doctor") launchers.add(parentPid);
}
parentPid = parent.command.ppid;
}
return { pids: processes.filter(({ pid, state, command }) => {
if (pid === process.pid || launchers.has(pid)) return false;
if (state.startsWith("Z") && isPidDefinitelyDead(pid)) return false;
if (!command || !("argv" in command)) return false;
if (command.argv.some((arg) => /(?:^|[/\\])openclaw-update-runtime-[A-Za-z0-9]{6}(?:[/\\]|$)/u.test(arg))) return true;
const identity = classifyOpenClawArgv(command.argv, {
pid,
serviceMarker: command.serviceMarker,
additionalEntrypoints: workerEntrypoints
});
if (identity.kind === "unclassified") throw new Error(`Could not classify PID ${pid}: ${identity.reason}`);
return identity.kind === "openclaw";
}).map(({ pid }) => pid) };
} catch (error) {
return { error: `Could not inspect OpenClaw processes: ${String(error)}` };
}
}
//#endregion
export { inspectOtherOpenClawProcesses };