UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

255 lines (254 loc) • 9.6 kB
import { spawn } from "node:child_process"; import { constants } from "node:os"; import path from "node:path"; import fs from "node:fs"; //#region \0rolldown/runtime.js var __esmMin = (fn, res, err) => () => { if (err) throw err[0]; try { return fn && (res = fn(fn = 0)), res; } catch (e) { throw err = [e], e; } }; //#endregion //#region scripts/lib/managed-child-process.mts /** Return the conventional shell exit code for a signal. */ function signalExitCode(signal) { const signalNumber = signalNumberFor(signal); return signalNumber ? 128 + signalNumber : 1; } function signalNumberFor(signal) { switch (signal) { case "SIGHUP": return 1; case "SIGINT": return 2; case "SIGTERM": return 15; default: return constants.signals?.[signal] ?? 0; } } //#endregion //#region packages/normalization-core/src/mountinfo-path.ts __esmMin((() => {}))(); const MOUNT_PATH_OCTAL_ESCAPE_RE = /\\([0-7]{3})/g; /** Decodes an octal-escaped path field from a Linux procfs mount table. */ function decodeMountInfoPath(value) { return value.replace(MOUNT_PATH_OCTAL_ESCAPE_RE, (_match, octal) => String.fromCharCode(Number.parseInt(octal, 8))); } //#endregion //#region scripts/lib/process-memory.mts const DEFAULT_CGROUP_V2_MOUNT_PATH = "/sys/fs/cgroup"; const DEFAULT_CGROUP_V1_MEMORY_MOUNT_PATH = "/sys/fs/cgroup/memory"; const PROC_SELF_CGROUP_PATH = "/proc/self/cgroup"; const PROC_SELF_MOUNTINFO_PATH = "/proc/self/mountinfo"; const CGROUP_V2_MEMORY_LIMIT_FILES = ["memory.max", "memory.high"]; const CGROUP_V1_MEMORY_LIMIT_FILES = ["memory.limit_in_bytes"]; function parseCgroupMemoryLimitBytes(value) { const trimmed = value.trim(); if (trimmed === "" || trimmed === "max" || !/^\d+$/u.test(trimmed)) return null; const parsed = BigInt(trimmed); if (parsed < 0n || parsed > BigInt(Number.MAX_SAFE_INTEGER)) return null; return Number(parsed); } function resolveCgroupMountPoints(params = {}) { const fsImpl = params.fs ?? fs; let rawMountinfo = ""; try { rawMountinfo = fsImpl.readFileSync(PROC_SELF_MOUNTINFO_PATH, "utf8"); } catch {} const unified = []; const v1Memory = []; for (const line of rawMountinfo.split("\n")) { const [fields, describe] = line.split(" - "); const mountFields = (fields ?? "").split(" "); const rawRoot = mountFields[3]; const rawMountPoint = mountFields[4]; const [fsType, , superOptions] = (describe ?? "").split(" "); if (!rawMountPoint || !rawRoot) continue; const root = decodeMountInfoPath(rawRoot); const mountPoint = decodeMountInfoPath(rawMountPoint); if (fsType === "cgroup2") unified.push({ mountPoint, observed: true, root }); else if (fsType === "cgroup" && (superOptions ?? "").split(",").includes("memory")) v1Memory.push({ mountPoint, observed: true, root }); } return { unified: unified.length > 0 ? unified : [{ mountPoint: DEFAULT_CGROUP_V2_MOUNT_PATH, observed: false, root: "/" }], v1Memory: v1Memory.length > 0 ? v1Memory : [{ mountPoint: DEFAULT_CGROUP_V1_MEMORY_MOUNT_PATH, observed: false, root: "/" }] }; } function relativeCgroupPath(mountRoot, cgroupPath) { if (cgroupPath.split("/").includes("..")) return null; if (mountRoot === "/") return cgroupPath; const mountRootSegments = mountRoot.split("/").filter(Boolean); if (mountRootSegments.length > 0 && mountRootSegments.every((segment) => segment === "..")) return null; if (mountRootSegments.includes("..")) return null; if (cgroupPath === "/") return null; if (cgroupPath === mountRoot) return "/"; return cgroupPath.startsWith(`${mountRoot}/`) ? cgroupPath.slice(mountRoot.length) : null; } function resolveCgroupMemoryLimitPaths(params = {}) { const fsImpl = params.fs ?? fs; let rawCgroup = ""; let cgroupRecordReadFailed = false; try { rawCgroup = fsImpl.readFileSync(PROC_SELF_CGROUP_PATH, "utf8"); } catch { cgroupRecordReadFailed = (params.platform ?? process.platform) === "linux"; } const paths = []; const addHierarchy = (mounts, limitFiles, cgroupPath, hierarchyFile) => { const initialPathCount = paths.length; let addedObservedPath = false; let hierarchyMetadataUnreadable = false; for (const mount of mounts) { const mountInitialPathCount = paths.length; const relative = relativeCgroupPath(mount.root, cgroupPath); if (relative === null) continue; const segments = relative.split("/").filter(Boolean); for (let depth = segments.length; depth >= 0; depth -= 1) { if (hierarchyFile && depth < segments.length) try { const hierarchyPath = path.join(mount.mountPoint, ...segments.slice(0, depth), hierarchyFile); const hierarchyMode = fsImpl.readFileSync(hierarchyPath, "utf8").trim(); if (hierarchyMode === "0") break; if (hierarchyMode !== "1") { hierarchyMetadataUnreadable = true; break; } } catch { hierarchyMetadataUnreadable = true; break; } for (const limitFile of limitFiles) paths.push(path.join(mount.mountPoint, ...segments.slice(0, depth), limitFile)); } addedObservedPath ||= mount.observed && paths.length > mountInitialPathCount; } return { added: paths.length > initialPathCount, addedObservedPath, hierarchyMetadataUnreadable }; }; const mounts = resolveCgroupMountPoints(params); let sawMemoryRecord = false; let sawObservedV2Mapping = false; let sawObservedV2Root = false; let sawUnreadableV1HierarchyMetadata = false; let sawV1MemoryRecord = false; let sawUnresolvedCgroupLimit = false; for (const line of rawCgroup.split("\n")) { const record = /^\d+:([^:]*):(.*)$/u.exec(line); if (!record) continue; const controllers = record[1] ?? ""; if (controllers === "") { sawMemoryRecord = true; const cgroupPath = record[2] ?? ""; sawObservedV2Root ||= cgroupPath === "/" && mounts.unified.some((mount) => mount.observed && mount.root === "/"); const resolved = addHierarchy(mounts.unified, CGROUP_V2_MEMORY_LIMIT_FILES, cgroupPath); sawObservedV2Mapping ||= resolved.addedObservedPath; sawUnresolvedCgroupLimit ||= !resolved.added; } else if (controllers.split(",").includes("memory")) { sawMemoryRecord = true; sawV1MemoryRecord = true; const resolved = addHierarchy(mounts.v1Memory, CGROUP_V1_MEMORY_LIMIT_FILES, record[2] ?? "", "memory.use_hierarchy"); sawUnreadableV1HierarchyMetadata ||= resolved.hierarchyMetadataUnreadable; sawUnresolvedCgroupLimit ||= !resolved.added; } } if (!sawMemoryRecord) { for (const mount of mounts.unified) addHierarchy([mount], CGROUP_V2_MEMORY_LIMIT_FILES, mount.root); for (const mount of mounts.v1Memory) addHierarchy([mount], CGROUP_V1_MEMORY_LIMIT_FILES, mount.root); } return { paths, cgroupRecordReadFailed, sawMemoryRecord, sawObservedV2Mapping, sawObservedUnconstrainedV2Root: sawObservedV2Root && !sawV1MemoryRecord, sawUnresolvedCgroupLimit, sawUnreadableV1HierarchyMetadata, sawV1MemoryRecord }; } /** Verify actual kernel containment, rather than accepting an environment or manager claim. */ function hasLinuxMemoryContainment(maxBytes, params = {}, scope) { if ((params.platform ?? process.platform) !== "linux") return false; const resolved = resolveCgroupMemoryLimitPaths(params); if (!resolved.sawObservedV2Mapping || resolved.cgroupRecordReadFailed || resolved.sawUnresolvedCgroupLimit) return false; const files = params.fs ?? fs; return resolved.paths.some((file) => { if (path.basename(file) !== "memory.max") return false; if (scope && path.basename(path.dirname(file)) !== scope) return false; try { const limit = parseCgroupMemoryLimitBytes(files.readFileSync(file, "utf8")); const directory = path.dirname(file); return limit !== null && limit > 0 && limit <= maxBytes && files.readFileSync(path.join(directory, "memory.swap.max"), "utf8").trim() === "0" && files.readFileSync(path.join(directory, "memory.oom.group"), "utf8").trim() === "1"; } catch { return false; } }); } //#endregion //#region scripts/lib/managed-memory-launcher.mts const [rawLimit, scope, shell, bin, ...args] = process.argv.slice(2); const maxBytes = Number(rawLimit); if (!scope || !bin || shell !== "true" && shell !== "false" || !Number.isSafeInteger(maxBytes) || maxBytes <= 0 || !hasLinuxMemoryContainment(maxBytes, {}, scope)) { console.error("[memory] The owned cgroup has no verified memory/swap limit; semantic command was not started. Use a cgroup-v2 host with a user systemd manager or a bounded Crabbox."); process.exitCode = 75; } else { const env = { ...process.env, NODE_OPTIONS: process.env.OPENCLAW_MANAGED_NODE_OPTIONS ?? "" }; delete env.OPENCLAW_MANAGED_NODE_OPTIONS; const child = spawn(bin, args, { env, stdio: "inherit", shell: shell === "true" }); let received; let exitSignal; const remember = (signal) => { received ??= signal; }; const handlers = [ "SIGINT", "SIGTERM", "SIGHUP" ].map((signal) => [signal, () => remember(signal)]); for (const [signal, handler] of handlers) process.on(signal, handler); process.exitCode = await new Promise((resolve) => { child.once("error", (error) => { console.error(error); resolve(75); }); child.once("exit", (status, signal) => { exitSignal = received ?? signal ?? void 0; resolve(exitSignal ? signalExitCode(exitSignal) : status ?? 75); }); }); for (const [signal, handler] of handlers) process.off(signal, handler); if (exitSignal) { if (exitSignal !== "SIGKILL" && exitSignal !== "SIGSTOP") { const reset = () => {}; process.on(exitSignal, reset); process.off(exitSignal, reset); } process.kill(process.pid, exitSignal); } } //#endregion export {};