evobench_tools/io_utils/
lockable_file.rs1use 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
20pub struct SharedFileLock<'s, F: FileExt> {
23 debug: &'s Option<Box<Path>>,
24 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#[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#[derive(Debug)]
91pub struct LockableFile<F: FileExt> {
92 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 debug: None,
103 file,
104 }
105 }
106}
107
108#[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 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 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#[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 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}