fix(system): 编译产物读 /proc、/sys 走 cat 兜底,运行状态资源监控与沙箱内存看门狗恢复
Deno 2 不带 --allow-all 时读 /proc、/sys 一律 NotCapable,部署环境资源监控一直无数据。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
708e99746c
commit
9268a4d920
9 files changed
+84
-19
No files matched your search
@@ -4,8 +4,8 @@
|
||||
"exports": { ".": "./src/main.ts" },
|
||||
"tasks": {
|
||||
"dev": "deno run -A --watch --env-file=../../.env src/main.ts",
|
||||
"start": "deno run --allow-net --allow-env --allow-sys --allow-read --allow-write --allow-run=ffprobe,sha256sum,deno --env-file=../../.env src/main.ts",
|
||||
"compile": "deno compile --frozen --node-modules-dir=none --exclude-unused-npm --allow-net --allow-env --allow-sys --allow-read --allow-write --allow-run=ffprobe,sha256sum,/app/umt-server --include ../../packages/db/migrations --include ../../packages/agent/skills --output ../../dist/umt-server src/main.ts",
|
||||
"start": "deno run --allow-net --allow-env --allow-sys --allow-read --allow-write --allow-run=ffprobe,sha256sum,cat,deno --env-file=../../.env src/main.ts",
|
||||
"compile": "deno compile --frozen --node-modules-dir=none --exclude-unused-npm --allow-net --allow-env --allow-sys --allow-read --allow-write --allow-run=ffprobe,sha256sum,cat,/app/umt-server --include ../../packages/db/migrations --include ../../packages/agent/skills --output ../../dist/umt-server src/main.ts",
|
||||
"test": "deno run -A npm:vitest run",
|
||||
"typecheck": "deno run -A npm:typescript/tsc --noEmit -p tsconfig.json"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { availableParallelism, cpus } from "node:os";
|
||||
import process from "node:process";
|
||||
import type { ResourceSample, ResourceScope, SystemResources } from "@umt/contracts";
|
||||
import { readProcText } from "../../proc-read.ts";
|
||||
|
||||
// 资源监控(界面第二轮 V 线,参考 MP 仪表盘 CPU / 内存 / 网络组件):api 进程每 5 秒读一次 /proc 与 cgroup v2,
|
||||
// 相邻两次读数的差算速率,采样只放内存(环形,保留 6 小时)。读不到的文件按 null 降级,不报错。
|
||||
@@ -14,13 +15,8 @@ const MAX_POINTS = 360;
|
||||
|
||||
export type ReadText = (path: string) => Promise<string | null>;
|
||||
|
||||
export const defaultReadText: ReadText = async (path) => {
|
||||
try {
|
||||
return await Deno.readTextFile(path);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
/** /proc 与 /sys 需特权读,编译产物走 cat 兜底(见 proc-read.ts) */
|
||||
export const defaultReadText: ReadText = readProcText;
|
||||
|
||||
// ── 解析(纯函数) ──
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// 读 /proc 与 /sys 的伪文件。
|
||||
// Deno 2 把这两处当特权路径:不带 --allow-all 时 Deno.readTextFile 一律 NotCapable(--allow-read 及按路径授权都不行)。
|
||||
// 开发 / 测试用 -A 能直接读;编译产物按最小权限运行,只能借 `cat` 子进程读(编译与 start 任务的 --allow-run 已含 cat)。
|
||||
// 第一次遇到 NotCapable 后记住,后续直接走 cat。
|
||||
|
||||
let viaCat = false;
|
||||
|
||||
async function catText(path: string): Promise<string | null> {
|
||||
try {
|
||||
const o = await new Deno.Command("cat", { args: [path], stdout: "piped", stderr: "null" }).output();
|
||||
return o.success ? new TextDecoder().decode(o.stdout) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 读不到(不存在、无权限、cat 不可用)返回 null */
|
||||
export async function readProcText(path: string): Promise<string | null> {
|
||||
if (viaCat) return await catText(path);
|
||||
try {
|
||||
return await Deno.readTextFile(path);
|
||||
} catch (e) {
|
||||
if (e instanceof Deno.errors.NotCapable) {
|
||||
viaCat = true;
|
||||
return await catText(path);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
// 起不来(权限 / 运行时不支持)时退回进程内执行并记日志:仍有超时,但卡死的同步代码无法收回(只在本机调试时出现)。
|
||||
import { basename } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { readProcText } from "../proc-read.ts";
|
||||
import { prepareJobs, SANDBOX_JOBS, type SandboxInput, type SandboxJobName, type SandboxOutput } from "./jobs.ts";
|
||||
|
||||
export type SandboxErrorKind = "timeout" | "memory" | "crash" | "job" | "spawn";
|
||||
@@ -52,13 +53,9 @@ function command(memoryMb: number): { cmd: string; args: string[] } {
|
||||
|
||||
/** 子进程实际内存(KB);读不到返回 null */
|
||||
async function rssKb(pid: number): Promise<number | null> {
|
||||
try {
|
||||
const s = await Deno.readTextFile(`/proc/${pid}/status`);
|
||||
const m = /VmRSS:\s+(\d+)\s+kB/.exec(s);
|
||||
return m ? Number(m[1]) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const s = await readProcText(`/proc/${pid}/status`);
|
||||
const m = s ? /VmRSS:\s+(\d+)\s+kB/.exec(s) : null;
|
||||
return m ? Number(m[1]) : null;
|
||||
}
|
||||
|
||||
export async function runSandbox<J extends SandboxJobName>(
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// 生产权限下读 /proc、/sys:Deno 2 不带 --allow-all 时 readTextFile 一律 NotCapable,
|
||||
// 编译产物的资源监控与沙箱内存看门狗曾因此静默取不到数(v0.3.34 运行状态页一直无数据)。
|
||||
// 这里用与 apps/server/deno.json compile 相同的权限起子进程,确认走 cat 兜底能读到。
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const here = fileURLToPath(new URL(".", import.meta.url));
|
||||
const probe = `
|
||||
import { readProcText } from "${here}../../src/proc-read.ts";
|
||||
import { readResources } from "${here}../../src/modules/system/resources.ts";
|
||||
const stat = await readProcText("/proc/stat");
|
||||
const r = await readResources();
|
||||
console.log(JSON.stringify({ stat: !!stat && stat.startsWith("cpu"), host: r.host != null, mem: r.meminfo != null }));
|
||||
`;
|
||||
|
||||
describe.skipIf(Deno.build.os !== "linux")("readProcText 在生产权限下", () => {
|
||||
it("走 cat 兜底读到 /proc 与资源读数", async () => {
|
||||
const out = await new Deno.Command(Deno.execPath(), {
|
||||
args: [
|
||||
"run",
|
||||
"--no-prompt",
|
||||
"--allow-net",
|
||||
"--allow-env",
|
||||
"--allow-sys",
|
||||
"--allow-read",
|
||||
"--allow-write",
|
||||
"--allow-run=ffprobe,sha256sum,cat",
|
||||
"-",
|
||||
],
|
||||
stdin: "piped",
|
||||
stdout: "piped",
|
||||
stderr: "piped",
|
||||
}).spawn();
|
||||
const w = out.stdin.getWriter();
|
||||
await w.write(new TextEncoder().encode(probe));
|
||||
await w.close();
|
||||
const o = await out.output();
|
||||
const text = new TextDecoder().decode(o.stdout).trim();
|
||||
expect(o.success, new TextDecoder().decode(o.stderr)).toBe(true);
|
||||
expect(JSON.parse(text.split("\n").at(-1)!)).toEqual({ stat: true, host: true, mem: true });
|
||||
}, 60_000);
|
||||
});
|
||||
Reference in new issue
Block a user