1use std::fs::{File, OpenOptions};
8use std::io;
9use std::path::{Path, PathBuf};
10
11use sys::*;
12
13#[derive(Debug)]
20pub struct FileLock {
21 f: Option<File>,
22 path: PathBuf,
23}
24
25impl Drop for FileLock {
26 fn drop(&mut self) {
27 if let Some(f) = self.f.take() {
28 if let Err(e) = unlock(&f) {
29 eprintln!("failed to release lock {}: {e:?}", self.path.display());
30 }
31 }
32 }
33}
34
35fn open(path: &Path, opts: &OpenOptions, create: bool) -> io::Result<File> {
36 let f = opts.open(path).or_else(|e| {
37 if e.kind() == io::ErrorKind::NotFound && create {
41 std::fs::create_dir_all(path.parent().unwrap())?;
42 Ok(opts.open(path)?)
43 } else {
44 Err(e)
45 }
46 })?;
47 Ok(f)
48}
49
50pub fn open_rw_exclusive_create<P>(path: P, msg: &str) -> io::Result<FileLock>
51where
52 P: AsRef<Path>,
53{
54 let mut opts = OpenOptions::new();
55 let path = path.as_ref();
56 opts.read(true).write(true).create(true);
57 let f = open(path, &opts, true)?;
58 acquire(msg, path, &|| try_lock_exclusive(&f), &|| {
59 lock_exclusive(&f)
60 })?;
61 Ok(FileLock {
62 f: Some(f),
63 path: path.to_owned(),
64 })
65}
66
67pub fn open_ro_shared_create<P: AsRef<Path>>(path: P, msg: &str) -> std::io::Result<FileLock> {
68 let mut opts = OpenOptions::new();
69 let path = path.as_ref();
70 opts.read(true).write(true).create(true);
71 let f = open(path, &opts, true)?;
72 acquire(msg, path, &|| try_lock_shared(&f), &|| lock_shared(&f))?;
73 Ok(FileLock {
74 f: Some(f),
75 path: path.to_owned(),
76 })
77}
78
79fn try_acquire(path: &Path, lock_try: &dyn Fn() -> io::Result<()>) -> std::io::Result<bool> {
80 if is_on_nfs_mount(path) {
91 eprintln!("{path:?} appears to be an NFS mount, not trying to lock");
92 return Ok(true);
93 }
94
95 match lock_try() {
96 Ok(()) => return Ok(true),
97
98 Err(e) if error_unsupported(&e) => return Ok(true),
102
103 Err(e) => {
104 if !error_contended(&e) {
105 return Err(e);
107 }
108 }
109 }
110 Ok(false)
111}
112
113fn acquire(
129 msg: &str,
130 path: &Path,
131 lock_try: &dyn Fn() -> io::Result<()>,
132 lock_block: &dyn Fn() -> io::Result<()>,
133) -> io::Result<()> {
134 if try_acquire(path, lock_try)? {
135 return Ok(());
136 }
137 eprintln!("Waiting for file lock on {}", msg);
138
139 lock_block()?;
140 Ok(())
141}
142
143#[cfg(all(target_os = "linux", not(target_env = "musl")))]
144fn is_on_nfs_mount(path: &Path) -> bool {
145 use std::ffi::CString;
146 use std::mem;
147 use std::os::unix::prelude::*;
148
149 let Ok(path) = CString::new(path.as_os_str().as_bytes()) else {
150 return false;
151 };
152
153 unsafe {
154 let mut buf: libc::statfs = mem::zeroed();
155 let r = libc::statfs(path.as_ptr(), &mut buf);
156
157 r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
158 }
159}
160
161#[cfg(any(not(target_os = "linux"), target_env = "musl"))]
162fn is_on_nfs_mount(_path: &Path) -> bool {
163 false
164}
165
166#[cfg(unix)]
167mod sys {
168 use std::fs::File;
169 use std::io::{Error, Result};
170 use std::os::unix::io::AsRawFd;
171
172 #[cfg(not(target_os = "solaris"))]
173 const LOCK_SH: i32 = libc::LOCK_SH;
174 #[cfg(target_os = "solaris")]
175 const LOCK_SH: i32 = 1;
176 #[cfg(not(target_os = "solaris"))]
177 const LOCK_EX: i32 = libc::LOCK_EX;
178 #[cfg(target_os = "solaris")]
179 const LOCK_EX: i32 = 2;
180 #[cfg(not(target_os = "solaris"))]
181 const LOCK_NB: i32 = libc::LOCK_NB;
182 #[cfg(target_os = "solaris")]
183 const LOCK_NB: i32 = 4;
184 #[cfg(not(target_os = "solaris"))]
185 const LOCK_UN: i32 = libc::LOCK_UN;
186 #[cfg(target_os = "solaris")]
187 const LOCK_UN: i32 = 8;
188
189 pub(super) fn lock_shared(file: &File) -> Result<()> {
190 flock(file, LOCK_SH)
191 }
192
193 pub(super) fn lock_exclusive(file: &File) -> Result<()> {
194 flock(file, LOCK_EX)
195 }
196
197 pub(super) fn try_lock_shared(file: &File) -> Result<()> {
198 flock(file, LOCK_SH | LOCK_NB)
199 }
200
201 pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
202 flock(file, LOCK_EX | LOCK_NB)
203 }
204
205 pub(super) fn unlock(file: &File) -> Result<()> {
206 flock(file, LOCK_UN)
207 }
208
209 pub(super) fn error_contended(err: &Error) -> bool {
210 err.raw_os_error() == Some(libc::EWOULDBLOCK)
211 }
212
213 pub(super) fn error_unsupported(err: &Error) -> bool {
214 match err.raw_os_error() {
215 #[allow(unreachable_patterns)]
218 Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
219 Some(libc::ENOSYS) => true,
220 _ => false,
221 }
222 }
223
224 #[cfg(not(target_os = "solaris"))]
225 fn flock(file: &File, flag: libc::c_int) -> Result<()> {
226 let ret = unsafe { libc::flock(file.as_raw_fd(), flag) };
227 if ret < 0 {
228 Err(Error::last_os_error())
229 } else {
230 Ok(())
231 }
232 }
233
234 #[cfg(target_os = "solaris")]
235 fn flock(file: &File, flag: libc::c_int) -> Result<()> {
236 let mut flock = libc::flock {
238 l_type: 0,
239 l_whence: 0,
240 l_start: 0,
241 l_len: 0,
242 l_sysid: 0,
243 l_pid: 0,
244 l_pad: [0, 0, 0, 0],
245 };
246 flock.l_type = if flag & LOCK_UN != 0 {
247 libc::F_UNLCK
248 } else if flag & LOCK_EX != 0 {
249 libc::F_WRLCK
250 } else if flag & LOCK_SH != 0 {
251 libc::F_RDLCK
252 } else {
253 panic!("unexpected flock() operation")
254 };
255
256 let mut cmd = libc::F_SETLKW;
257 if (flag & LOCK_NB) != 0 {
258 cmd = libc::F_SETLK;
259 }
260
261 let ret = unsafe { libc::fcntl(file.as_raw_fd(), cmd, &flock) };
262
263 if ret < 0 {
264 Err(Error::last_os_error())
265 } else {
266 Ok(())
267 }
268 }
269}
270
271#[cfg(windows)]
272mod sys {
273 use std::fs::File;
274 use std::io::{Error, Result};
275 use std::mem;
276 use std::os::windows::io::AsRawHandle;
277
278 use windows_sys::Win32::Foundation::HANDLE;
279 use windows_sys::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_LOCK_VIOLATION};
280 use windows_sys::Win32::Storage::FileSystem::{
281 LockFileEx, UnlockFile, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
282 };
283
284 pub(super) fn lock_shared(file: &File) -> Result<()> {
285 lock_file(file, 0)
286 }
287
288 pub(super) fn lock_exclusive(file: &File) -> Result<()> {
289 lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)
290 }
291
292 pub(super) fn try_lock_shared(file: &File) -> Result<()> {
293 lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)
294 }
295
296 pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
297 lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)
298 }
299
300 pub(super) fn error_contended(err: &Error) -> bool {
301 err.raw_os_error()
302 .map_or(false, |x| x == ERROR_LOCK_VIOLATION as i32)
303 }
304
305 pub(super) fn error_unsupported(err: &Error) -> bool {
306 err.raw_os_error()
307 .map_or(false, |x| x == ERROR_INVALID_FUNCTION as i32)
308 }
309
310 pub(super) fn unlock(file: &File) -> Result<()> {
311 unsafe {
312 let ret = UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, !0, !0);
313 if ret == 0 {
314 Err(Error::last_os_error())
315 } else {
316 Ok(())
317 }
318 }
319 }
320
321 fn lock_file(file: &File, flags: u32) -> Result<()> {
322 unsafe {
323 let mut overlapped = mem::zeroed();
324 let ret = LockFileEx(
325 file.as_raw_handle() as HANDLE,
326 flags,
327 0,
328 !0,
329 !0,
330 &mut overlapped,
331 );
332 if ret == 0 {
333 Err(Error::last_os_error())
334 } else {
335 Ok(())
336 }
337 }
338 }
339}