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