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 : }
|