import { spawnSync } from "node:child_process"; import { parseStrictFiniteNumber, parseStrictInteger } from "openclaw/plugin-sdk/number-runtime"; type ProcessTreeSnapshot = { childrenByParent: Map; cpuByPid: Map; rssByPid: Map; }; function isPlainObject(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function parsePositiveInteger(value: unknown): number | null { const parsed = parseStrictInteger(value); if (parsed === undefined || parsed <= 0) { return null; } return parsed; } function parseNonNegativeInteger(value: unknown): number | null { const parsed = parseStrictInteger(value); if (parsed === undefined || parsed < 0) { return null; } return parsed; } function parseNonNegativeNumber(value: unknown): number | null { const parsed = parseStrictFiniteNumber(value); if (parsed === undefined || parsed < 0) { return null; } return parsed; } export function parsePsCpuTimeMs(raw: string): number | null { const match = raw.trim().match(/^(?:(\d+)-)?(\d+):(\d{2}(?:\.\d+)?)(?::(\d{2}(?:\.\d+)?))?$/u); if (!match) { return null; } const [, daysRaw, firstRaw, secondRaw, thirdRaw] = match; if (daysRaw !== undefined && thirdRaw === undefined) { return null; } const days = daysRaw === undefined ? 0 : Number(daysRaw); const first = Number(firstRaw); const second = Number(secondRaw); const third = thirdRaw === undefined ? 0 : Number(thirdRaw); const values = [days, first, second, third]; if (values.some((part) => !Number.isFinite(part) || part < 0)) { return null; } if (thirdRaw !== undefined && !Number.isInteger(second)) { return null; } if (second >= 60 || (thirdRaw !== undefined && third >= 60)) { return null; } if (daysRaw !== undefined && thirdRaw !== undefined) { return Math.round((days * 24 * 60 * 60 + first * 60 * 60 + second * 60 + third) * 1000); } if (thirdRaw !== undefined) { return Math.round((first * 60 * 60 + second * 60 + third) * 1000); } return Math.round((first * 60 + second) * 1000); } export function parsePsRssBytes(raw: string): number | null { const trimmed = raw.trim(); if (!trimmed) { return null; } const rssKiB = parseStrictFiniteNumber(trimmed); if (rssKiB === undefined || rssKiB < 0) { return null; } return Math.round(rssKiB * 1024); } export function parseWindowsProcessCpuTimeMs(params: { kernelModeTime: unknown; userModeTime: unknown; }): number | null { const kernelModeTime = parseNonNegativeNumber(params.kernelModeTime); const userModeTime = parseNonNegativeNumber(params.userModeTime); if (kernelModeTime === null || userModeTime === null) { return null; } return Math.round((kernelModeTime + userModeTime) / 10_000); } export function parseWindowsWorkingSetBytes(raw: unknown): number | null { const parsed = parseNonNegativeNumber(raw); return parsed === null ? null : Math.round(parsed); } export function parseWindowsProcessTreeSnapshot(raw: string): ProcessTreeSnapshot | null { let parsed: unknown; try { parsed = JSON.parse(raw); } catch { return null; } const entries = Array.isArray(parsed) ? parsed : isPlainObject(parsed) ? [parsed] : []; if (entries.length === 0) { return null; } const childrenByParent = new Map(); const cpuByPid = new Map(); const rssByPid = new Map(); for (const entry of entries) { if (!isPlainObject(entry)) { continue; } const pid = parsePositiveInteger(entry.ProcessId); const ppid = parseNonNegativeInteger(entry.ParentProcessId); if (pid === null || ppid === null) { continue; } const children = childrenByParent.get(ppid) ?? []; children.push(pid); childrenByParent.set(ppid, children); const cpuMs = parseWindowsProcessCpuTimeMs({ kernelModeTime: entry.KernelModeTime, userModeTime: entry.UserModeTime, }); if (cpuMs !== null) { cpuByPid.set(pid, cpuMs); } const rssBytes = parseWindowsWorkingSetBytes(entry.WorkingSetSize); if (rssBytes !== null) { rssByPid.set(pid, rssBytes); } } return { childrenByParent, cpuByPid, rssByPid, }; } function collectProcessTreeMetric( rootPid: number, childrenByParent: Map, metricByPid: Map, ): number | null { if (!metricByPid.has(rootPid)) { return null; } let total = 0; const seen = new Set(); const stack: number[] = [rootPid]; while (stack.length > 0) { const pid = stack.pop(); if (pid === undefined || seen.has(pid)) { continue; } seen.add(pid); total += metricByPid.get(pid) ?? 0; for (const childPid of childrenByParent.get(pid) ?? []) { stack.push(childPid); } } return total; } function readWindowsProcessTreeSnapshot(): ProcessTreeSnapshot | null { const result = spawnSync( "powershell.exe", [ "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", [ "$ErrorActionPreference='Stop';", "Get-CimInstance Win32_Process |", "Select-Object ProcessId,ParentProcessId,KernelModeTime,UserModeTime,WorkingSetSize |", "ConvertTo-Json -Compress", ].join(" "), ], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"], }, ); if (result.status !== 0) { return null; } return parseWindowsProcessTreeSnapshot(result.stdout); } export function readProcessTreeCpuMs(rootPid: number | null | undefined): number | null { if (typeof rootPid !== "number" || !Number.isInteger(rootPid) || rootPid <= 0) { return null; } if (process.platform === "win32") { const snapshot = readWindowsProcessTreeSnapshot(); return snapshot ? collectProcessTreeMetric(rootPid, snapshot.childrenByParent, snapshot.cpuByPid) : null; } const result = spawnSync("ps", ["-eo", "pid=,ppid=,time="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }); if (result.status !== 0) { return null; } const childrenByParent = new Map(); const cpuByPid = new Map(); for (const line of result.stdout.split("\n")) { const match = line.trim().match(/^(\d+)\s+(\d+)\s+(\S+)$/u); if (!match) { continue; } const [, pidRaw, ppidRaw, cpuRaw] = match; const pid = Number(pidRaw); const ppid = Number(ppidRaw); const cpuMs = parsePsCpuTimeMs(cpuRaw ?? ""); if (!Number.isInteger(pid) || !Number.isInteger(ppid) || cpuMs === null) { continue; } cpuByPid.set(pid, cpuMs); const children = childrenByParent.get(ppid) ?? []; children.push(pid); childrenByParent.set(ppid, children); } if (!cpuByPid.has(rootPid)) { return null; } return collectProcessTreeMetric(rootPid, childrenByParent, cpuByPid); } export function readProcessTreeRssBytes(rootPid: number | null | undefined): number | null { if (typeof rootPid !== "number" || !Number.isInteger(rootPid) || rootPid <= 0) { return null; } if (process.platform === "win32") { const snapshot = readWindowsProcessTreeSnapshot(); return snapshot ? collectProcessTreeMetric(rootPid, snapshot.childrenByParent, snapshot.rssByPid) : null; } const result = spawnSync("ps", ["-eo", "pid=,ppid=,rss="], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }); if (result.status !== 0) { return null; } const childrenByParent = new Map(); const rssByPid = new Map(); for (const line of result.stdout.split("\n")) { const match = line.trim().match(/^(\d+)\s+(\d+)\s+(\S+)$/u); if (!match) { continue; } const [, pidRaw, ppidRaw, rssRaw] = match; const pid = Number(pidRaw); const ppid = Number(ppidRaw); const rssBytes = parsePsRssBytes(rssRaw ?? ""); if (!Number.isInteger(pid) || !Number.isInteger(ppid) || rssBytes === null) { continue; } rssByPid.set(pid, rssBytes); const children = childrenByParent.get(ppid) ?? []; children.push(pid); childrenByParent.set(ppid, children); } if (!rssByPid.has(rootPid)) { return null; } return collectProcessTreeMetric(rootPid, childrenByParent, rssByPid); }