1use std::path::Path;
15
16use crate::error::StoreResult;
17
18#[cfg(target_arch = "wasm32")]
21mod imp {
22 use super::*;
23
24 #[derive(Debug, Clone)]
26 pub struct Lock;
27
28 #[derive(Debug)]
30 pub struct LockGuard;
31
32 impl Lock {
33 pub fn open(_path: &Path) -> StoreResult<Self> {
35 Ok(Self)
36 }
37
38 pub fn lock(&self) -> StoreResult<LockGuard> {
40 Ok(LockGuard)
41 }
42
43 pub fn try_lock(&self) -> StoreResult<Option<LockGuard>> {
45 Ok(Some(LockGuard))
46 }
47 }
48}
49
50#[cfg(not(target_arch = "wasm32"))]
53mod imp {
54 use super::{Path, StoreResult};
55 use crate::error::StoreError;
56 use std::fs::{self, File, OpenOptions};
57 use std::sync::Arc;
58
59 #[derive(Debug, Clone)]
62 pub struct Lock {
63 file: Arc<File>,
64 }
65
66 #[derive(Debug)]
68 pub struct LockGuard {
69 file: Arc<File>,
70 }
71
72 impl Lock {
73 pub fn open(path: &Path) -> StoreResult<Self> {
79 if let Some(parent) = path.parent() {
80 fs::create_dir_all(parent).map_err(|err| map_io_err(&err))?;
81 }
82 let file = OpenOptions::new()
83 .read(true)
84 .write(true)
85 .create(true)
86 .truncate(false)
87 .open(path)
88 .map_err(|err| map_io_err(&err))?;
89 Ok(Self {
90 file: Arc::new(file),
91 })
92 }
93
94 pub fn lock(&self) -> StoreResult<LockGuard> {
100 lock_exclusive(&self.file).map_err(|err| map_io_err(&err))?;
101 Ok(LockGuard {
102 file: Arc::clone(&self.file),
103 })
104 }
105
106 pub fn try_lock(&self) -> StoreResult<Option<LockGuard>> {
113 if try_lock_exclusive(&self.file).map_err(|err| map_io_err(&err))? {
114 Ok(Some(LockGuard {
115 file: Arc::clone(&self.file),
116 }))
117 } else {
118 Ok(None)
119 }
120 }
121 }
122
123 impl Drop for LockGuard {
124 fn drop(&mut self) {
125 let _ = unlock(&self.file);
126 }
127 }
128
129 fn map_io_err(err: &std::io::Error) -> StoreError {
130 StoreError::Lock(err.to_string())
131 }
132
133 #[cfg(unix)]
136 fn lock_exclusive(file: &File) -> std::io::Result<()> {
137 let fd = std::os::unix::io::AsRawFd::as_raw_fd(file);
138 let result = unsafe { flock(fd, LOCK_EX) };
139 if result == 0 {
140 Ok(())
141 } else {
142 Err(std::io::Error::last_os_error())
143 }
144 }
145
146 #[cfg(unix)]
147 fn try_lock_exclusive(file: &File) -> std::io::Result<bool> {
148 let fd = std::os::unix::io::AsRawFd::as_raw_fd(file);
149 let result = unsafe { flock(fd, LOCK_EX | LOCK_NB) };
150 if result == 0 {
151 Ok(true)
152 } else {
153 let err = std::io::Error::last_os_error();
154 if err.kind() == std::io::ErrorKind::WouldBlock {
155 Ok(false)
156 } else {
157 Err(err)
158 }
159 }
160 }
161
162 #[cfg(unix)]
163 fn unlock(file: &File) -> std::io::Result<()> {
164 let fd = std::os::unix::io::AsRawFd::as_raw_fd(file);
165 let result = unsafe { flock(fd, LOCK_UN) };
166 if result == 0 {
167 Ok(())
168 } else {
169 Err(std::io::Error::last_os_error())
170 }
171 }
172
173 #[cfg(unix)]
174 use std::os::raw::c_int;
175
176 #[cfg(unix)]
177 const LOCK_EX: c_int = 2;
178 #[cfg(unix)]
179 const LOCK_NB: c_int = 4;
180 #[cfg(unix)]
181 const LOCK_UN: c_int = 8;
182
183 #[cfg(unix)]
184 extern "C" {
185 fn flock(fd: c_int, operation: c_int) -> c_int;
186 }
187
188 #[cfg(windows)]
191 fn lock_exclusive(file: &File) -> std::io::Result<()> {
192 lock_file(file, 0)
193 }
194
195 #[cfg(windows)]
196 fn try_lock_exclusive(file: &File) -> std::io::Result<bool> {
197 match lock_file(file, LOCKFILE_FAIL_IMMEDIATELY) {
198 Ok(()) => Ok(true),
199 Err(err) => {
200 if err.raw_os_error() == Some(ERROR_LOCK_VIOLATION) {
201 Ok(false)
202 } else {
203 Err(err)
204 }
205 }
206 }
207 }
208
209 #[cfg(windows)]
210 fn unlock(file: &File) -> std::io::Result<()> {
211 let handle = std::os::windows::io::AsRawHandle::as_raw_handle(file) as HANDLE;
212 let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
213 let result = unsafe { UnlockFileEx(handle, 0, 1, 0, &mut overlapped) };
214 if result != 0 {
215 Ok(())
216 } else {
217 Err(std::io::Error::last_os_error())
218 }
219 }
220
221 #[cfg(windows)]
222 fn lock_file(file: &File, flags: u32) -> std::io::Result<()> {
223 let handle = std::os::windows::io::AsRawHandle::as_raw_handle(file) as HANDLE;
224 let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
225 let result = unsafe {
226 LockFileEx(
227 handle,
228 LOCKFILE_EXCLUSIVE_LOCK | flags,
229 0,
230 1,
231 0,
232 &mut overlapped,
233 )
234 };
235 if result != 0 {
236 Ok(())
237 } else {
238 Err(std::io::Error::last_os_error())
239 }
240 }
241
242 #[cfg(windows)]
243 type HANDLE = *mut std::ffi::c_void;
244
245 #[cfg(windows)]
246 #[repr(C)]
247 struct OVERLAPPED {
248 internal: usize,
249 internal_high: usize,
250 offset: u32,
251 offset_high: u32,
252 h_event: HANDLE,
253 }
254
255 #[cfg(windows)]
256 const LOCKFILE_EXCLUSIVE_LOCK: u32 = 0x2;
257 #[cfg(windows)]
258 const LOCKFILE_FAIL_IMMEDIATELY: u32 = 0x1;
259 #[cfg(windows)]
260 const ERROR_LOCK_VIOLATION: i32 = 33;
261
262 #[cfg(windows)]
263 extern "system" {
264 fn LockFileEx(
265 h_file: HANDLE,
266 flags: u32,
267 reserved: u32,
268 bytes_to_lock_low: u32,
269 bytes_to_lock_high: u32,
270 overlapped: *mut OVERLAPPED,
271 ) -> i32;
272 fn UnlockFileEx(
273 h_file: HANDLE,
274 reserved: u32,
275 bytes_to_unlock_low: u32,
276 bytes_to_unlock_high: u32,
277 overlapped: *mut OVERLAPPED,
278 ) -> i32;
279 }
280
281 #[cfg(test)]
282 mod tests {
283 use super::Lock;
284
285 #[test]
286 fn test_lock_is_exclusive() {
287 let dir = tempfile::tempdir().expect("create temp dir");
288 let path = dir.path().join("lock.lock");
289 let lock_a = Lock::open(&path).expect("open lock");
290 let guard = lock_a.lock().expect("acquire lock");
291
292 let lock_b = Lock::open(&path).expect("open lock");
293 let blocked = lock_b.try_lock().expect("try lock");
294 assert!(blocked.is_none());
295
296 drop(guard);
297 let guard = lock_b.try_lock().expect("try lock");
298 assert!(guard.is_some());
299 }
300
301 #[test]
302 fn test_lock_serializes_across_threads() {
303 use std::sync::mpsc;
304 use std::thread;
305
306 let dir = tempfile::tempdir().expect("create temp dir");
307 let path = dir.path().join("lock.lock");
308 let lock = Lock::open(&path).expect("open lock");
309
310 let (locked_tx, locked_rx) = mpsc::channel();
311 let (release_tx, release_rx) = mpsc::channel();
312 let (released_tx, released_rx) = mpsc::channel();
313
314 let thread_a = thread::spawn(move || {
315 let guard = lock.lock().expect("lock in thread");
316 locked_tx.send(()).expect("signal locked");
317 release_rx.recv().expect("wait release");
318 drop(guard);
319 released_tx.send(()).expect("signal released");
320 });
321
322 locked_rx.recv().expect("wait locked");
323 let lock_b = Lock::open(&path).expect("open lock");
324 let blocked = lock_b.try_lock().expect("try lock");
325 assert!(blocked.is_none());
326
327 release_tx.send(()).expect("release");
328 released_rx.recv().expect("wait released");
329
330 let guard = lock_b.try_lock().expect("try lock");
331 assert!(guard.is_some());
332
333 thread_a.join().expect("thread join");
334 }
335 }
336}
337
338pub use imp::{Lock, LockGuard};