Files
UHD_Media_Tool/packages/adapters/src/fs
rootandClaude Opus 5.5 7daf544bd9 fix(fs): fsworker always reports phase completion progress
The throttle could drop the final copy heartbeat when it landed inside the
interval (fsproxy.test saw 6291456 instead of 6291461 under ci load). Phase
completion (bytes >= total) is now sent immediately like phase changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 00:56:31 +08:00
..

fs

本地文件操作:copyWithVerify(同目录隐藏临时文件 → fsync → size/sha256/抽样校验 → 提交 → fsync 目录,保留 mtime,不复制权限/属主,默认不建父目录;目标已存在按 onExisting:conflict 抛 EEXIST / skip 不写 / replace 旧文件硬链接留底 → rename 原子替换 → 留底进回收目录或删除,不裸覆盖;写前顺带清本目录 >24h 的 .uhd-partial)、moveSafe(同设备 no-clobber 移动,EXDEV 返回 exdev 不降级)、crossDeviceMove(copy → verify → delete_source,返回步骤日志,可重入,删源失败为 attention;copy 已做 sha256 时 verify 不再重复全量哈希)、statSafe/checkRoot(先查库根哨兵 .uhd-root,区分 missing / permission_denied / root_offline)、scanIncremental、ffprobe、diskUsage、sweepStalePartials、filesMatchSha256(读失败/超时抛 FsError,不返回 match=false 冒充"内容不同")。

挂死隔离:FsProxy(对标 MP fsproxy)

new FsProxy({ hangTimeoutMs, quickTimeoutMs, progressIntervalMs, killGraceMs, mode }) 把单个文件操作(copy / crossMove / move / match / sha256 / stat / checkRoot / sweep / unlink)放进一次性执行子进程 fsworker.ts(deno run --no-config -A,约 50 ms 启动),stdin 一条 JSON 请求,stdout 逐行心跳 + 结果。

  • 数据类操作(复制、跨卷搬运、哈希)按进度停滞判挂死:hangTimeoutMs(缺省 120 s)内字节数与阶段都没前进 → SIGTERM(worker 中止并清掉自己的 partial)→ killGraceMs 后 SIGKILL → FsError('TIMEOUT')。大文件复制几小时不会误杀。
  • 快操作按总时长 quickTimeoutMs(缺省 30 s)。statSafe 超时返回 {kind:"error",code:"TIMEOUT"},checkRoot 超时返回 offline(timeout)——挂死不等于文件缺失。
  • 卡在 D 状态的子进程被 unref,不拖住本进程;被 SIGKILL 的 partial 由下次写前清理收走。
  • 子进程里 sha256 走流式(带心跳),不走 sha256sum(看不到进度);吞吐见 docs/porting/moviepilot-rust-transfer.md 基准记录。
  • mode: "auto":Deno.execPath() 是 deno CLI → deno run -A fsworker.ts;否则视为 deno compile 出的 umt-server 二进制 → <execPath> --role fsworker(apps/server/src/main.ts 在加载其余模块前分派,启动约 150 ms)。编译产物的 --allow-run 必须含自身绝对路径(镜像内 /app/umt-server,见 apps/server/deno.json);起不来(NotCapable/ENOENT)自动回退 in-process(同样的看门狗,超时立即报 TIMEOUT 并中止,但卡死系统调用占住的线程无法回收),stats().fallbackReason 记原因。stats().workerKind = script / self / custom。
  • 容器验收开关:worker 进程设 UMT_FS_TEST_HANG_AFTER_BYTES=<n> 后,每个数据类操作的 worker 读到 n 字节即卡死 JS 线程并屏蔽 SIGTERM,用来在生产二进制里验证 停滞超时 → SIGKILL → TIMEOUT;生产勿设。
  • 测试开关 _test: { hangAfterBytes, ignoreTerm }:worker 在累计进度达到阈值后阻塞 JS 线程,模拟卡死的系统调用。

错误码处置类别见 fsErrorClass:IO/TIMEOUT/ROOT_OFFLINE/SOURCE_CHANGED 可退避重试;EACCES/ENOSPC/EXDEV 进待处理;EEXIST/VERIFY_FAILED/ENOENT 需人工。

在本机临时目录与 /dev/shm(跨设备)上做了集成测试(含 FIFO 真实阻塞);权限拒绝类用例在 root 下跳过。未在真实 NAS/FUSE/网络挂载上验证。