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 #[must_use]
85 pub fn mode(&self) -> LockMode {
86 self.mode
87 }
88
89 pub fn downgrade_to_shared(&mut self) -> Result<()> {
90 if self.mode == LockMode::None {
91 return Err(MemvidError::Lock(
92 "cannot downgrade an unlocked file handle".to_string(),
93 ));
94 }
95 if self.mode == LockMode::Shared {
96 return Ok(());
97 }
98 self.file
99 .unlock()
100 .map_err(|err| MemvidError::Lock(err.to_string()))?;
101 Self::lock_with_retry(&self.file, LockMode::Shared)?;
102 self.mode = LockMode::Shared;
103 Ok(())
104 }
105
106 pub fn upgrade_to_exclusive(&mut self) -> Result<()> {
107 if self.mode == LockMode::None {
108 return Err(MemvidError::Lock(
109 "cannot upgrade an unlocked file handle".to_string(),
110 ));
111 }
112 if self.mode == LockMode::Exclusive {
113 return Ok(());
114 }
115 self.file
116 .unlock()
117 .map_err(|err| MemvidError::Lock(err.to_string()))?;
118 Self::lock_with_retry(&self.file, LockMode::Exclusive)?;
119 self.mode = LockMode::Exclusive;
120 Ok(())
121 }
122
123 pub(crate) fn acquire_with_mode(file: &File, mode: LockMode) -> Result<Self> {
124 let clone = file.try_clone()?;
125 Self::lock_with_retry(&clone, mode)?;
126 Ok(Self { file: clone, mode })
127 }
128
129 fn lock_with_retry(file: &File, mode: LockMode) -> Result<()> {
130 const MAX_ATTEMPTS: u32 = 200; const BACKOFF: Duration = Duration::from_millis(50);
132 let mut attempts = 0;
133 loop {
134 let result = match mode {
135 LockMode::None => return Ok(()),
136 LockMode::Exclusive => file.try_lock_exclusive(),
137 LockMode::Shared => FileExt::try_lock_shared(file),
138 };
139 match result {
140 Ok(()) => return Ok(()),
141 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue,
142 Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
143 if attempts >= MAX_ATTEMPTS {
144 return Err(MemvidError::Lock(
145 "exclusive access unavailable; file is in use by another process"
146 .to_string(),
147 ));
148 }
149 attempts += 1;
150 thread::sleep(BACKOFF);
151 continue;
152 }
153 Err(err) => return Err(MemvidError::Lock(err.to_string())),
154 }
155 }
156 }
157}
158
159impl Drop for FileLock {
160 fn drop(&mut self) {
161 if self.mode != LockMode::None {
162 let _ = self.file.unlock();
163 }
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use std::io::Write;
171 use tempfile::NamedTempFile;
172
173 #[test]
174 #[cfg(not(target_os = "windows"))] fn acquiring_lock_blocks_second_writer() {
176 let temp = NamedTempFile::new().expect("temp file");
177 let path = temp.path();
178 writeln!(&mut temp.as_file().try_clone().unwrap(), "seed").unwrap();
179
180 let file = OpenOptions::new()
181 .read(true)
182 .write(true)
183 .open(path)
184 .expect("open file");
185 let guard = FileLock::acquire(&file, path).expect("first lock succeeds");
186
187 let second = FileLock::try_acquire(&file, path).expect("second lock attempt");
188 assert!(second.is_none(), "lock should already be held");
189
190 drop(guard);
191 let third = FileLock::try_acquire(&file, path).expect("third lock attempt");
192 assert!(third.is_some(), "lock released after drop");
193 }
194}