feat(server,fs): W5-E production fsworker role, settings auto-investigate mode, replace old file to trash_item
- main.ts: `--role fsworker` dispatched before the rest of the module graph is loaded (other imports became dynamic); runs fsWorkerMain from the new `@umt/adapters/fsworker` export (~150 ms startup in the compiled binary) - fsproxy: default worker command picked per runtime (deno CLI -> `deno run fsworker.ts`, compiled binary -> `<execPath> --role fsworker`); stats expose workerKind; UMT_FS_TEST_HANG_AFTER_BYTES acceptance switch for containers - compile: --allow-run adds /app/umt-server (spawn self); start task allows deno - organize: replaced old file moved into the file-manager trash layout (<root>/.umt-trash/<day>/<uuid>/<rel>) and registered in trash_item, ledger result follows; organize.transfer log carries fsproxy stats - settings: autoInvestigateMode (investigate/recover) + max recover attempts - deploy.md: container verification (subprocess self, TIMEOUT -> SIGKILL x3) Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
fc49c58359
commit
3641ab9573
12 files changed
+248
-48
No files matched your search
@@ -3,7 +3,15 @@ import { existsSync } from "node:fs";
|
||||
import { readdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { createTempTree, type TempTree } from "@umt/testkit";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { FsError, type FsProgress, FsProxy, fsErrorClass, sweepStalePartials, toFsError } from "./index.ts";
|
||||
import {
|
||||
defaultWorkerCommand,
|
||||
FsError,
|
||||
type FsProgress,
|
||||
FsProxy,
|
||||
fsErrorClass,
|
||||
sweepStalePartials,
|
||||
toFsError,
|
||||
} from "./index.ts";
|
||||
|
||||
// fsproxy:单个文件操作在执行子进程里跑,按进度心跳判挂死 → SIGTERM → SIGKILL → FsError('TIMEOUT')。
|
||||
|
||||
@@ -122,6 +130,38 @@ describe("FsProxy(子进程)", () => {
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
describe("FsProxy 启动方式", () => {
|
||||
it("deno CLI 下缺省 run fsworker.ts;显式 workerCommand 记为 custom", () => {
|
||||
const auto = defaultWorkerCommand();
|
||||
expect(auto?.kind).toBe("script");
|
||||
expect(auto?.args.at(-1)).toMatch(/fsworker\.ts$/);
|
||||
expect(new FsProxy().stats()).toMatchObject({ mode: "subprocess", workerKind: "script" });
|
||||
const px = new FsProxy({ workerCommand: { cmd: "/app/umt-server", args: ["--role", "fsworker"] } });
|
||||
expect(px.stats()).toMatchObject({ mode: "subprocess", workerKind: "custom" });
|
||||
});
|
||||
|
||||
it("起不来的 worker 命令(auto)回退 in-process 并记原因", async () => {
|
||||
const t = await tree({ "a.mkv": "x" });
|
||||
const px = new FsProxy({ workerCommand: { cmd: "/nonexistent/umt-server", args: ["--role", "fsworker"] } });
|
||||
expect(await px.statSafe(t.path("a.mkv"))).toMatchObject({ kind: "ok" });
|
||||
expect(px.stats()).toMatchObject({ mode: "inprocess", fallbackReason: expect.stringMatching(/^spawn_failed/) });
|
||||
});
|
||||
|
||||
it("UMT_FS_TEST_HANG_AFTER_BYTES(容器验收开关)→ 数据类操作卡死 → SIGKILL → TIMEOUT;快操作不受影响", async () => {
|
||||
const t = await tree({ "src/a.mkv": 3 * MiB, "dst/": null });
|
||||
const px = new FsProxy({ mode: "subprocess", ...fast });
|
||||
Deno.env.set("UMT_FS_TEST_HANG_AFTER_BYTES", String(MiB));
|
||||
try {
|
||||
expect(await px.statSafe(t.path("src/a.mkv"))).toMatchObject({ kind: "ok" });
|
||||
const e = await px.copy(t.path("src/a.mkv"), t.path("dst/a.mkv")).catch((x) => x);
|
||||
expect(e.code).toBe("TIMEOUT");
|
||||
expect(px.stats()).toMatchObject({ timeouts: 1, killed: 1 });
|
||||
} finally {
|
||||
Deno.env.delete("UMT_FS_TEST_HANG_AFTER_BYTES");
|
||||
}
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
describe("FsProxy(in-process 回退)", () => {
|
||||
it("正常复制有进度;FIFO 挂死 → TIMEOUT(看门狗),不等卡住的调用返回", async () => {
|
||||
const t = await tree({ "src/a.mkv": 3 * MiB, "dst/": null });
|
||||
|
||||
Reference in new issue
Block a user