1use std::fs::{self, create_dir_all};
5use std::io::{Read, Write};
6use std::path::Path;
7use std::time::Duration;
8
9pub fn with_lock<F: FnOnce() -> R, R>(lock_path: &Path) -> impl FnOnce(F) -> R {
19 let lock_path = lock_path.to_path_buf();
20 move |f: F| -> R {
21 if let Some(parent) = lock_path.parent() {
22 let _ = create_dir_all(parent);
23 }
24 loop {
25 match fs::OpenOptions::new()
26 .write(true)
27 .create_new(true)
28 .open(&lock_path)
29 {
30 Ok(mut file) => {
31 let _ = writeln!(file, "{}", std::process::id());
32 drop(file);
33 let result = f();
34 let _ = fs::remove_file(&lock_path);
35 return result;
36 }
37 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
38 if lock_is_stale(&lock_path) {
39 crate::warn::warn(&format!(
40 "lock at {} looks stale (file older than {}s) — \
41 removing and continuing",
42 lock_path.display(),
43 STALE_LOCK.as_secs()
44 ));
45 let _ = fs::remove_file(&lock_path);
46 continue;
47 }
48 std::thread::sleep(Duration::from_millis(200));
49 }
50 Err(e) => panic!(
51 "npm-utils: failed to acquire lock at {}: {}",
52 lock_path.display(),
53 e
54 ),
55 }
56 }
57 }
58}
59
60const STALE_LOCK: Duration = Duration::from_secs(600);
67
68fn lock_is_stale(lock_path: &Path) -> bool {
72 fs::metadata(lock_path)
73 .and_then(|m| m.modified())
74 .ok()
75 .and_then(|t| t.elapsed().ok())
76 .is_some_and(|age| age > STALE_LOCK)
77}
78
79pub fn dir_has_content(dir: &Path) -> bool {
81 if !dir.exists() {
82 return false;
83 }
84 match std::fs::read_dir(dir) {
85 Ok(mut entries) => entries.next().is_some(),
86 Err(_) => false,
87 }
88}
89
90pub fn file_hash(path: &Path) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
95 let mut file = fs::File::open(path)?;
96 let mut contents = Vec::new();
97 file.read_to_end(&mut contents)?;
98
99 let mut hash: u64 = 0;
100 for (i, byte) in contents.iter().enumerate() {
101 hash = hash.wrapping_add((*byte as u64).wrapping_mul((i as u64).wrapping_add(1)));
102 }
103 Ok(format!("{:016x}", hash))
104}
105
106pub fn marker_matches(marker_path: &Path, expected_hash: &str) -> bool {
108 match fs::read_to_string(marker_path) {
109 Ok(content) => content.trim() == expected_hash,
110 Err(_) => false,
111 }
112}
113
114pub fn write_marker(
116 marker_path: &Path,
117 hash: &str,
118) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
119 let mut file = fs::File::create(marker_path)?;
120 file.write_all(hash.as_bytes())?;
121 Ok(())
122}
123
124pub fn clear_directory(dir: &Path) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
134 if fs::symlink_metadata(dir)
135 .map(|meta| meta.file_type().is_symlink())
136 .unwrap_or(false)
137 {
138 fs::remove_file(dir)?;
139 }
140 if dir.exists() {
141 let mut delay_ms: u64 = 50;
142 let mut attempts = 0;
143 loop {
144 match fs::remove_dir_all(dir) {
145 Ok(()) => break,
146 Err(e) if is_not_empty_error(&e) && attempts < 5 => {
147 attempts += 1;
148 std::thread::sleep(Duration::from_millis(delay_ms));
149 delay_ms *= 2;
150 }
151 Err(e) => return Err(Box::new(e)),
152 }
153 }
154 }
155 create_dir_all(dir)?;
156 Ok(())
157}
158
159fn is_not_empty_error(e: &std::io::Error) -> bool {
160 matches!(e.raw_os_error(), Some(39) | Some(66))
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use tempfile::tempdir;
167
168 #[test]
169 fn hash_changes_with_content_and_markers_round_trip() {
170 let tmp = tempdir().unwrap();
171 let f = tmp.path().join("input");
172 fs::write(&f, b"alpha").unwrap();
173 let h1 = file_hash(&f).unwrap();
174 fs::write(&f, b"alphb").unwrap();
175 let h2 = file_hash(&f).unwrap();
176 assert_ne!(h1, h2);
177
178 let marker = tmp.path().join(".marker");
179 assert!(!marker_matches(&marker, &h2));
180 write_marker(&marker, &h2).unwrap();
181 assert!(marker_matches(&marker, &h2));
182 assert!(!marker_matches(&marker, &h1));
183 }
184
185 #[test]
186 fn clear_directory_empties_and_recreates() {
187 let tmp = tempdir().unwrap();
188 let d = tmp.path().join("d");
189 fs::create_dir_all(d.join("nested")).unwrap();
190 fs::write(d.join("nested/file"), b"x").unwrap();
191 assert!(dir_has_content(&d));
192 clear_directory(&d).unwrap();
193 assert!(d.exists());
194 assert!(!dir_has_content(&d));
195 }
196
197 #[cfg(unix)]
198 #[test]
199 fn clear_directory_unlinks_a_dangling_symlink_instead_of_following_it() {
200 use std::os::unix::fs::symlink;
201 let tmp = tempdir().unwrap();
202 let target = tmp.path().join("outside");
205 let link = tmp.path().join("dest");
206 symlink(&target, &link).unwrap();
207
208 clear_directory(&link).unwrap();
209 assert!(
210 !target.exists(),
211 "the link's target directory must not be created"
212 );
213 assert!(link.is_dir() && !link.symlink_metadata().unwrap().file_type().is_symlink());
214 }
215
216 #[cfg(unix)]
217 #[test]
218 fn clear_directory_replaces_a_live_symlink_with_a_real_dir() {
219 use std::os::unix::fs::symlink;
220 let tmp = tempdir().unwrap();
221 let target = tmp.path().join("real");
222 fs::create_dir_all(&target).unwrap();
223 fs::write(target.join("keep"), b"x").unwrap();
224 let link = tmp.path().join("dest");
225 symlink(&target, &link).unwrap();
226
227 clear_directory(&link).unwrap();
228 assert!(link.is_dir() && !link.symlink_metadata().unwrap().file_type().is_symlink());
229 assert!(
230 target.join("keep").exists(),
231 "the link's target is left untouched"
232 );
233 }
234
235 #[test]
236 fn with_lock_runs_the_closure_and_releases() {
237 let tmp = tempdir().unwrap();
238 let lock = tmp.path().join(".lock");
239 let out = with_lock(&lock)(|| 42);
240 assert_eq!(out, 42);
241 assert!(!lock.exists(), "lock should be released");
242 }
243
244 #[test]
245 fn fresh_and_missing_locks_are_not_stale() {
246 let tmp = tempdir().unwrap();
247 let lock = tmp.path().join(".lock");
248 fs::write(&lock, "123\n").unwrap();
249 assert!(
250 !lock_is_stale(&lock),
251 "a just-created lock must not be considered stale"
252 );
253 assert!(
254 !lock_is_stale(&tmp.path().join("absent")),
255 "a missing lock reads as not stale (keep waiting)"
256 );
257 }
258}