From b6a84118410a60430c6f9cb525ece628c974467d Mon Sep 17 00:00:00 2001 From: Nikita Galaiko Date: Thu, 21 Dec 2023 11:43:48 +0100 Subject: [PATCH] lock::Dir implementation --- gitbutler-app/src/lib.rs | 1 + gitbutler-app/src/lock.rs | 158 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 gitbutler-app/src/lock.rs diff --git a/gitbutler-app/src/lib.rs b/gitbutler-app/src/lib.rs index 698a487ab..071ffdb5f 100644 --- a/gitbutler-app/src/lib.rs +++ b/gitbutler-app/src/lib.rs @@ -15,6 +15,7 @@ pub mod gb_repository; pub mod git; pub mod github; pub mod keys; +pub mod lock; pub mod logs; pub mod menu; pub mod paths; diff --git a/gitbutler-app/src/lock.rs b/gitbutler-app/src/lock.rs new file mode 100644 index 000000000..7cb6fb040 --- /dev/null +++ b/gitbutler-app/src/lock.rs @@ -0,0 +1,158 @@ +use std::sync::{Arc, Mutex}; + +#[derive(Debug, Clone)] +pub struct Dir { + inner: Arc, +} + +impl Dir { + pub fn new>(path: P) -> Result { + Inner::new(path).map(Arc::new).map(|inner| Self { inner }) + } + + pub fn batch( + &self, + action: impl FnOnce(&std::path::Path) -> Result, + ) -> Result> { + self.inner.batch(action) + } +} + +#[derive(Debug)] +struct Inner { + path: std::path::PathBuf, + flock: Mutex, +} + +impl Inner { + fn new>(path: P) -> Result { + let path = path.as_ref().to_path_buf(); + if !path.is_dir() { + return Err(OpenError::NotDirectory(path)); + } + let flock = fslock::LockFile::open(&path.with_extension("lock")).map(Mutex::new)?; + Ok(Self { path, flock }) + } + + fn batch( + &self, + action: impl FnOnce(&std::path::Path) -> Result, + ) -> Result> { + let mut flock = self.flock.lock().unwrap(); + + flock.lock()?; + let result = action(&self.path).map_err(BatchError::Batch); + flock.unlock()?; + + result + } +} + +#[derive(Debug, thiserror::Error)] +pub enum OpenError { + #[error("{0} is not a directory")] + NotDirectory(std::path::PathBuf), + #[error(transparent)] + Io(#[from] std::io::Error), +} + +#[derive(Debug, thiserror::Error)] +pub enum BatchError { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Batch(E), +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::test_utils::temp_dir; + + #[tokio::test] + async fn test_lock_same_instance() { + let dir_path = temp_dir(); + std::fs::write(dir_path.join("file.txt"), "").unwrap(); + let dir = Dir::new(&dir_path).unwrap(); + + let (tx, rx) = std::sync::mpsc::sync_channel(1); + + // spawn a task that will signal right after aquireing the lock + let _ = tokio::spawn({ + let dir = dir.clone(); + async move { + dir.batch(|root| { + tx.send(()).unwrap(); + assert_eq!( + std::fs::read_to_string(root.join("file.txt")).unwrap(), + String::new() + ); + std::fs::write(root.join("file.txt"), "1") + }) + } + }) + .await + .unwrap(); + + // then we wait until the lock is aquired + rx.recv().unwrap(); + + // and immidiately try to lock again + dir.batch(|root| { + assert_eq!(std::fs::read_to_string(root.join("file.txt")).unwrap(), "1"); + std::fs::write(root.join("file.txt"), "2") + }) + .unwrap(); + + assert_eq!( + std::fs::read_to_string(dir_path.join("file.txt")).unwrap(), + "2" + ); + } + + #[tokio::test] + async fn test_lock_different_instances() { + let dir_path = temp_dir(); + std::fs::write(dir_path.join("file.txt"), "").unwrap(); + + let (tx, rx) = std::sync::mpsc::sync_channel(1); + + // spawn a task that will signal right after aquireing the lock + let _ = tokio::spawn({ + let dir_path = dir_path.clone(); + async move { + // one dir instance is created on a separate thread + let dir = Dir::new(&dir_path).unwrap(); + dir.batch(|root| { + tx.send(()).unwrap(); + assert_eq!( + std::fs::read_to_string(root.join("file.txt")).unwrap(), + String::new() + ); + std::fs::write(root.join("file.txt"), "1") + }) + } + }) + .await + .unwrap(); + + // another dir instance is created on the main thread + let dir = Dir::new(&dir_path).unwrap(); + + // then we wait until the lock is aquired + rx.recv().unwrap(); + + // and immidiately try to lock again + dir.batch(|root| { + assert_eq!(std::fs::read_to_string(root.join("file.txt")).unwrap(), "1"); + std::fs::write(root.join("file.txt"), "2") + }) + .unwrap(); + + assert_eq!( + std::fs::read_to_string(dir_path.join("file.txt")).unwrap(), + "2" + ); + } +}