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>> {
130 if dir.exists() {
131 let mut delay_ms: u64 = 50;
132 let mut attempts = 0;
133 loop {
134 match fs::remove_dir_all(dir) {
135 Ok(()) => break,
136 Err(e) if is_not_empty_error(&e) && attempts < 5 => {
137 attempts += 1;
138 std::thread::sleep(Duration::from_millis(delay_ms));
139 delay_ms *= 2;
140 }
141 Err(e) => return Err(Box::new(e)),
142 }
143 }
144 }
145 create_dir_all(dir)?;
146 Ok(())
147}
148
149fn is_not_empty_error(e: &std::io::Error) -> bool {
150 matches!(e.raw_os_error(), Some(39) | Some(66))
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use tempfile::tempdir;
157
158 #[test]
159 fn hash_changes_with_content_and_markers_round_trip() {
160 let tmp = tempdir().unwrap();
161 let f = tmp.path().join("input");
162 fs::write(&f, b"alpha").unwrap();
163 let h1 = file_hash(&f).unwrap();
164 fs::write(&f, b"alphb").unwrap();
165 let h2 = file_hash(&f).unwrap();
166 assert_ne!(h1, h2);
167
168 let marker = tmp.path().join(".marker");
169 assert!(!marker_matches(&marker, &h2));
170 write_marker(&marker, &h2).unwrap();
171 assert!(marker_matches(&marker, &h2));
172 assert!(!marker_matches(&marker, &h1));
173 }
174
175 #[test]
176 fn clear_directory_empties_and_recreates() {
177 let tmp = tempdir().unwrap();
178 let d = tmp.path().join("d");
179 fs::create_dir_all(d.join("nested")).unwrap();
180 fs::write(d.join("nested/file"), b"x").unwrap();
181 assert!(dir_has_content(&d));
182 clear_directory(&d).unwrap();
183 assert!(d.exists());
184 assert!(!dir_has_content(&d));
185 }
186
187 #[test]
188 fn with_lock_runs_the_closure_and_releases() {
189 let tmp = tempdir().unwrap();
190 let lock = tmp.path().join(".lock");
191 let out = with_lock(&lock)(|| 42);
192 assert_eq!(out, 42);
193 assert!(!lock.exists(), "lock should be released");
194 }
195
196 #[test]
197 fn fresh_and_missing_locks_are_not_stale() {
198 let tmp = tempdir().unwrap();
199 let lock = tmp.path().join(".lock");
200 fs::write(&lock, "123\n").unwrap();
201 assert!(
202 !lock_is_stale(&lock),
203 "a just-created lock must not be considered stale"
204 );
205 assert!(
206 !lock_is_stale(&tmp.path().join("absent")),
207 "a missing lock reads as not stale (keep waiting)"
208 );
209 }
210}