1use std::fs::{self, create_dir_all};
5use std::io::{Read, Write};
6use std::path::Path;
7use std::time::{Duration, Instant};
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 let start = Instant::now();
25 let max_wait = Duration::from_secs(120);
26 loop {
27 match fs::OpenOptions::new()
28 .write(true)
29 .create_new(true)
30 .open(&lock_path)
31 {
32 Ok(mut file) => {
33 let _ = writeln!(file, "{}", std::process::id());
34 drop(file);
35 let result = f();
36 let _ = fs::remove_file(&lock_path);
37 return result;
38 }
39 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
40 if start.elapsed() > max_wait {
41 eprintln!(
42 "npm-utils: lock at {} held for {}s — assuming stale and continuing",
43 lock_path.display(),
44 start.elapsed().as_secs()
45 );
46 let _ = fs::remove_file(&lock_path);
47 continue;
48 }
49 std::thread::sleep(Duration::from_millis(200));
50 }
51 Err(e) => panic!(
52 "npm-utils: failed to acquire lock at {}: {}",
53 lock_path.display(),
54 e
55 ),
56 }
57 }
58 }
59}
60
61pub fn dir_has_content(dir: &Path) -> bool {
63 if !dir.exists() {
64 return false;
65 }
66 match std::fs::read_dir(dir) {
67 Ok(mut entries) => entries.next().is_some(),
68 Err(_) => false,
69 }
70}
71
72pub fn file_hash(path: &Path) -> Result<String, Box<dyn std::error::Error>> {
77 let mut file = fs::File::open(path)?;
78 let mut contents = Vec::new();
79 file.read_to_end(&mut contents)?;
80
81 let mut hash: u64 = 0;
82 for (i, byte) in contents.iter().enumerate() {
83 hash = hash.wrapping_add((*byte as u64).wrapping_mul((i as u64).wrapping_add(1)));
84 }
85 Ok(format!("{:016x}", hash))
86}
87
88pub fn marker_matches(marker_path: &Path, expected_hash: &str) -> bool {
90 match fs::read_to_string(marker_path) {
91 Ok(content) => content.trim() == expected_hash,
92 Err(_) => false,
93 }
94}
95
96pub fn write_marker(marker_path: &Path, hash: &str) -> Result<(), Box<dyn std::error::Error>> {
98 let mut file = fs::File::create(marker_path)?;
99 file.write_all(hash.as_bytes())?;
100 Ok(())
101}
102
103pub fn clear_directory(dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
109 if dir.exists() {
110 let mut delay_ms: u64 = 50;
111 let mut attempts = 0;
112 loop {
113 match fs::remove_dir_all(dir) {
114 Ok(()) => break,
115 Err(e) if is_not_empty_error(&e) && attempts < 5 => {
116 attempts += 1;
117 std::thread::sleep(Duration::from_millis(delay_ms));
118 delay_ms *= 2;
119 }
120 Err(e) => return Err(Box::new(e)),
121 }
122 }
123 }
124 create_dir_all(dir)?;
125 Ok(())
126}
127
128fn is_not_empty_error(e: &std::io::Error) -> bool {
129 matches!(e.raw_os_error(), Some(39) | Some(66))
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use tempfile::tempdir;
136
137 #[test]
138 fn hash_changes_with_content_and_markers_round_trip() {
139 let tmp = tempdir().unwrap();
140 let f = tmp.path().join("input");
141 fs::write(&f, b"alpha").unwrap();
142 let h1 = file_hash(&f).unwrap();
143 fs::write(&f, b"alphb").unwrap();
144 let h2 = file_hash(&f).unwrap();
145 assert_ne!(h1, h2);
146
147 let marker = tmp.path().join(".marker");
148 assert!(!marker_matches(&marker, &h2));
149 write_marker(&marker, &h2).unwrap();
150 assert!(marker_matches(&marker, &h2));
151 assert!(!marker_matches(&marker, &h1));
152 }
153
154 #[test]
155 fn clear_directory_empties_and_recreates() {
156 let tmp = tempdir().unwrap();
157 let d = tmp.path().join("d");
158 fs::create_dir_all(d.join("nested")).unwrap();
159 fs::write(d.join("nested/file"), b"x").unwrap();
160 assert!(dir_has_content(&d));
161 clear_directory(&d).unwrap();
162 assert!(d.exists());
163 assert!(!dir_has_content(&d));
164 }
165
166 #[test]
167 fn with_lock_runs_the_closure_and_releases() {
168 let tmp = tempdir().unwrap();
169 let lock = tmp.path().join(".lock");
170 let out = with_lock(&lock)(|| 42);
171 assert_eq!(out, 42);
172 assert!(!lock.exists(), "lock should be released");
173 }
174}