1use std::fs::{File, OpenOptions};
2use std::path::Path;
3use std::thread;
4use std::time::Duration;
5
6use fs2::FileExt;
7
8use crate::error::{MemvidError, Result};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum LockMode {
12 None,
13 Shared,
14 Exclusive,
15}
16
17pub struct FileLock {
19 file: File,
20 mode: LockMode,
21}
22
23impl FileLock {
24 pub fn open_and_lock(path: &Path) -> Result<(File, Self)> {
26 let file = OpenOptions::new().read(true).write(true).open(path)?;
27 let guard = Self::acquire_with_mode(&file, LockMode::Exclusive)?;
28 Ok((file, guard))
29 }
30
31 pub fn open_read_only(path: &Path) -> Result<(File, Self)> {
33 let file = OpenOptions::new().read(true).write(true).open(path)?;
34 let guard = Self::acquire_with_mode(&file, LockMode::Shared)?;
35 Ok((file, guard))
36 }
37
38 pub fn unlocked(file: &File) -> Result<Self> {
40 Ok(Self {
41 file: file.try_clone()?,
42 mode: LockMode::None,
43 })
44 }
45
46 pub fn acquire(file: &File, _path: &Path) -> Result<Self> {
48 Self::acquire_with_mode(file, LockMode::Exclusive)
49 }
50
51 pub fn try_acquire(_file: &File, path: &Path) -> Result<Option<Self>> {
53 let clone = OpenOptions::new().read(true).write(true).open(path)?;
54 loop {
55 match clone.try_lock_exclusive() {
56 Ok(()) => {
57 return Ok(Some(Self {
58 file: clone,
59 mode: LockMode::Exclusive,
60 }));
61 }
62 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue,
63 Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => return Ok(None),
64 Err(err) => return Err(MemvidError::Lock(err.to_string())),
65 }
66 }
67 }
68
69 pub fn unlock(&mut self) -> Result<()> {
71 if self.mode == LockMode::None {
72 return Ok(());
73 }
74 self.file
75 .unlock()
76 .map_err(|err| MemvidError::Lock(err.to_string()))
77 }
78
79 pub fn clone_handle(&self) -> Result<File> {
81 Ok(self.file.try_clone()?)
82 }
83
84 pub fn mode(&self) -> LockMode {
85 self.mode
86 }
87
88 pub fn downgrade_to_shared(&mut self) -> Result<()> {
89 if self.mode == LockMode::None {
90 return Err(MemvidError::Lock(
91 "cannot downgrade an unlocked file handle".to_string(),
92 ));
93 }
94 if self.mode == LockMode::Shared {
95 return Ok(());
96 }
97 self.file
98 .unlock()
99 .map_err(|err| MemvidError::Lock(err.to_string()))?;
100 Self::lock_with_retry(&self.file, LockMode::Shared)?;
101 self.mode = LockMode::Shared;
102 Ok(())
103 }
104
105 pub fn upgrade_to_exclusive(&mut self) -> Result<()> {
106 if self.mode == LockMode::None {
107 return Err(MemvidError::Lock(
108 "cannot upgrade an unlocked file handle".to_string(),
109 ));
110 }
111 if self.mode == LockMode::Exclusive {
112 return Ok(());
113 }
114 self.file
115 .unlock()
116 .map_err(|err| MemvidError::Lock(err.to_string()))?;
117 Self::lock_with_retry(&self.file, LockMode::Exclusive)?;
118 self.mode = LockMode::Exclusive;
119 Ok(())
120 }
121
122 pub(crate) fn acquire_with_mode(file: &File, mode: LockMode) -> Result<Self> {
123 let clone = file.try_clone()?;
124 Self::lock_with_retry(&clone, mode)?;
125 Ok(Self { file: clone, mode })
126 }
127
128 fn lock_with_retry(file: &File, mode: LockMode) -> Result<()> {
129 const MAX_ATTEMPTS: u32 = 200; const BACKOFF: Duration = Duration::from_millis(50);
131 let mut attempts = 0;
132 loop {
133 let result = match mode {
134 LockMode::None => return Ok(()),
135 LockMode::Exclusive => file.try_lock_exclusive(),
136 LockMode::Shared => FileExt::try_lock_shared(file),
137 };
138 match result {
139 Ok(()) => return Ok(()),
140 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue,
141 Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
142 if attempts >= MAX_ATTEMPTS {
143 return Err(MemvidError::Lock(
144 "exclusive access unavailable; file is in use by another process"
145 .to_string(),
146 ));
147 }
148 attempts += 1;
149 thread::sleep(BACKOFF);
150 continue;
151 }
152 Err(err) => return Err(MemvidError::Lock(err.to_string())),
153 }
154 }
155 }
156}
157
158impl Drop for FileLock {
159 fn drop(&mut self) {
160 if self.mode != LockMode::None {
161 let _ = self.file.unlock();
162 }
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use std::io::Write;
170 use tempfile::NamedTempFile;
171
172 #[test]
173 #[cfg(not(target_os = "windows"))] fn acquiring_lock_blocks_second_writer() {
175 let temp = NamedTempFile::new().expect("temp file");
176 let path = temp.path();
177 writeln!(&mut temp.as_file().try_clone().unwrap(), "seed").unwrap();
178
179 let file = OpenOptions::new()
180 .read(true)
181 .write(true)
182 .open(path)
183 .expect("open file");
184 let guard = FileLock::acquire(&file, path).expect("first lock succeeds");
185
186 let second = FileLock::try_acquire(&file, path).expect("second lock attempt");
187 assert!(second.is_none(), "lock should already be held");
188
189 drop(guard);
190 let third = FileLock::try_acquire(&file, path).expect("third lock attempt");
191 assert!(third.is_some(), "lock released after drop");
192 }
193}