//! A module to create and read lock files. //! //! File locking is done using [`nix::fcntl::Flock`] exclusive locks. //! The only consumer of this module is currently //! [`pid_file`](crate::pid_file). See the module-level comment //! there for potential pitfalls with lock files that are used //! to store PIDs (pidfiles). use std::fs; use std::io::{Read, Write}; use std::ops::Deref; use anyhow::Context; use camino::{Utf8Path, Utf8PathBuf}; use nix::errno::Errno::EAGAIN; use nix::fcntl::{Flock, FlockArg}; use crate::crashsafe; /// A handle to an open and flocked, but not-yet-written lock file. /// Returned by [`create_exclusive`]. #[must_use] pub struct UnwrittenLockFile { path: Utf8PathBuf, file: Flock, } /// Returned by [`UnwrittenLockFile::write_content`]. #[must_use] pub struct LockFileGuard(Flock); impl Deref for LockFileGuard { type Target = fs::File; fn deref(&self) -> &Self::Target { &self.0 } } impl UnwrittenLockFile { /// Replace the content of this lock file with the byte representation of `contents`. pub fn write_content(mut self, contents: String) -> anyhow::Result { self.file .set_len(0) .context("Failed to truncate lockfile")?; self.file .write_all(contents.as_bytes()) .with_context(|| format!("Failed to write '{contents}' contents into lockfile"))?; crashsafe::fsync_file_and_parent(&self.path).context("fsync lockfile")?; Ok(LockFileGuard(self.file)) } } /// Creates and opens a lock file in the path, grabs an exclusive flock on it, and returns /// a handle that allows overwriting the locked file's content. /// /// The exclusive lock is released when dropping the returned handle. /// /// It is not an error if the file already exists. /// It is an error if the file is already locked. pub fn create_exclusive(lock_file_path: &Utf8Path) -> anyhow::Result { let lock_file = fs::OpenOptions::new() .create(true) // O_CREAT .truncate(true) .write(true) .open(lock_file_path) .context("open lock file")?; let res = Flock::lock(lock_file, FlockArg::LockExclusiveNonblock); match res { Ok(lock_file) => Ok(UnwrittenLockFile { path: lock_file_path.to_owned(), file: lock_file, }), Err((_, EAGAIN)) => anyhow::bail!("file is already locked"), Err((_, e)) => Err(e).context("flock error"), } } /// Returned by [`read_and_hold_lock_file`]. /// Check out the [`pid_file`](crate::pid_file) module for what the variants mean /// and potential caveats if the lock files that are used to store PIDs. pub enum LockFileRead { /// No file exists at the given path. NotExist, /// No other process held the lock file, so we grabbed an flock /// on it and read its contents. /// Release the flock by dropping the [`LockFileGuard`]. NotHeldByAnyProcess(LockFileGuard, String), /// The file exists but another process was holding an flock on it. LockedByOtherProcess { not_locked_file: fs::File, content: String, }, } /// Open & try to lock the lock file at the given `path`, returning a [handle][`LockFileRead`] to /// inspect its content. /// /// It is not an `Err(...)` if the file does not exist or is already locked. /// Check the [`LockFileRead`] variants for details. pub fn read_and_hold_lock_file(path: &Utf8Path) -> anyhow::Result { let res = fs::OpenOptions::new().read(true).open(path); let lock_file = match res { Ok(f) => f, Err(e) => match e.kind() { std::io::ErrorKind::NotFound => return Ok(LockFileRead::NotExist), _ => return Err(e).context("open lock file"), }, }; let res = Flock::lock(lock_file, FlockArg::LockExclusiveNonblock); // We need the content regardless of lock success / failure. // But, read it after flock so that, if it succeeded, the content is consistent. match res { Ok(mut locked_file) => { let mut content = String::new(); locked_file .read_to_string(&mut content) .context("read lock file")?; Ok(LockFileRead::NotHeldByAnyProcess( LockFileGuard(locked_file), content, )) } Err((mut not_locked_file, EAGAIN)) => { let mut content = String::new(); not_locked_file .read_to_string(&mut content) .context("read lock file")?; Ok(LockFileRead::LockedByOtherProcess { not_locked_file, content, }) } Err((_, e)) => Err(e).context("flock error"), } }