LCOV - code coverage report
Current view: top level - libs/utils/src - lock_file.rs (source / functions) Coverage Total Hit
Test: 5445d246133daeceb0507e6cc0797ab7c1c70cb8.info Lines: 0.0 % 60 0
Test Date: 2025-03-12 18:05:02 Functions: 0.0 % 5 0

            Line data    Source code
       1              : //! A module to create and read lock files.
       2              : //!
       3              : //! File locking is done using [`fcntl::flock`] exclusive locks.
       4              : //! The only consumer of this module is currently
       5              : //! [`pid_file`](crate::pid_file). See the module-level comment
       6              : //! there for potential pitfalls with lock files that are used
       7              : //! to store PIDs (pidfiles).
       8              : 
       9              : use std::fs;
      10              : use std::io::{Read, Write};
      11              : use std::ops::Deref;
      12              : use std::os::unix::prelude::AsRawFd;
      13              : 
      14              : use anyhow::Context;
      15              : use camino::{Utf8Path, Utf8PathBuf};
      16              : use nix::errno::Errno::EAGAIN;
      17              : use nix::fcntl;
      18              : 
      19              : use crate::crashsafe;
      20              : 
      21              : /// A handle to an open and unlocked, but not-yet-written lock file.
      22              : /// Returned by [`create_exclusive`].
      23              : #[must_use]
      24              : pub struct UnwrittenLockFile {
      25              :     path: Utf8PathBuf,
      26              :     file: fs::File,
      27              : }
      28              : 
      29              : /// Returned by [`UnwrittenLockFile::write_content`].
      30              : #[must_use]
      31              : pub struct LockFileGuard(fs::File);
      32              : 
      33              : impl Deref for LockFileGuard {
      34              :     type Target = fs::File;
      35              : 
      36            0 :     fn deref(&self) -> &Self::Target {
      37            0 :         &self.0
      38            0 :     }
      39              : }
      40              : 
      41              : impl UnwrittenLockFile {
      42              :     /// Replace the content of this lock file with the byte representation of `contents`.
      43            0 :     pub fn write_content(mut self, contents: String) -> anyhow::Result<LockFileGuard> {
      44            0 :         self.file
      45            0 :             .set_len(0)
      46            0 :             .context("Failed to truncate lockfile")?;
      47            0 :         self.file
      48            0 :             .write_all(contents.as_bytes())
      49            0 :             .with_context(|| format!("Failed to write '{contents}' contents into lockfile"))?;
      50            0 :         crashsafe::fsync_file_and_parent(&self.path).context("fsync lockfile")?;
      51            0 :         Ok(LockFileGuard(self.file))
      52            0 :     }
      53              : }
      54              : 
      55              : /// Creates and opens a lock file in the path, grabs an exclusive flock on it, and returns
      56              : /// a handle that allows overwriting the locked file's content.
      57              : ///
      58              : /// The exclusive lock is released when dropping the returned handle.
      59              : ///
      60              : /// It is not an error if the file already exists.
      61              : /// It is an error if the file is already locked.
      62            0 : pub fn create_exclusive(lock_file_path: &Utf8Path) -> anyhow::Result<UnwrittenLockFile> {
      63            0 :     let lock_file = fs::OpenOptions::new()
      64            0 :         .create(true) // O_CREAT
      65            0 :         .truncate(true)
      66            0 :         .write(true)
      67            0 :         .open(lock_file_path)
      68            0 :         .context("open lock file")?;
      69              : 
      70            0 :     let res = fcntl::flock(
      71            0 :         lock_file.as_raw_fd(),
      72            0 :         fcntl::FlockArg::LockExclusiveNonblock,
      73            0 :     );
      74            0 :     match res {
      75            0 :         Ok(()) => Ok(UnwrittenLockFile {
      76            0 :             path: lock_file_path.to_owned(),
      77            0 :             file: lock_file,
      78            0 :         }),
      79            0 :         Err(EAGAIN) => anyhow::bail!("file is already locked"),
      80            0 :         Err(e) => Err(e).context("flock error"),
      81              :     }
      82            0 : }
      83              : 
      84              : /// Returned by [`read_and_hold_lock_file`].
      85              : /// Check out the [`pid_file`](crate::pid_file) module for what the variants mean
      86              : /// and potential caveats if the lock files that are used to store PIDs.
      87              : pub enum LockFileRead {
      88              :     /// No file exists at the given path.
      89              :     NotExist,
      90              :     /// No other process held the lock file, so we grabbed an flock
      91              :     /// on it and read its contents.
      92              :     /// Release the flock by dropping the [`LockFileGuard`].
      93              :     NotHeldByAnyProcess(LockFileGuard, String),
      94              :     /// The file exists but another process was holding an flock on it.
      95              :     LockedByOtherProcess {
      96              :         not_locked_file: fs::File,
      97              :         content: String,
      98              :     },
      99              : }
     100              : 
     101              : /// Open & try to lock the lock file at the given `path`, returning a [handle][`LockFileRead`] to
     102              : /// inspect its content.
     103              : ///
     104              : /// It is not an `Err(...)` if the file does not exist or is already locked.
     105              : /// Check the [`LockFileRead`] variants for details.
     106            0 : pub fn read_and_hold_lock_file(path: &Utf8Path) -> anyhow::Result<LockFileRead> {
     107            0 :     let res = fs::OpenOptions::new().read(true).open(path);
     108            0 :     let mut lock_file = match res {
     109            0 :         Ok(f) => f,
     110            0 :         Err(e) => match e.kind() {
     111            0 :             std::io::ErrorKind::NotFound => return Ok(LockFileRead::NotExist),
     112            0 :             _ => return Err(e).context("open lock file"),
     113              :         },
     114              :     };
     115            0 :     let res = fcntl::flock(
     116            0 :         lock_file.as_raw_fd(),
     117            0 :         fcntl::FlockArg::LockExclusiveNonblock,
     118            0 :     );
     119            0 :     // We need the content regardless of lock success / failure.
     120            0 :     // But, read it after flock so that, if it succeeded, the content is consistent.
     121            0 :     let mut content = String::new();
     122            0 :     lock_file
     123            0 :         .read_to_string(&mut content)
     124            0 :         .context("read lock file")?;
     125            0 :     match res {
     126            0 :         Ok(()) => Ok(LockFileRead::NotHeldByAnyProcess(
     127            0 :             LockFileGuard(lock_file),
     128            0 :             content,
     129            0 :         )),
     130            0 :         Err(EAGAIN) => Ok(LockFileRead::LockedByOtherProcess {
     131            0 :             not_locked_file: lock_file,
     132            0 :             content,
     133            0 :         }),
     134            0 :         Err(e) => Err(e).context("flock error"),
     135              :     }
     136            0 : }
        

Generated by: LCOV version 2.1-beta