write-then-rename 原子写:先写到同目录的临时文件再 os.replace 重命名。 保证别的进程读 target 时只能看到完整旧内容或完整新内容,不会读到半截。 Windows 兼容:当目标文件被另一进程持 read handle(如 web server 正在 readFile) 时,os.replace 会抛 PermissionError [WinError 5]。重试 3 次(50ms 间隔)。 最终失败时**保留临时文件**让用户可手动 mv 收拾,而不是删了重写——避免数据丢失。
(target: Path, content: str, encoding: str = "utf-8")
| 25 | |
| 26 | |
| 27 | def _atomic_write_text(target: Path, content: str, encoding: str = "utf-8") -> None: |
| 28 | """write-then-rename 原子写:先写到同目录的临时文件再 os.replace 重命名。 |
| 29 | 保证别的进程读 target 时只能看到完整旧内容或完整新内容,不会读到半截。 |
| 30 | |
| 31 | Windows 兼容:当目标文件被另一进程持 read handle(如 web server 正在 readFile) |
| 32 | 时,os.replace 会抛 PermissionError [WinError 5]。重试 3 次(50ms 间隔)。 |
| 33 | 最终失败时**保留临时文件**让用户可手动 mv 收拾,而不是删了重写——避免数据丢失。 |
| 34 | """ |
| 35 | import time as _time |
| 36 | |
| 37 | target.parent.mkdir(parents=True, exist_ok=True) |
| 38 | tmp_fd, tmp_path = tempfile.mkstemp( |
| 39 | dir=str(target.parent), |
| 40 | prefix=f".{target.name}.", |
| 41 | suffix=".tmp", |
| 42 | ) |
| 43 | try: |
| 44 | with os.fdopen(tmp_fd, "w", encoding=encoding, newline="") as f: |
| 45 | f.write(content) |
| 46 | except Exception: |
| 47 | # 写 tmp 都失败 → 直接清理 + 抛 |
| 48 | try: |
| 49 | os.unlink(tmp_path) |
| 50 | except OSError: |
| 51 | pass |
| 52 | raise |
| 53 | |
| 54 | # rename 阶段:Windows 锁竞争重试 |
| 55 | last_exc: Exception | None = None |
| 56 | for attempt in range(3): |
| 57 | try: |
| 58 | os.replace(tmp_path, str(target)) |
| 59 | return |
| 60 | except PermissionError as e: |
| 61 | last_exc = e |
| 62 | _time.sleep(0.05) |
| 63 | # 3 次都失败:清理 tmp(避免污染 git status),给 stderr 明确提示让用户重试 |
| 64 | try: |
| 65 | os.unlink(tmp_path) |
| 66 | except OSError: |
| 67 | # 实在删不掉 → 提示用户清理路径 |
| 68 | print( |
| 69 | f"[_atomic_write_text] 警告:临时文件无法清理:{tmp_path} " |
| 70 | f"(可能仍被进程持有;请重启相关进程后手动 rm)", |
| 71 | file=sys.stderr, |
| 72 | ) |
| 73 | print( |
| 74 | f"[_atomic_write_text] os.replace 失败 3 次(目标 {target} 可能被另一进程持锁);" |
| 75 | f"请稍后重试,或重启占用该文件的进程", |
| 76 | file=sys.stderr, |
| 77 | ) |
| 78 | if last_exc is not None: |
| 79 | raise last_exc |
| 80 | |
| 81 | # 让 Windows 终端也能正确输出中文 |
| 82 | if sys.platform == "win32": |