evobench_tools/io_utils/
lockable_file.rs

1//! Wrapper guards around `fs2` crate.
2
3//! First move your file handle into a `LockableFile` via
4//! From/Into. Then call locking methods on that to get a guard with
5//! access to the file handle.
6
7use std::{
8    cell::RefCell,
9    collections::HashSet,
10    fmt::Display,
11    fs::File,
12    ops::Deref,
13    path::{Path, PathBuf},
14};
15
16use fs2::{FileExt, lock_contended_error};
17use lazy_static::lazy_static;
18use ouroboros::self_referencing;
19
20// -----------------------------------------------------------------------------
21
22pub struct SharedFileLock<'s, F: FileExt> {
23    debug: &'s Option<Box<Path>>,
24    // XX joke: need DerefMut anyway, even reading requires mut
25    // access. So the two locks are identical now. TODO: eliminate or
26    // ? Parameterize instead?
27    file: &'s F,
28}
29
30impl<'s, F: FileExt> Drop for SharedFileLock<'s, F> {
31    fn drop(&mut self) {
32        self.file
33            .unlock()
34            .expect("no way another path to unlock exists");
35        if let Some(path) = self.debug {
36            eprintln!("dropped SharedFileLock on {path:?}");
37            HELD_LOCKS.with_borrow_mut(|set| {
38                set.remove(&**path);
39            });
40        }
41    }
42}
43
44impl<'s, F: FileExt> Deref for SharedFileLock<'s, F> {
45    type Target = F;
46
47    fn deref(&self) -> &Self::Target {
48        self.file
49    }
50}
51
52// -----------------------------------------------------------------------------
53
54#[derive(Debug)]
55pub struct ExclusiveFileLock<'s, F: FileExt> {
56    debug: &'s Option<Box<Path>>,
57    file: &'s F,
58}
59
60impl<'s, F: FileExt> PartialEq for ExclusiveFileLock<'s, F> {
61    fn eq(&self, other: &Self) -> bool {
62        self.debug == other.debug
63    }
64}
65
66impl<'s, F: FileExt> Drop for ExclusiveFileLock<'s, F> {
67    fn drop(&mut self) {
68        self.file
69            .unlock()
70            .expect("no way another path to unlock exists");
71        if let Some(path) = self.debug {
72            eprintln!("dropped ExclusiveFileLock on {path:?}");
73            HELD_LOCKS.with_borrow_mut(|set| {
74                set.remove(&**path);
75            });
76        }
77    }
78}
79
80impl<'s, F: FileExt> Deref for ExclusiveFileLock<'s, F> {
81    type Target = F;
82
83    fn deref(&self) -> &Self::Target {
84        self.file
85    }
86}
87
88// -----------------------------------------------------------------------------
89
90#[derive(Debug)]
91pub struct LockableFile<F: FileExt> {
92    /// Path for, and only if, debugging is set via
93    /// `DEBUG_LOCKABLE_FILE` env var
94    debug: Option<Box<Path>>,
95    file: F,
96}
97
98impl<F: FileExt> From<F> for LockableFile<F> {
99    fn from(file: F) -> Self {
100        Self {
101            // XX can't have path here, what to do?
102            debug: None,
103            file,
104        }
105    }
106}
107
108/// Information about what kind of lock is held
109#[derive(Debug, Clone, Copy, PartialEq)]
110pub enum LockStatus {
111    Unlocked,
112    SharedLock,
113    ExclusiveLock,
114}
115impl LockStatus {
116    pub fn as_str(self) -> &'static str {
117        match self {
118            LockStatus::Unlocked => "unlocked",
119            LockStatus::SharedLock => "shared lock",
120            LockStatus::ExclusiveLock => "exclusive lock",
121        }
122    }
123}
124
125impl Display for LockStatus {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        f.write_str(self.as_str())
128    }
129}
130
131thread_local! {
132    pub(crate) static HELD_LOCKS: RefCell< HashSet<PathBuf>> = Default::default();
133}
134
135impl<F: FileExt> LockableFile<F> {
136    /// Determines lock status by temporarily getting locks in
137    /// nonblocking manner, thus not very performant! Also, may
138    /// erroneously return `LockStatus::SharedLock` if during testing
139    /// an exclusive lock is released.
140    pub fn get_lock_status(&mut self) -> std::io::Result<LockStatus> {
141        use LockStatus::*;
142        Ok(if self.try_lock_exclusive()?.is_some() {
143            Unlocked
144        } else if self.try_lock_shared()?.is_some() {
145            SharedLock
146        } else {
147            ExclusiveLock
148        })
149    }
150
151    pub fn lock_shared<'s>(&'s self) -> std::io::Result<SharedFileLock<'s, F>> {
152        if let Some(path) = self.debug.as_ref() {
153            eprintln!("getting SharedFileLock on {path:?}");
154            HELD_LOCKS.with_borrow_mut(|set| -> std::io::Result<()> {
155                // XXX: ah, todo: allow multiple shared
156                if set.contains(&**path) {
157                    panic!("{path:?} is already locked by this thread")
158                }
159                FileExt::lock_shared(&self.file)?;
160                eprintln!("got SharedFileLock on {path:?}");
161                set.insert((&**path).to_owned());
162                Ok(())
163            })?;
164        } else {
165            FileExt::lock_shared(&self.file)?;
166        }
167        Ok(SharedFileLock {
168            debug: &self.debug,
169            file: &self.file,
170        })
171    }
172
173    pub fn lock_exclusive<'s>(&'s self) -> std::io::Result<ExclusiveFileLock<'s, F>> {
174        if let Some(path) = self.debug.as_ref() {
175            eprintln!("getting ExclusiveFileLock on {path:?}");
176            HELD_LOCKS.with_borrow_mut(|set| -> std::io::Result<()> {
177                if set.contains(&**path) {
178                    panic!("{path:?} is already locked by this thread")
179                }
180                FileExt::lock_exclusive(&self.file)?;
181                eprintln!("got ExclusiveFileLock on {path:?}");
182                set.insert((&**path).to_owned());
183                Ok(())
184            })?;
185        } else {
186            FileExt::lock_exclusive(&self.file)?;
187        }
188        Ok(ExclusiveFileLock {
189            debug: &self.debug,
190            file: &self.file,
191        })
192    }
193
194    pub fn try_lock_shared<'s>(&'s self) -> std::io::Result<Option<SharedFileLock<'s, F>>> {
195        match FileExt::try_lock_shared(&self.file) {
196            Ok(()) => {
197                if let Some(path) = self.debug.as_ref() {
198                    eprintln!("got SharedFileLock on {path:?}");
199                    HELD_LOCKS.with_borrow_mut(|set| {
200                        set.insert((&**path).to_owned());
201                    });
202                }
203                Ok(Some(SharedFileLock {
204                    debug: &self.debug,
205                    file: &self.file,
206                }))
207            }
208            Err(e) => {
209                if e.kind() == lock_contended_error().kind() {
210                    Ok(None)
211                } else {
212                    Err(e)
213                }
214            }
215        }
216    }
217
218    pub fn try_lock_exclusive<'s>(&'s self) -> std::io::Result<Option<ExclusiveFileLock<'s, F>>> {
219        match FileExt::try_lock_exclusive(&self.file) {
220            Ok(()) => {
221                if let Some(path) = self.debug.as_ref() {
222                    eprintln!("got ExclusiveFileLock on {path:?}");
223                    HELD_LOCKS.with_borrow_mut(|set| {
224                        set.insert((&**path).to_owned());
225                    });
226                }
227                Ok(Some(ExclusiveFileLock {
228                    debug: &self.debug,
229                    file: &self.file,
230                }))
231            }
232            Err(e) => {
233                if e.kind() == lock_contended_error().kind() {
234                    Ok(None)
235                } else {
236                    Err(e)
237                }
238            }
239        }
240    }
241}
242
243lazy_static! {
244    static ref DEBUGGING: bool = if let Some(val) = std::env::var_os("DEBUG_LOCKABLE_FILE") {
245        match val
246            .into_string()
247            .expect("utf-8 for env var DEBUG_LOCKABLE_FILE")
248            .as_str()
249        {
250            "0" => false,
251            "1" | "" => true,
252            _ => panic!("need 1|0 or empty string for DEBUG_LOCKABLE_FILE"),
253        }
254    } else {
255        false
256    };
257}
258
259impl LockableFile<File> {
260    pub fn open<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
261        File::open(path.as_ref()).and_then(|file| {
262            let path = if *DEBUGGING {
263                Some(path.as_ref().to_owned().into_boxed_path())
264            } else {
265                None
266            };
267
268            Ok(LockableFile { debug: path, file })
269        })
270    }
271}
272
273/// A simple file or dir lock based on `flock`; dropping this type
274/// unlocks it and also drops the file handle at the same time, thus
275/// it's less efficient than allocating a `LockableFile<File>` and
276/// then doing the locking operations on it.
277#[self_referencing]
278pub struct StandaloneExclusiveFileLock {
279    lockable: LockableFile<File>,
280    #[borrows(lockable)]
281    #[covariant]
282    lock: Option<ExclusiveFileLock<'this, File>>,
283}
284
285#[derive(thiserror::Error, Debug)]
286pub enum StandaloneFileLockError {
287    #[error("error locking {path:?}: {error:#}")]
288    IOError {
289        path: PathBuf,
290        error: std::io::Error,
291    },
292
293    #[error("{msg}: the path {path:?} is already locked")]
294    AlreadyLocked { path: PathBuf, msg: String },
295}
296
297impl StandaloneExclusiveFileLock {
298    pub fn lock_path<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
299        Self::try_new(LockableFile::open(path)?, |file| {
300            Ok(Some(file.lock_exclusive()?))
301        })
302    }
303
304    /// If the lock is already taken, returns a
305    /// `StandaloneFileLockError::AlreadyLocked` error that includes
306    /// the result of running `already_locked_msg` as the first part
307    /// of the error message.
308    pub fn try_lock_path<P: AsRef<Path>>(
309        path: P,
310        already_locked_msg: impl Fn() -> String,
311    ) -> Result<Self, StandaloneFileLockError> {
312        let us = (|| -> std::io::Result<_> {
313            Self::try_new(LockableFile::open(path.as_ref())?, |file| {
314                file.try_lock_exclusive()
315            })
316        })()
317        .map_err(|error| StandaloneFileLockError::IOError {
318            path: path.as_ref().to_owned(),
319            error,
320        })?;
321        if us.borrow_lock().is_some() {
322            Ok(us)
323        } else {
324            let msg = already_locked_msg();
325            Err(StandaloneFileLockError::AlreadyLocked {
326                path: path.as_ref().to_owned(),
327                msg,
328            })
329        }
330    }
331}