Skip to content

Commit 4ab9cde

Browse files
committed
fixed locking on windows
1 parent 6c0a973 commit 4ab9cde

File tree

2 files changed

+125
-40
lines changed

2 files changed

+125
-40
lines changed

tmc-langs-framework/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ tempfile = "3"
2323
anyhow = "1"
2424
fd-lock = "2"
2525

26+
[target.'cfg(windows)'.dependencies]
27+
winapi = "0.3"
28+
2629
[dev-dependencies]
2730
tempfile = "3"
2831
mockall = "0.9"
Lines changed: 122 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1-
//! File locking utilities on Unix platforms.
1+
//! File locking utilities on Windows.
2+
//!
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(?).
26
37
use crate::error::FileIo;
48
use crate::file_util::*;
59
use fd_lock::{FdLock, FdLockGuard};
6-
use std::fs::File;
10+
use std::fs::OpenOptions;
11+
use std::os::windows::fs::OpenOptionsExt;
712
use std::path::PathBuf;
13+
use winapi::{
14+
shared::winerror::ERROR_SHARING_VIOLATION,
15+
um::{winbase::FILE_FLAG_BACKUP_SEMANTICS, winnt::GENERIC_READ},
16+
};
817

918
#[macro_export]
1019
macro_rules! lock {
@@ -24,43 +33,69 @@ pub use crate::lock;
2433
// TODO: should this be in file_util or in the frontend (CLI)?
2534
pub struct FileLock {
2635
path: PathBuf,
27-
fd_lock: FdLock<File>,
36+
// this is re-set in every lock command if the target is a file
37+
// ideally it would be set to none when the guard is dropped, but doing so is probably not worth the trouble
38+
lock: Option<FdLock<File>>,
2839
}
2940

3041
impl FileLock {
3142
pub fn new(path: PathBuf) -> Result<FileLock, FileIo> {
32-
let file = open_file(&path)?;
33-
Ok(Self {
34-
path,
35-
fd_lock: FdLock::new(file),
36-
})
43+
Ok(Self { path, lock: None })
3744
}
3845

3946
/// Blocks until the lock can be acquired.
47+
/// On Windows, directories cannot be locked, so we use a lock file instead.
4048
pub fn lock(&mut self) -> Result<FileLockGuard, FileIo> {
4149
log::debug!("locking {}", self.path.display());
42-
let path = &self.path;
43-
let fd_lock = &mut self.fd_lock;
44-
let guard = fd_lock
45-
.lock()
46-
.map_err(|e| FileIo::FdLock(path.clone(), e))?;
47-
log::debug!("locked {}", self.path.display());
48-
Ok(FileLockGuard {
49-
path,
50-
_guard: guard,
51-
})
50+
51+
if self.path.is_file() {
52+
// for files, just use the path
53+
let file = open_file(&self.path)?;
54+
let lock = FdLock::new(file);
55+
self.lock = Some(lock);
56+
let guard = self.lock.as_mut().unwrap().lock().unwrap();
57+
Ok(FileLockGuard::File(guard, &self.path))
58+
} else if self.path.is_dir() {
59+
// for directories, we'll continuously try opening it in exclusive access mode
60+
loop {
61+
// try to create lock file
62+
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)
67+
{
68+
Ok(file) => return Ok(FileLockGuard::Dir(file, &self.path)), // succeeded in "locking" the dir
69+
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));
75+
} else {
76+
todo!()
77+
}
78+
}
79+
}
80+
}
81+
} else {
82+
panic!("invalid path");
83+
}
5284
}
5385
}
5486

55-
#[derive(Debug)]
56-
pub struct FileLockGuard<'a> {
57-
path: &'a Path,
58-
_guard: FdLockGuard<'a, File>,
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
5990
}
6091

6192
impl Drop for FileLockGuard<'_> {
6293
fn drop(&mut self) {
63-
log::debug!("unlocking {}", self.path.display());
94+
let path = match self {
95+
Self::File(_, path) => path,
96+
Self::Dir(_, path) => path,
97+
};
98+
log::debug!("unlocking {}", path.display());
6499
}
65100
}
66101

@@ -84,24 +119,32 @@ mod test {
84119
let temp = NamedTempFile::new().unwrap();
85120
let temp_path = temp.path();
86121
let mut lock = FileLock::new(temp_path.to_path_buf()).unwrap();
87-
let guard = lock.lock().unwrap();
122+
let mutex = Arc::new(Mutex::new(vec![]));
88123

89-
let refcell = std::cell::RefCell::new(vec![]);
124+
// take file lock and then mutex
125+
let guard = lock.lock().unwrap();
126+
let mut mguard = mutex.try_lock().unwrap();
90127

91128
let handle = {
92129
let temp_path = temp_path.to_path_buf();
93-
let refcell = refcell.clone();
130+
let mutex = mutex.clone();
94131

95132
std::thread::spawn(move || {
133+
// if the file lock doesn't block, the mutex lock will panic and the test will fail
96134
let mut lock = FileLock::new(temp_path).unwrap();
97135
let _guard = lock.lock().unwrap();
98-
refcell.borrow_mut().push(1);
136+
mutex.try_lock().unwrap().push(1);
99137
})
100138
};
101139

102-
std::thread::sleep(std::time::Duration::from_millis(100));
103-
refcell.borrow_mut().push(1);
140+
// sleep while holding the lock to let the thread execute
141+
std::thread::sleep(std::time::Duration::from_millis(200));
142+
mguard.push(1);
143+
144+
// release locks and allow the thread to proceed
145+
drop(mguard);
104146
drop(guard);
147+
// wait for thread, if it panicked, it tried to lock the mutex without the file lock
105148
handle.join().unwrap();
106149
}
107150

@@ -112,31 +155,70 @@ mod test {
112155
let temp = tempfile::tempdir().unwrap();
113156
let temp_path = temp.path();
114157
let mut lock = FileLock::new(temp_path.to_path_buf()).unwrap();
115-
let refcell = Arc::new(Mutex::new(vec![]));
158+
let mutex = Arc::new(Mutex::new(vec![]));
116159

117-
// take file lock and then refcell
160+
// take file lock and mutex
118161
let guard = lock.lock().unwrap();
119-
let mut refmut = refcell.lock().unwrap();
162+
let mut mguard = mutex.try_lock().unwrap();
120163

121164
let handle = {
122165
let temp_path = temp_path.to_path_buf();
123-
let refcell = refcell.clone();
166+
let mutex = mutex.clone();
124167

125168
std::thread::spawn(move || {
169+
// if the file lock doesn't block, the mutex lock will panic and the test will fail
126170
let mut lock = FileLock::new(temp_path).unwrap();
127-
// block on file lock and use refcell
128171
let _guard = lock.lock().unwrap();
129-
refcell.lock().unwrap().push(1);
172+
mutex.try_lock().unwrap().push(1);
130173
})
131174
};
132175

133-
// wait for the other thread to actually lock
134-
std::thread::sleep(std::time::Duration::from_millis(100));
135-
refmut.push(1);
176+
// release locks and allow the thread to proceed
177+
std::thread::sleep(std::time::Duration::from_millis(200));
178+
mguard.push(1);
136179

137-
// drop refcell borrow then file lock
138-
drop(refmut);
180+
// release locks and allow the thread to proceed
181+
drop(mguard);
139182
drop(guard);
183+
// wait for thread, if it panicked, it tried to lock the mutex without the file lock
140184
handle.join().unwrap();
141185
}
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+
}
142224
}

0 commit comments

Comments
 (0)