use std::path::{Path, PathBuf};
use parking_lot::{ReentrantMutex, ReentrantMutexGuard};
use rskit_errors::{AppError, AppResult};
static CWD_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
#[derive(Debug)]
#[must_use = "the working directory is restored when the guard is dropped; bind it to a name"]
pub struct CurrentDirGuard {
previous: PathBuf,
_lock: ReentrantMutexGuard<'static, ()>,
}
impl CurrentDirGuard {
pub fn pin() -> AppResult<Self> {
let lock = CWD_LOCK.lock();
let previous = current_dir()?;
Ok(Self {
previous,
_lock: lock,
})
}
pub fn change_to(dir: impl AsRef<Path>) -> AppResult<Self> {
let lock = CWD_LOCK.lock();
let previous = current_dir()?;
set_current_dir(dir.as_ref())?;
Ok(Self {
previous,
_lock: lock,
})
}
#[must_use]
pub fn previous(&self) -> &Path {
&self.previous
}
}
impl Drop for CurrentDirGuard {
fn drop(&mut self) {
if let Err(error) = std::env::set_current_dir(&self.previous) {
assert!(
std::thread::panicking(),
"CurrentDirGuard failed to restore the working directory to '{}': {error}",
self.previous.display()
);
}
}
}
fn current_dir() -> AppResult<PathBuf> {
std::env::current_dir()
.map_err(|error| AppError::from(error).context("failed to read current directory"))
}
fn set_current_dir(dir: &Path) -> AppResult<()> {
std::env::set_current_dir(dir).map_err(|error| {
AppError::from(error).context(format!(
"failed to set current directory to '{}'",
dir.display()
))
})
}
#[cfg(test)]
mod tests {
use super::CurrentDirGuard;
use crate::TestWorkspace;
#[test]
fn change_to_switches_and_restores_on_drop() {
let before = std::env::current_dir().unwrap();
let workspace = TestWorkspace::new("cwd-guard");
let target = workspace.path().canonicalize().unwrap();
{
let guard = CurrentDirGuard::change_to(&target).unwrap();
assert_eq!(std::env::current_dir().unwrap(), target);
assert_eq!(guard.previous(), before);
}
assert_eq!(std::env::current_dir().unwrap(), before);
}
#[test]
fn pin_holds_the_current_directory() {
let before = std::env::current_dir().unwrap();
{
let _guard = CurrentDirGuard::pin().unwrap();
assert_eq!(std::env::current_dir().unwrap(), before);
}
assert_eq!(std::env::current_dir().unwrap(), before);
}
#[test]
fn change_to_rejects_a_missing_directory() {
let workspace = TestWorkspace::new("cwd-missing");
let missing = workspace.path().join("does-not-exist");
let error = CurrentDirGuard::change_to(&missing).unwrap_err();
assert!(
error
.to_string()
.contains("failed to set current directory")
);
}
#[test]
fn guards_serialize_across_threads() {
use std::sync::Arc;
use std::sync::Barrier;
use std::sync::atomic::{AtomicUsize, Ordering};
const THREADS: usize = 8;
let barrier = Arc::new(Barrier::new(THREADS));
let live = Arc::new(AtomicUsize::new(0));
let peak = Arc::new(AtomicUsize::new(0));
let handles: Vec<_> = (0..THREADS)
.map(|_| {
let barrier = Arc::clone(&barrier);
let live = Arc::clone(&live);
let peak = Arc::clone(&peak);
std::thread::spawn(move || {
barrier.wait();
let _guard = CurrentDirGuard::pin().unwrap();
let now = live.fetch_add(1, Ordering::SeqCst) + 1;
peak.fetch_max(now, Ordering::SeqCst);
for _ in 0..10_000 {
std::hint::spin_loop();
}
live.fetch_sub(1, Ordering::SeqCst);
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
assert_eq!(
peak.load(Ordering::SeqCst),
1,
"guards must never be held concurrently across threads"
);
}
#[test]
fn nested_guards_on_one_thread_do_not_deadlock() {
let _outer = CurrentDirGuard::pin().unwrap();
let _inner = CurrentDirGuard::pin().unwrap();
}
}