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