1use std::path::Path;
19
20use anyhow::{Context, Result};
21pub fn write_file_atomic(path: &Path, content: &str) -> Result<()> {
28 if let Some(parent) = path.parent() {
29 std::fs::create_dir_all(parent)
30 .with_context(|| format!("创建目录失败: {}", parent.display()))?;
31 }
32 let tmp = path.with_extension("tmp");
33 std::fs::write(&tmp, content)
34 .with_context(|| format!("写入临时文件失败: {}", tmp.display()))?;
35 let file = std::fs::OpenOptions::new()
40 .write(true)
41 .open(&tmp)
42 .with_context(|| format!("打开临时文件刷新失败: {}", tmp.display()))?;
43 file.sync_all()
44 .with_context(|| format!("刷新临时文件失败: {}", tmp.display()))?;
45 std::fs::rename(&tmp, path)
46 .with_context(|| format!("原子替换失败: {} -> {}", tmp.display(), path.display()))?;
47 Ok(())
48}
49
50#[derive(Debug)]
63pub struct RunLock {
64 path: std::path::PathBuf,
65}
66
67impl Drop for RunLock {
68 fn drop(&mut self) {
69 let _ = std::fs::remove_file(&self.path);
72 }
73}
74
75pub fn acquire_run_lock(config: &crate::config::schema::WikiConfig) -> Result<RunLock> {
78 let state_dir = config.output_dir().join(".state");
79 std::fs::create_dir_all(&state_dir)
80 .with_context(|| format!("创建状态目录失败: {}", state_dir.display()))?;
81 let path = state_dir.join("run.lock");
82 match std::fs::OpenOptions::new().write(true).create_new(true).open(&path) {
83 Ok(mut f) => {
84 use std::io::Write;
85 if let Err(e) = writeln!(f, "{}", std::process::id()) {
87 eprintln!("code-repo-wiki: 运行锁 PID 写入失败(不影响锁): {e}");
88 }
89 Ok(RunLock { path })
90 }
91 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => anyhow::bail!(
92 "另一 code-repo-wiki 实例正在运行(锁文件: {})。确认无残留实例后可删除该文件重试",
93 path.display()
94 ),
95 Err(e) => Err(e).with_context(|| format!("获取运行锁失败: {}", path.display())),
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 fn temp_path(tag: &str, name: &str) -> std::path::PathBuf {
104 let dir = std::env::temp_dir().join(format!("code_repo_wiki_fs_{}_{}", tag, std::process::id()));
105 let _ = std::fs::remove_dir_all(&dir);
106 std::fs::create_dir_all(&dir).unwrap();
107 dir.join(name)
108 }
109
110 fn lock_config(dir: &std::path::Path) -> crate::config::schema::WikiConfig {
111 crate::config::schema::WikiConfig {
112 output_dir: Some(dir.to_path_buf()),
113 ..Default::default()
114 }
115 }
116
117 #[test]
119 fn test_run_lock_acquire_and_release() {
120 let dir = temp_path("lock_roundtrip", "");
121 let config = lock_config(&dir);
122 let lock = acquire_run_lock(&config).unwrap();
123 assert!(dir.join(".state/run.lock").exists());
124 drop(lock);
125 assert!(!dir.join(".state/run.lock").exists(), "Drop 应释放锁");
126 let lock2 = acquire_run_lock(&config).unwrap();
128 drop(lock2);
129 let _ = std::fs::remove_dir_all(&dir);
130 }
131
132 #[test]
134 fn test_run_lock_rejects_second() {
135 let dir = temp_path("lock_reject", "");
136 let config = lock_config(&dir);
137 let _lock = acquire_run_lock(&config).unwrap();
138 let err = acquire_run_lock(&config).unwrap_err();
139 let msg = err.to_string();
140 assert!(msg.contains("正在运行"), "应报并发错误: {msg}");
141 assert!(msg.contains("run.lock"), "报错应含锁路径: {msg}");
142 let _ = std::fs::remove_dir_all(&dir);
143 }
144
145 #[test]
147 fn test_write_new_file() {
148 let path = temp_path("new", "a.json");
149 write_file_atomic(&path, "{\"v\":1}").unwrap();
150 assert_eq!(std::fs::read_to_string(&path).unwrap(), "{\"v\":1}");
151 assert!(!path.with_extension("tmp").exists(), "rename 后不应残留临时文件");
153 let _ = std::fs::remove_dir_all(path.parent().unwrap());
154 }
155
156 #[test]
158 fn test_overwrite_existing() {
159 let path = temp_path("overwrite", "b.json");
160 write_file_atomic(&path, "old").unwrap();
161 write_file_atomic(&path, "new").unwrap();
162 assert_eq!(std::fs::read_to_string(&path).unwrap(), "new");
163 let _ = std::fs::remove_dir_all(path.parent().unwrap());
164 }
165
166 #[test]
168 fn test_creates_parent_dir() {
169 let dir = std::env::temp_dir().join(format!("code_repo_wiki_fs_nested_{}", std::process::id()));
170 let _ = std::fs::remove_dir_all(&dir);
171 let path = dir.join("deep").join("nested").join("c.json");
172 write_file_atomic(&path, "x").unwrap();
173 assert!(path.exists());
174 let _ = std::fs::remove_dir_all(&dir);
175 }
176}
177