use std::fs::{self, create_dir_all};
use std::io::{Read, Write};
use std::path::Path;
use std::time::{Duration, Instant};
pub fn with_lock<F: FnOnce() -> R, R>(lock_path: &Path) -> impl FnOnce(F) -> R {
let lock_path = lock_path.to_path_buf();
move |f: F| -> R {
if let Some(parent) = lock_path.parent() {
let _ = create_dir_all(parent);
}
let start = Instant::now();
let max_wait = Duration::from_secs(120);
loop {
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&lock_path)
{
Ok(mut file) => {
let _ = writeln!(file, "{}", std::process::id());
drop(file);
let result = f();
let _ = fs::remove_file(&lock_path);
return result;
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
if start.elapsed() > max_wait {
eprintln!(
"npm-utils: lock at {} held for {}s — assuming stale and continuing",
lock_path.display(),
start.elapsed().as_secs()
);
let _ = fs::remove_file(&lock_path);
continue;
}
std::thread::sleep(Duration::from_millis(200));
}
Err(e) => panic!(
"npm-utils: failed to acquire lock at {}: {}",
lock_path.display(),
e
),
}
}
}
}
pub fn dir_has_content(dir: &Path) -> bool {
if !dir.exists() {
return false;
}
match std::fs::read_dir(dir) {
Ok(mut entries) => entries.next().is_some(),
Err(_) => false,
}
}
pub fn file_hash(path: &Path) -> Result<String, Box<dyn std::error::Error>> {
let mut file = fs::File::open(path)?;
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
let mut hash: u64 = 0;
for (i, byte) in contents.iter().enumerate() {
hash = hash.wrapping_add((*byte as u64).wrapping_mul((i as u64).wrapping_add(1)));
}
Ok(format!("{:016x}", hash))
}
pub fn marker_matches(marker_path: &Path, expected_hash: &str) -> bool {
match fs::read_to_string(marker_path) {
Ok(content) => content.trim() == expected_hash,
Err(_) => false,
}
}
pub fn write_marker(marker_path: &Path, hash: &str) -> Result<(), Box<dyn std::error::Error>> {
let mut file = fs::File::create(marker_path)?;
file.write_all(hash.as_bytes())?;
Ok(())
}
pub fn clear_directory(dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
if dir.exists() {
let mut delay_ms: u64 = 50;
let mut attempts = 0;
loop {
match fs::remove_dir_all(dir) {
Ok(()) => break,
Err(e) if is_not_empty_error(&e) && attempts < 5 => {
attempts += 1;
std::thread::sleep(Duration::from_millis(delay_ms));
delay_ms *= 2;
}
Err(e) => return Err(Box::new(e)),
}
}
}
create_dir_all(dir)?;
Ok(())
}
fn is_not_empty_error(e: &std::io::Error) -> bool {
matches!(e.raw_os_error(), Some(39) | Some(66))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn hash_changes_with_content_and_markers_round_trip() {
let tmp = tempdir().unwrap();
let f = tmp.path().join("input");
fs::write(&f, b"alpha").unwrap();
let h1 = file_hash(&f).unwrap();
fs::write(&f, b"alphb").unwrap();
let h2 = file_hash(&f).unwrap();
assert_ne!(h1, h2);
let marker = tmp.path().join(".marker");
assert!(!marker_matches(&marker, &h2));
write_marker(&marker, &h2).unwrap();
assert!(marker_matches(&marker, &h2));
assert!(!marker_matches(&marker, &h1));
}
#[test]
fn clear_directory_empties_and_recreates() {
let tmp = tempdir().unwrap();
let d = tmp.path().join("d");
fs::create_dir_all(d.join("nested")).unwrap();
fs::write(d.join("nested/file"), b"x").unwrap();
assert!(dir_has_content(&d));
clear_directory(&d).unwrap();
assert!(d.exists());
assert!(!dir_has_content(&d));
}
#[test]
fn with_lock_runs_the_closure_and_releases() {
let tmp = tempdir().unwrap();
let lock = tmp.path().join(".lock");
let out = with_lock(&lock)(|| 42);
assert_eq!(out, 42);
assert!(!lock.exists(), "lock should be released");
}
}