"""CHV 宿主侧小文件的原子写(P2-18 收尾)。 此前 net.json / snapshots.json / pool registry / known_hosts 各自用固定名 `.tmp` + 直接 os.replace, 存在两个问题: 1. 固定 tmp 名:同一进程内两个写者(或一次崩溃残留的 .tmp)会互相覆盖/误读;不同 VM 的文件虽在不同目录, 但同一目录内的快照登记与 net 登记仍共用可预测的 tmp 名,崩溃后残留的 .tmp 会被下一次写静默覆盖, 丢了“上次写了一半”的信号。 2. 无 fsync:os.replace 只保证目录项原子,不保证内容 已落盘;宿主断电/崩溃后新文件可能回到旧内容或 0 字节,而调用方已认为登记成功。 本模块是唯一写路径:mkstemp 随机名(0600)→ 写 → flush + fsync 文件 → os.replace → fsync 父目录。 写失败时删临时文件再原样抛错,调用方按各自语义 处理(审计/拒绝/脏标记),本模块不吞错、不记日志。 """ import os import tempfile def atomic_write(path: str, data: bytes, mode: int = 0o600) -> None: """原子写 bytes 到 path(随机 tmp 名 + 双 fsync)。 mode 是最终文件的权限(默认 0600:这些文件都在 0700 的 state_dir 内、且含登记/密钥类内容,按敏感 文件对待;调用方要放宽时显式传)。 """ directory = os.path.dirname(path) or "." fd, tmp_path = tempfile.mkstemp(prefix=".tmp-", dir=directory) try: os.fchmod(fd, mode) with os.fdopen(fd, "wb") as fh: fh.write(data) fh.flush() os.fsync(fh.fileno()) os.replace(tmp_path, path) _fsync_dir(directory) except BaseException: try: os.unlink(tmp_path) except OSError: pass raise def atomic_write_text(path: str, text: str, mode: int = 0o600) -> None: """atomic_write 的 str 版(UTF-8 编码)。""" atomic_write(path, text.encode("utf-8"), mode=mode) def _fsync_dir(directory: str) -> None: """fsync 父目录,让 rename 本身落盘(best-effort: 某些文件系统不支持目录 fsync,失败即跳过——文件 内容的 fsync 已在 atomic_write 里做完,目录 fsync 丢了只影响“rename 是否在崩溃后可见”,不影响内容)。 """ try: dir_fd = os.open(directory, os.O_RDONLY) except OSError: return try: os.fsync(dir_fd) except OSError: pass finally: os.close(dir_fd)