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 0 : pub fn write_content(mut self, contents: String) -> anyhow::Result<LockFileGuard> {
45 0 : self.file
46 0 : .set_len(0)
47 0 : .context("Failed to truncate lockfile")?;
48 0 : self.file
49 0 : .write_all(contents.as_bytes())
50 0 : .with_context(|| format!("Failed to write '{contents}' contents into lockfile"))?;
51 0 : crashsafe::fsync_file_and_parent(&self.path).context("fsync lockfile")?;
52 0 : Ok(LockFileGuard(self.file))
53 0 : }
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 0 : pub fn create_exclusive(lock_file_path: &Utf8Path) -> anyhow::Result<UnwrittenLockFile> {
64 0 : let lock_file = fs::OpenOptions::new()
65 0 : .create(true) // O_CREAT
66 0 : .truncate(true)
67 0 : .write(true)
68 0 : .open(lock_file_path)
69 0 : .context("open lock file")?;
70 :
71 0 : let res = fcntl::flock(
72 0 : lock_file.as_raw_fd(),
73 0 : fcntl::FlockArg::LockExclusiveNonblock,
74 0 : );
75 0 : match res {
76 0 : Ok(()) => Ok(UnwrittenLockFile {
77 0 : path: lock_file_path.to_owned(),
78 0 : file: lock_file,
79 0 : }),
80 0 : Err(EAGAIN) => anyhow::bail!("file is already locked"),
81 0 : Err(e) => Err(e).context("flock error"),
82 : }
83 0 : }
84 :
85 : /// Returned by [`read_and_hold_lock_file`].
86 : /// Check out the [`pid_file`](crate::pid_file) module for what the variants mean
87 : /// and potential caveats if the lock files that are used to store PIDs.
88 : pub enum LockFileRead {
89 : /// No file exists at the given path.
90 : NotExist,
91 : /// No other process held the lock file, so we grabbed an flock
92 : /// on it and read its contents.
93 : /// Release the flock by dropping the [`LockFileGuard`].
94 : NotHeldByAnyProcess(LockFileGuard, String),
95 : /// The file exists but another process was holding an flock on it.
96 : LockedByOtherProcess {
97 : not_locked_file: fs::File,
98 : content: String,
99 : },
100 : }
101 :
102 : /// Open & try to lock the lock file at the given `path`, returning a [handle][`LockFileRead`] to
103 : /// inspect its content.
104 : ///
105 : /// It is not an `Err(...)` if the file does not exist or is already locked.
106 : /// Check the [`LockFileRead`] variants for details.
107 0 : pub fn read_and_hold_lock_file(path: &Utf8Path) -> anyhow::Result<LockFileRead> {
108 0 : let res = fs::OpenOptions::new().read(true).open(path);
109 0 : let mut lock_file = match res {
110 0 : Ok(f) => f,
111 0 : Err(e) => match e.kind() {
112 0 : std::io::ErrorKind::NotFound => return Ok(LockFileRead::NotExist),
113 0 : _ => return Err(e).context("open lock file"),
114 : },
115 : };
116 0 : let res = fcntl::flock(
117 0 : lock_file.as_raw_fd(),
118 0 : fcntl::FlockArg::LockExclusiveNonblock,
119 0 : );
120 0 : // We need the content regardless of lock success / failure.
121 0 : // But, read it after flock so that, if it succeeded, the content is consistent.
122 0 : let mut content = String::new();
123 0 : lock_file
124 0 : .read_to_string(&mut content)
125 0 : .context("read lock file")?;
126 0 : match res {
127 0 : Ok(()) => Ok(LockFileRead::NotHeldByAnyProcess(
128 0 : LockFileGuard(lock_file),
129 0 : content,
130 0 : )),
131 0 : Err(EAGAIN) => Ok(LockFileRead::LockedByOtherProcess {
132 0 : not_locked_file: lock_file,
133 0 : content,
134 0 : }),
135 0 : Err(e) => Err(e).context("flock error"),
136 : }
137 0 : }
|