summaryrefslogtreecommitdiffstats
path: root/crates/common/flockfile/src/unix.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/common/flockfile/src/unix.rs')
-rw-r--r--crates/common/flockfile/src/unix.rs171
1 files changed, 171 insertions, 0 deletions
diff --git a/crates/common/flockfile/src/unix.rs b/crates/common/flockfile/src/unix.rs
new file mode 100644
index 00000000..28646601
--- /dev/null
+++ b/crates/common/flockfile/src/unix.rs
@@ -0,0 +1,171 @@
+use nix::fcntl::{flock, FlockArg};
+use std::{
+ fs::{self, File, OpenOptions},
+ io,
+ os::unix::io::AsRawFd,
+ path::{Path, PathBuf},
+};
+use tracing::{debug, error, warn};
+
+#[derive(thiserror::Error, Debug)]
+pub enum FlockfileError {
+ #[error("Couldn't acquire file lock.")]
+ FromIo {
+ path: PathBuf,
+ #[source]
+ source: std::io::Error,
+ },
+
+ #[error("Couldn't acquire file lock.")]
+ FromNix {
+ path: PathBuf,
+ #[source]
+ source: nix::Error,
+ },
+}
+
+/// flockfile creates a lockfile in the filesystem under `/run/lock` and then creates a filelock using system fcntl with flock.
+/// flockfile will automatically remove lockfile on application exit and the OS should cleanup the filelock afterwards.
+/// If application exits unexpectedly the filelock will be dropped, but the lockfile will not be removed unless handled in signal handler.
+#[derive(Debug)]
+pub struct Flockfile {
+ handle: Option<File>,
+ pub path: PathBuf,
+}
+
+impl Flockfile {
+ /// Create new lockfile in `/run/lock` with specific name:
+ ///
+ /// #Example
+ ///
+ /// let _lockfile = match flockfile::Flockfile::new_lock("app")).unwrap();
+ ///
+ pub fn new_lock(lock_name: impl AsRef<Path>) -> Result<Flockfile, FlockfileError> {
+ let path = Path::new("/run/lock").join(lock_name);
+
+ let file = match OpenOptions::new()
+ .create(true)
+ .read(true)
+ .write(true)
+ .open(&path)
+ {
+ Ok(file) => file,
+ Err(err) => {
+ return Err(FlockfileError::FromIo { path, source: err });
+ }
+ };
+
+ let () = match flock(file.as_raw_fd(), FlockArg::LockExclusiveNonblock) {
+ Ok(()) => (),
+ Err(err) => {
+ return Err(FlockfileError::FromNix { path, source: err });
+ }
+ };
+
+ debug!(r#"Lockfile created "{:?}""#, &path);
+ Ok(Flockfile {
+ handle: Some(file),
+ path,
+ })
+ }
+
+ /// Manually remove filelock and lockfile from the filesystem, this method doesn't have to be called explicitly,
+ /// however if access to the locked file is required this must be called.
+ pub fn unlock(mut self) -> Result<(), io::Error> {
+ self.handle.take().expect("handle dropped");
+ fs::remove_file(&self.path)?;
+ Ok(())
+ }
+}
+
+impl Drop for Flockfile {
+ /// The Drop trait will be called always when the lock goes out of scope, however,
+ /// if the program exits unexpectedly and drop is not called the lock will be removed by the system.
+ fn drop(&mut self) {
+ if let Some(handle) = self.handle.take() {
+ drop(handle);
+
+ // Even if the file is not removed this is not an issue, as OS will take care of the flock.
+ // Additionally if the file is created before an attempt to create the lock that won't be an issue as we rely on filesystem lock.
+ match fs::remove_file(&self.path) {
+ Ok(()) => debug!(r#"Lockfile deleted "{:?}""#, self.path),
+ Err(err) => warn!(
+ r#"Error while handling lockfile at "{:?}": {:?}"#,
+ self.path, err
+ ),
+ }
+ }
+ }
+}
+
+impl AsRef<Path> for Flockfile {
+ fn as_ref(&self) -> &Path {
+ self.path.as_ref()
+ }
+}
+
+/// Check /run/lock/ for a lock file of a given `app_name`
+pub fn check_another_instance_is_not_running(app_name: &str) -> Result<Flockfile, FlockfileError> {
+ match Flockfile::new_lock(format!("{}.lock", app_name)) {
+ Ok(file) => Ok(file),
+ Err(err) => {
+ return match &err {
+ FlockfileError::FromIo { path, .. } | FlockfileError::FromNix { path, .. } => {
+ error!("Another instance of {} is running.", app_name);
+ error!("Lock file path: {}", path.as_path().to_str().unwrap());
+ Err(err)
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+
+ use super::*;
+ use assert_matches::*;
+ use std::{fs, io};
+ use tempfile::NamedTempFile;
+
+ #[test]
+ fn lock_access_remove() {
+ let path = NamedTempFile::new().unwrap().into_temp_path().to_owned();
+ let lockfile = Flockfile::new_lock(&path).unwrap();
+
+ assert_eq!(lockfile.path, path);
+
+ lockfile.unlock().unwrap();
+
+ assert_eq!(
+ fs::metadata(path).unwrap_err().kind(),
+ io::ErrorKind::NotFound
+ );
+ }
+
+ #[test]
+ fn lock_out_of_scope() {
+ let path = NamedTempFile::new().unwrap().into_temp_path().to_owned();
+ {
+ let _lockfile = Flockfile::new_lock(&path).unwrap();
+ // assert!(path.exists());
+ assert!(fs::metadata(&path).is_ok());
+ }
+
+ assert_eq!(
+ fs::metadata(path).unwrap_err().kind(),
+ io::ErrorKind::NotFound
+ );
+ }
+
+ #[test]
+ fn lock_twice() {
+ let path = NamedTempFile::new().unwrap().into_temp_path().to_owned();
+ let _lockfile = Flockfile::new_lock(&path).unwrap();
+
+ assert_matches!(
+ Flockfile::new_lock(&path).unwrap_err(),
+ FlockfileError::FromNix { .. }
+ );
+ }
+}