LCOV - code coverage report
Current view: top level - libs/utils/src - lock_file.rs (source / functions) Coverage Total Hit
Test: c639aa5f7ab62b43d647b10f40d15a15686ce8a9.info Lines: 83.1 % 59 49
Test Date: 2024-02-12 20:26:03 Functions: 60.0 % 5 3

            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::{
      10              :     fs,
      11              :     io::{Read, Write},
      12              :     ops::Deref,
      13              :     os::unix::prelude::AsRawFd,
      14              : };
      15              : 
      16              : use anyhow::Context;
      17              : use camino::{Utf8Path, Utf8PathBuf};
      18              : use nix::{errno::Errno::EAGAIN, fcntl};
      19              : 
      20              : use crate::crashsafe;
      21              : 
      22              : /// A handle to an open and unlocked, but not-yet-written lock file.
      23              : /// Returned by [`create_exclusive`].
      24              : #[must_use]
      25              : pub struct UnwrittenLockFile {
      26              :     path: Utf8PathBuf,
      27              :     file: fs::File,
      28              : }
      29              : 
      30              : /// Returned by [`UnwrittenLockFile::write_content`].
      31              : #[must_use]
      32              : pub struct LockFileGuard(fs::File);
      33              : 
      34              : impl Deref for LockFileGuard {
      35              :     type Target = fs::File;
      36              : 
      37            0 :     fn deref(&self) -> &Self::Target {
      38            0 :         &self.0
      39            0 :     }
      40              : }
      41              : 
      42              : impl UnwrittenLockFile {
      43              :     /// Replace the content of this lock file with the byte representation of `contents`.
      44         1133 :     pub fn write_content(mut self, contents: String) -> anyhow::Result<LockFileGuard> {
      45         1133 :         self.file
      46         1133 :             .set_len(0)
      47         1133 :             .context("Failed to truncate lockfile")?;
      48         1133 :         self.file
      49         1133 :             .write_all(contents.as_bytes())
      50         1133 :             .with_context(|| format!("Failed to write '{contents}' contents into lockfile"))?;
      51         1133 :         crashsafe::fsync_file_and_parent(&self.path).context("fsync lockfile")?;
      52         1133 :         Ok(LockFileGuard(self.file))
      53         1133 :     }
      54              : }
      55              : 
      56              : /// Creates and opens a lock file in the path, grabs an exclusive flock on it, and returns
      57              : /// a handle that allows overwriting the locked file's content.
      58              : ///
      59              : /// The exclusive lock is released when dropping the returned handle.
      60              : ///
      61              : /// It is not an error if the file already exists.
      62              : /// It is an error if the file is already locked.
      63         1133 : pub fn create_exclusive(lock_file_path: &Utf8Path) -> anyhow::Result<UnwrittenLockFile> {
      64         1133 :     let lock_file = fs::OpenOptions::new()
      65         1133 :         .create(true) // O_CREAT
      66         1133 :         .write(true)
      67         1133 :         .open(lock_file_path)
      68         1133 :         .context("open lock file")?;
      69              : 
      70         1133 :     let res = fcntl::flock(
      71         1133 :         lock_file.as_raw_fd(),
      72         1133 :         fcntl::FlockArg::LockExclusiveNonblock,
      73         1133 :     );
      74            0 :     match res {
      75         1133 :         Ok(()) => Ok(UnwrittenLockFile {
      76         1133 :             path: lock_file_path.to_owned(),
      77         1133 :             file: lock_file,
      78         1133 :         }),
      79            0 :         Err(EAGAIN) => anyhow::bail!("file is already locked"),
      80            0 :         Err(e) => Err(e).context("flock error"),
      81              :     }
      82         1133 : }
      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. It is not an `Err(...)` if the file does not exist or is already locked.
     103              : /// Check the [`LockFileRead`] variants for details.
     104         3405 : pub fn read_and_hold_lock_file(path: &Utf8Path) -> anyhow::Result<LockFileRead> {
     105         3405 :     let res = fs::OpenOptions::new().read(true).open(path);
     106         3405 :     let mut lock_file = match res {
     107         3405 :         Ok(f) => f,
     108            0 :         Err(e) => match e.kind() {
     109            0 :             std::io::ErrorKind::NotFound => return Ok(LockFileRead::NotExist),
     110            0 :             _ => return Err(e).context("open lock file"),
     111              :         },
     112              :     };
     113         3405 :     let res = fcntl::flock(
     114         3405 :         lock_file.as_raw_fd(),
     115         3405 :         fcntl::FlockArg::LockExclusiveNonblock,
     116         3405 :     );
     117         3405 :     // We need the content regardless of lock success / failure.
     118         3405 :     // But, read it after flock so that, if it succeeded, the content is consistent.
     119         3405 :     let mut content = String::new();
     120         3405 :     lock_file
     121         3405 :         .read_to_string(&mut content)
     122         3405 :         .context("read lock file")?;
     123         3358 :     match res {
     124           47 :         Ok(()) => Ok(LockFileRead::NotHeldByAnyProcess(
     125           47 :             LockFileGuard(lock_file),
     126           47 :             content,
     127           47 :         )),
     128         3358 :         Err(EAGAIN) => Ok(LockFileRead::LockedByOtherProcess {
     129         3358 :             not_locked_file: lock_file,
     130         3358 :             content,
     131         3358 :         }),
     132            0 :         Err(e) => Err(e).context("flock error"),
     133              :     }
     134         3405 : }
        

Generated by: LCOV version 2.1-beta