tmc_langs_util/file_util/
lock_unix.rs

1//! File locking utilities on Unix-based platforms.
2
3use super::LockOptions;
4use crate::{
5    error::FileError,
6    file_util::{self, LOCK_FILE_NAME},
7};
8use file_lock::{FileLock, FileOptions};
9use std::{
10    fs::File,
11    path::{Path, PathBuf},
12};
13
14/// Blocks until the lock can be acquired.
15#[derive(Debug)]
16pub struct Lock {
17    pub path: PathBuf,
18    options: LockOptions,
19    lock_file_path: Option<PathBuf>,
20    forget: bool,
21}
22
23impl Lock {
24    pub fn file(path: impl AsRef<Path>, options: LockOptions) -> Result<Self, FileError> {
25        let path = path.as_ref().to_path_buf();
26
27        if matches!(options, LockOptions::ReadCreate | LockOptions::WriteCreate) {
28            if let Some(parent) = path.parent() {
29                file_util::create_dir_all(parent)?;
30            }
31        }
32        Ok(Self {
33            path,
34            options,
35            lock_file_path: None,
36            forget: false,
37        })
38    }
39
40    pub fn dir(path: impl AsRef<Path>, options: LockOptions) -> Result<Self, FileError> {
41        let path = path.as_ref().to_path_buf();
42
43        if matches!(options, LockOptions::ReadCreate | LockOptions::WriteCreate) {
44            file_util::create_dir_all(&path)?;
45        }
46
47        let lock_path = path.join(LOCK_FILE_NAME);
48        // first, try to create the lock file. this requires write options
49        // blocking set to false so it will fail if the lock file already exists,
50        // which is okay since we're not actually locking it here
51        let _creator_lock = FileLock::lock(
52            &lock_path,
53            false,
54            FileOptions::new().write(true).create(true),
55        );
56
57        Ok(Self {
58            path,
59            options,
60            lock_file_path: Some(lock_path),
61            forget: false,
62        })
63    }
64
65    pub fn lock(&mut self) -> Result<Guard<'_>, FileError> {
66        log::trace!("locking {}", self.path.display());
67        let path = match &self.lock_file_path {
68            Some(lock_file) => lock_file,
69            None => &self.path,
70        };
71        let lock = match FileLock::lock(path, true, self.options.into_file_options()) {
72            Ok(lock) => {
73                log::trace!("locked {}", path.display());
74                FileOrLock::Lock(lock)
75            }
76            Err(err) => {
77                // the file locking is mostly a safeguard rather than something absolutely necessary
78                // so rather than preventing the program from runningg here we'll just continue and things will probably work out
79                log::error!("Failed to lock {}: {err}", path.display());
80                let file = self
81                    .options
82                    .into_open_options()
83                    .open(&self.path)
84                    .map_err(|e| FileError::FileOpen(path.to_path_buf(), e))?;
85                FileOrLock::File(file)
86            }
87        };
88        Ok(Guard { lock, path })
89    }
90
91    pub fn forget(mut self) {
92        self.forget = true;
93    }
94}
95
96impl Drop for Lock {
97    fn drop(&mut self) {
98        if self.forget {
99            return;
100        }
101
102        // check if we created a lock file
103        if let Some(lock_file_path) = self.lock_file_path.take() {
104            // try to get a write lock and delete file
105            // if we can't get the lock, something else probably has it locked and we leave it there
106            match FileLock::lock(
107                &lock_file_path,
108                false,
109                FileOptions::new().read(true).write(true),
110            ) {
111                Ok(_) => {
112                    let _ = file_util::remove_file(&lock_file_path);
113                }
114                Err(err) => {
115                    log::warn!(
116                        "Failed to remove lock file {}: {err}",
117                        lock_file_path.display()
118                    );
119                }
120            }
121        }
122    }
123}
124
125#[derive(Debug)]
126pub struct Guard<'a> {
127    lock: FileOrLock,
128    path: &'a Path,
129}
130
131impl Guard<'_> {
132    pub fn get_file(&self) -> &File {
133        match &self.lock {
134            FileOrLock::File(f) => f,
135            FileOrLock::Lock(l) => &l.file,
136        }
137    }
138
139    pub fn get_file_mut(&mut self) -> &mut File {
140        match &mut self.lock {
141            FileOrLock::File(f) => f,
142            FileOrLock::Lock(l) => &mut l.file,
143        }
144    }
145}
146
147impl Drop for Guard<'_> {
148    fn drop(&mut self) {
149        log::trace!("unlocking {}", self.path.display())
150    }
151}
152
153#[derive(Debug)]
154enum FileOrLock {
155    File(File),
156    Lock(FileLock),
157}
158
159impl LockOptions {
160    fn into_file_options(self) -> FileOptions {
161        match self {
162            LockOptions::Read => FileOptions::new().read(true),
163            LockOptions::ReadCreate => FileOptions::new().read(true).create(true),
164            LockOptions::ReadTruncate => FileOptions::new().read(true).create(true).truncate(true),
165            LockOptions::Write => FileOptions::new().read(true).write(true).append(true),
166            LockOptions::WriteCreate => FileOptions::new()
167                .read(true)
168                .write(true)
169                .append(true)
170                .create(true),
171            LockOptions::WriteTruncate => FileOptions::new()
172                .read(true)
173                .write(true)
174                .create(true)
175                .truncate(true),
176        }
177    }
178}
179
180#[cfg(test)]
181mod test {
182    use super::*;
183
184    #[test]
185    fn can_lock_file() {
186        let file = tempfile::NamedTempFile::new().unwrap();
187        let _lock = Lock::file(file.path(), LockOptions::Read).unwrap();
188    }
189
190    #[test]
191    fn can_lock_dir() {
192        let dir = tempfile::tempdir().unwrap();
193        let _lock = Lock::dir(dir.path(), LockOptions::Read).unwrap();
194    }
195
196    #[test]
197    fn can_delete_locked_file() {
198        let file = tempfile::NamedTempFile::new().unwrap();
199        let _lock = Lock::file(file.path(), LockOptions::Read).unwrap();
200        let _delete_lock = Lock::file(file.path(), LockOptions::Write).unwrap();
201        file_util::remove_file(file.path()).unwrap();
202    }
203}