Replies: 1 comment
|
感谢如此精确的根因报告 + 最小复现。我对照源码逐点验证(repo HEAD 4e84901 = 0.1.2-alpha.4 的 1. 你的判定成立: 2. 一个值得注意的事实:Windows 分支从来不依赖硬链接。 3. 源码注释明确反对 rename()(:580-582):"Publish via link()+unlink(), NOT rename(): link fails with EEXIST ... rename() would silently overwrite." 这带来一个必须写进修复的约束:降级触发列表必须显式排除 EEXIST。你的列表 [EPERM, ENOTSUP, ENOSYS] 已经正确排除了它,但值得在代码里注释强调——一个"catch-all 就 rename"的朴素实现会把并发保护变成静默覆盖,恰好在它要防的竞态上失效。另建议补 EOPNOTSUPP(macOS 的 errno 与 Linux 不同,网络卷/某些 FUSE 挂载在 macOS 上抛的是它)和 EACCES(部分 SMB/FUSE 策略对 link 拒绝但允许 rename);EXDEV 不可能出现(tmp 与 final 同目录),无需处理。 4. 降级后的语义(回应你"rename 是否等价"的论证):基本成立,但要精确表述——rename 方案丢失的只是 no-clobber 保证,不是原子性(同目录 rename 原子,且后续 5. 测试建议:无硬链接文件系统无法在常规 CI 里可移植复现——用注入式单测覆盖降级分支即可(stub fs.link 抛 EPERM,断言走 rename 且 finalPath 内容正确、tmp 被清理);EEXIST 分支必须仍走原 refuse 路径(补一个 EEXIST 不触发降级的断言,锁住第 3 点的约束)。 一个附带的观察:既然 Windows 的 MoveFileExW-no-clobber 方案跨 FAT/exFAT 都成立,若未来想统一两分支语义,"POSIX 无硬链接 FS 上表达 no-clobber rename"只能靠原生 renameat2 绑定(成本高,不值);当前能力降级方案是正确取舍。 |
Uh oh!
There was an error while loading. Please reload this page.
环境
设备/系统:HarmonyOS NEXT(OpenHarmony 内核)平板(arm64)
运行平台:process.platform === 'openharmony',Node.js v26.7.0
用户数据目录挂在 hmdfs(HarmonyOS 多设备融合文件系统,路径如 /storage/Users//...)
受影响版本:0.1.1-rc.2 与 0.1.2-alpha.5 均已核查——两版 dsh-session-persistence-jsonl 的 materializePosix() 仍是裸 link() 发布,问题依旧存在
现象
每一轮新会话创建时(会话日志首次落盘)直接失败,报错:
EPERM: operation not permitted, link
'.../.dsh/sessions///session.jsonl.zstd..tmp'
-> '.../.dsh/sessions///session.jsonl.zstd'
Copy
最小复现
const fs = require('fs');
fs.writeFileSync('/storage/Users//.dsh/a.txt', 'x');
fs.linkSync('/storage/Users//.dsh/a.txt', '/storage/Users//.dsh/b.txt');
// → EPERM: operation not permitted, link '.../a.txt' -> '.../b.txt'
fs.renameSync('/storage/Users//.dsh/a.txt', '/storage/Users//.dsh/c.txt'); // ✅ 正常
Copy
根因
dsh-session-persistence-jsonl 的 materializePosix() 用经典"写临时文件 → fs.link(tmp, final) → 删 tmp"做原子发布(writeSyncedTempFile 生成 ${finalPath}.${randomBytes(6).toString('hex')}.tmp,随后 link 硬链接到最终名)。
hmdfs 不支持硬链接:link(2) 一律返回 EPERM,而 rename(2) 正常。这不是配置或权限问题,是文件系统能力限制(hmdfs 为分布式/融合 FUSE 语义,未实现硬链接),用户侧无法绕开。
影响
任何新会话的首次 materialize 都失败 → 会话/本轮任务无法开始(我们这边的直接表现就是"本轮运行失败")。
在 HarmonyOS 上运行 dsh 属于全量阻断,无降级路径。
为什么用 link、能否直接换 rename
link+unlink 的意义是"目标已存在则拒绝(EEXIST)"的无覆盖原子发布。materializePosix 在发布前已调用 rejectExistingLog() 做了存在性检查(另有创建路径的 TOCTOU 背防),因此在已检查的前提下,rename 是语义等价的原子发布——同目录 rename 本身原子,覆盖窗口已被既有 guard 兜住。
建议修复(上游通用,不只对 HarmonyOS 有益)
// materializePosix(): 发布处
let linked = false;
try {
try {
await link(tmp, finalPath);
} catch (error) {
// 不支持硬链接的文件系统(hmdfs / FAT / exFAT / 部分网络与 FUSE 挂载)
// 会抛 EPERM / ENOTSUP / ENOSYS; rename 是等价的原子发布
// (目标已存在语义由 rejectExistingLog 承担)。
if (!(error instanceof Error) || !['EPERM', 'ENOTSUP', 'ENOSYS'].includes(error.code)) throw error;
await rename(tmp, finalPath);
}
linked = true;
} finally {
if (!linked) await rm(tmp, { force: true });
}
Copy
配套:把 rename 加进 node:fs/promises 的 import。Windows 分支已走 publishNewFileWin32,不受影响。
All reactions