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

Generated by: LCOV version 2.1-beta