|
1 | 1 | //! File locking utilities on Windows. |
2 | 2 | //! |
3 | 3 | //! Windows directories can't be locked with fd-lock, so a different solution is needed. |
4 | | -//! Currently, regular files are locked with fd-lock, but directories are opened in exclusive mode. |
5 | | -//! This probably means the lock needs to be used with more care; deleting a locked directory is possible on Unix but not on Windows(?). |
| 4 | +//! Currently, regular files are locked with fd-lock, but for directories a .tmc.lock file is created. |
6 | 5 |
|
7 | 6 | use crate::error::FileIo; |
8 | 7 | use crate::file_util::*; |
9 | 8 | use fd_lock::{FdLock, FdLockGuard}; |
10 | | -use std::fs::OpenOptions; |
11 | | -use std::os::windows::fs::OpenOptionsExt; |
12 | 9 | use std::path::PathBuf; |
13 | | -use winapi::{ |
14 | | - shared::winerror::ERROR_SHARING_VIOLATION, |
15 | | - um::{winbase::FILE_FLAG_BACKUP_SEMANTICS, winnt::GENERIC_READ}, |
| 10 | +use std::{borrow::Cow, io::ErrorKind}; |
| 11 | +use std::{ |
| 12 | + fs::OpenOptions, |
| 13 | + time::{Duration, Instant}, |
16 | 14 | }; |
17 | 15 |
|
18 | 16 | #[macro_export] |
@@ -47,55 +45,90 @@ impl FileLock { |
47 | 45 | /// On Windows, directories cannot be locked, so we use a lock file instead. |
48 | 46 | pub fn lock(&mut self) -> Result<FileLockGuard, FileIo> { |
49 | 47 | log::debug!("locking {}", self.path.display()); |
| 48 | + let start_time = Instant::now(); |
| 49 | + let mut warning_timer = Instant::now(); |
50 | 50 |
|
51 | 51 | if self.path.is_file() { |
52 | 52 | // for files, just use the path |
53 | 53 | let file = open_file(&self.path)?; |
54 | 54 | let lock = FdLock::new(file); |
55 | 55 | self.lock = Some(lock); |
56 | | - let guard = self.lock.as_mut().unwrap().lock().unwrap(); |
57 | | - Ok(FileLockGuard::File(guard, &self.path)) |
| 56 | + let lock = self.lock.as_mut().unwrap(); |
| 57 | + let guard = lock.lock().unwrap(); |
| 58 | + Ok(FileLockGuard { |
| 59 | + _guard: guard, |
| 60 | + path: Cow::Borrowed(&self.path), |
| 61 | + is_lock_file: false, |
| 62 | + }) |
58 | 63 | } else if self.path.is_dir() { |
59 | | - // for directories, we'll continuously try opening it in exclusive access mode |
| 64 | + // for directories, we'll create/open a .tmc.lock file |
| 65 | + let lock_path = self.path.join(".tmc.lock"); |
60 | 66 | loop { |
61 | | - // try to create lock file |
| 67 | + // try to create a new lock file |
62 | 68 | match OpenOptions::new() |
63 | | - .access_mode(GENERIC_READ) |
64 | | - .share_mode(0) // exclusive access = fail if another process has the file open (locked) |
65 | | - .custom_flags(FILE_FLAG_BACKUP_SEMANTICS) |
66 | | - .open(&self.path) |
| 69 | + .write(true) |
| 70 | + .create_new(true) |
| 71 | + .open(&lock_path) |
67 | 72 | { |
68 | | - Ok(file) => return Ok(FileLockGuard::Dir(file, &self.path)), // succeeded in "locking" the dir |
| 73 | + Ok(file) => { |
| 74 | + // was able to create a new lock file |
| 75 | + let lock = FdLock::new(file); |
| 76 | + self.lock = Some(lock); |
| 77 | + let lock = self.lock.as_mut().unwrap(); |
| 78 | + let guard = lock.lock().unwrap(); |
| 79 | + return Ok(FileLockGuard { |
| 80 | + _guard: guard, |
| 81 | + path: Cow::Owned(lock_path), |
| 82 | + is_lock_file: true, |
| 83 | + }); |
| 84 | + } |
69 | 85 | Err(err) => { |
70 | | - let code = err.raw_os_error().unwrap(); |
71 | | - |
72 | | - if code as u32 == ERROR_SHARING_VIOLATION { |
73 | | - // file already opened in exclusive mode, wait for the other process |
74 | | - std::thread::sleep(std::time::Duration::from_secs(2)); |
| 86 | + if err.kind() == ErrorKind::AlreadyExists { |
| 87 | + // lock file already exists, let's wait a little and try again |
| 88 | + if start_time.elapsed() > Duration::from_secs(30) |
| 89 | + && warning_timer.elapsed() > Duration::from_secs(10) |
| 90 | + { |
| 91 | + warning_timer = Instant::now(); |
| 92 | + log::warn!( |
| 93 | + "The program has been waiting for lock file {} to be deleted for {} seconds, |
| 94 | + the lock file might have been left over from a previous run due to an error.", |
| 95 | + lock_path.display(), |
| 96 | + start_time.elapsed().as_secs() |
| 97 | + ); |
| 98 | + } |
| 99 | + std::thread::sleep(Duration::from_millis(500)); |
75 | 100 | } else { |
76 | | - todo!() |
| 101 | + // something else went wrong, propagate error |
| 102 | + return Err(FileIo::FileCreate(lock_path, err)); |
77 | 103 | } |
78 | 104 | } |
79 | 105 | } |
80 | 106 | } |
81 | 107 | } else { |
82 | | - panic!("invalid path"); |
| 108 | + return Err(FileIo::InvalidLockPath(self.path.to_path_buf())); |
83 | 109 | } |
84 | 110 | } |
85 | 111 | } |
86 | 112 |
|
87 | | -pub enum FileLockGuard<'a> { |
88 | | - File(FdLockGuard<'a, File>, &'a Path), // file locked with fd-lock |
89 | | - Dir(File, &'a Path), // directory opened in exclusive access mode |
| 113 | +pub struct FileLockGuard<'a> { |
| 114 | + _guard: FdLockGuard<'a, File>, |
| 115 | + path: Cow<'a, PathBuf>, |
| 116 | + is_lock_file: bool, |
90 | 117 | } |
91 | 118 |
|
92 | 119 | impl Drop for FileLockGuard<'_> { |
93 | 120 | fn drop(&mut self) { |
94 | | - let path = match self { |
95 | | - Self::File(_, path) => path, |
96 | | - Self::Dir(_, path) => path, |
97 | | - }; |
98 | | - log::debug!("unlocking {}", path.display()); |
| 121 | + log::debug!("unlocking {}", self.path.display()); |
| 122 | + if self.is_lock_file { |
| 123 | + log::debug!("removing lock file"); |
| 124 | + if let Err(err) = remove_file(self.path.as_ref()) { |
| 125 | + log::error!( |
| 126 | + "failed to remove lock file at {}: {}", |
| 127 | + self.path.display(), |
| 128 | + err |
| 129 | + ); |
| 130 | + } |
| 131 | + } |
99 | 132 | } |
100 | 133 | } |
101 | 134 |
|
@@ -183,42 +216,4 @@ mod test { |
183 | 216 | // wait for thread, if it panicked, it tried to lock the mutex without the file lock |
184 | 217 | handle.join().unwrap(); |
185 | 218 | } |
186 | | - |
187 | | - /// on windows locking the directory means we open the directory in exclusive mode |
188 | | - /// this test is just to make sure it doesn't matter for the files inside the dir |
189 | | - #[test] |
190 | | - fn locking_dir_doesnt_lock_files() { |
191 | | - init(); |
192 | | - |
193 | | - let temp = tempfile::tempdir().unwrap(); |
194 | | - let temp_path = temp.path(); |
195 | | - let file_path = temp_path.join("some file"); |
196 | | - std::fs::write(&file_path, "some contents").unwrap(); |
197 | | - let mut lock = FileLock::new(temp_path.to_path_buf()).unwrap(); |
198 | | - let mutex = Arc::new(Mutex::new(vec![])); |
199 | | - |
200 | | - // take file lock and mutex |
201 | | - let guard = lock.lock().unwrap(); |
202 | | - let mut mguard = mutex.try_lock().unwrap(); |
203 | | - |
204 | | - let handle = { |
205 | | - let file_path = file_path.clone(); |
206 | | - std::thread::spawn(move || { |
207 | | - // we try to rewrite the file from another thread |
208 | | - std::fs::write(file_path, "new contents").unwrap(); |
209 | | - }) |
210 | | - }; |
211 | | - |
212 | | - // release locks and allow the thread to proceed |
213 | | - std::thread::sleep(std::time::Duration::from_millis(200)); |
214 | | - mguard.push(1); |
215 | | - |
216 | | - // release locks and allow the thread to proceed |
217 | | - drop(mguard); |
218 | | - drop(guard); |
219 | | - // wait for thread, if it panicked, it tried to lock the mutex without the file lock |
220 | | - handle.join().unwrap(); |
221 | | - |
222 | | - assert_eq!("new contents", read_file_to_string(file_path).unwrap()); |
223 | | - } |
224 | 219 | } |
0 commit comments