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
37use crate :: error:: FileIo ;
48use crate :: file_util:: * ;
59use fd_lock:: { FdLock , FdLockGuard } ;
6- use std:: fs:: File ;
10+ use std:: fs:: OpenOptions ;
11+ use std:: os:: windows:: fs:: OpenOptionsExt ;
712use 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]
1019macro_rules! lock {
@@ -24,43 +33,69 @@ pub use crate::lock;
2433// TODO: should this be in file_util or in the frontend (CLI)?
2534pub 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
3041impl 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
6192impl 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