1use self::sys::*;
4use anyhow::{anyhow, Context, Result};
5use std::fs::{File, OpenOptions};
6use std::io;
7use std::io::{Read, Seek, SeekFrom, Write};
8use std::path::{Path, PathBuf};
9
10#[derive(Debug)]
14pub struct FileLock {
15 file: File,
16 path: PathBuf,
17}
18
19#[derive(Debug, Copy, Clone, Eq, PartialEq)]
20enum Access {
21 Shared,
22 Exclusive,
23}
24
25impl FileLock {
26 pub fn try_open_rw(path: impl Into<PathBuf>) -> Result<Option<Self>> {
38 Self::open(
39 path.into(),
40 OpenOptions::new().read(true).write(true).create(true),
41 Access::Exclusive,
42 true,
43 )
44 }
45
46 pub fn open_rw(path: impl Into<PathBuf>) -> Result<Self> {
59 Ok(Self::open(
60 path.into(),
61 OpenOptions::new().read(true).write(true).create(true),
62 Access::Exclusive,
63 false,
64 )?
65 .unwrap())
66 }
67
68 pub fn try_open_ro(path: impl Into<PathBuf>) -> Result<Option<Self>> {
80 Self::open(
81 path.into(),
82 OpenOptions::new().read(true),
83 Access::Shared,
84 true,
85 )
86 }
87
88 pub fn open_ro(path: impl Into<PathBuf>) -> Result<Self> {
100 Ok(Self::open(
101 path.into(),
102 OpenOptions::new().read(true),
103 Access::Shared,
104 false,
105 )?
106 .unwrap())
107 }
108
109 fn open(
110 path: PathBuf,
111 opts: &OpenOptions,
112 access: Access,
113 try_lock: bool,
114 ) -> Result<Option<Self>> {
115 let file = opts
119 .open(&path)
120 .or_else(|e| {
121 if e.kind() == io::ErrorKind::NotFound && access == Access::Exclusive {
122 std::fs::create_dir_all(path.parent().unwrap())?;
123 Ok(opts.open(&path)?)
124 } else {
125 Err(anyhow::Error::from(e))
126 }
127 })
128 .with_context(|| format!("failed to open `{path}`", path = path.display()))?;
129
130 let lock = Self { file, path };
131
132 if is_on_nfs_mount(&lock.path) {
143 return Ok(Some(lock));
144 }
145
146 let res = match (access, try_lock) {
147 (Access::Shared, true) => try_lock_shared(&lock.file),
148 (Access::Exclusive, true) => try_lock_exclusive(&lock.file),
149 (Access::Shared, false) => lock_shared(&lock.file),
150 (Access::Exclusive, false) => lock_exclusive(&lock.file),
151 };
152
153 return match res {
154 Ok(_) => Ok(Some(lock)),
155
156 Err(e) if error_unsupported(&e) => Ok(Some(lock)),
160
161 Err(e) if try_lock && error_contended(&e) => Ok(None),
163
164 Err(e) => Err(anyhow!(e).context(format!(
165 "failed to lock file `{path}`",
166 path = lock.path.display()
167 ))),
168 };
169
170 #[cfg(all(target_os = "linux", not(target_env = "musl")))]
171 fn is_on_nfs_mount(path: &Path) -> bool {
172 use std::ffi::CString;
173 use std::mem;
174 use std::os::unix::prelude::*;
175
176 let path = match CString::new(path.as_os_str().as_bytes()) {
177 Ok(path) => path,
178 Err(_) => return false,
179 };
180
181 unsafe {
182 let mut buf: libc::statfs = mem::zeroed();
183 let r = libc::statfs(path.as_ptr(), &mut buf);
184
185 r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
186 }
187 }
188
189 #[cfg(any(not(target_os = "linux"), target_env = "musl"))]
190 fn is_on_nfs_mount(_path: &Path) -> bool {
191 false
192 }
193 }
194
195 pub fn file(&self) -> &File {
197 &self.file
198 }
199
200 pub fn path(&self) -> &Path {
205 &self.path
206 }
207
208 pub fn parent(&self) -> &Path {
210 self.path.parent().unwrap()
211 }
212}
213
214impl Read for FileLock {
215 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
216 self.file().read(buf)
217 }
218}
219
220impl Seek for FileLock {
221 fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
222 self.file().seek(to)
223 }
224}
225
226impl Write for FileLock {
227 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
228 self.file().write(buf)
229 }
230
231 fn flush(&mut self) -> io::Result<()> {
232 self.file().flush()
233 }
234}
235
236impl Drop for FileLock {
237 fn drop(&mut self) {
238 let _ = unlock(&self.file);
239 }
240}
241
242#[cfg(unix)]
243mod sys {
244 use std::fs::File;
245 use std::io::{Error, Result};
246 use std::os::unix::io::AsRawFd;
247
248 pub(super) fn lock_shared(file: &File) -> Result<()> {
249 flock(file, libc::LOCK_SH)
250 }
251
252 pub(super) fn lock_exclusive(file: &File) -> Result<()> {
253 flock(file, libc::LOCK_EX)
254 }
255
256 pub(super) fn try_lock_shared(file: &File) -> Result<()> {
257 flock(file, libc::LOCK_SH | libc::LOCK_NB)
258 }
259
260 pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
261 flock(file, libc::LOCK_EX | libc::LOCK_NB)
262 }
263
264 pub(super) fn unlock(file: &File) -> Result<()> {
265 flock(file, libc::LOCK_UN)
266 }
267
268 pub(super) fn error_contended(err: &Error) -> bool {
269 err.raw_os_error().map_or(false, |x| x == libc::EWOULDBLOCK)
270 }
271
272 pub(super) fn error_unsupported(err: &Error) -> bool {
273 match err.raw_os_error() {
274 #[allow(unreachable_patterns)]
277 Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
278 Some(libc::ENOSYS) => true,
279 _ => false,
280 }
281 }
282
283 #[cfg(not(target_os = "solaris"))]
284 fn flock(file: &File, flag: libc::c_int) -> Result<()> {
285 let ret = unsafe { libc::flock(file.as_raw_fd(), flag) };
286 if ret < 0 {
287 Err(Error::last_os_error())
288 } else {
289 Ok(())
290 }
291 }
292
293 #[cfg(target_os = "solaris")]
294 fn flock(file: &File, flag: libc::c_int) -> Result<()> {
295 let mut flock = libc::flock {
297 l_type: 0,
298 l_whence: 0,
299 l_start: 0,
300 l_len: 0,
301 l_sysid: 0,
302 l_pid: 0,
303 l_pad: [0, 0, 0, 0],
304 };
305 flock.l_type = if flag & libc::LOCK_UN != 0 {
306 libc::F_UNLCK
307 } else if flag & libc::LOCK_EX != 0 {
308 libc::F_WRLCK
309 } else if flag & libc::LOCK_SH != 0 {
310 libc::F_RDLCK
311 } else {
312 panic!("unexpected flock() operation")
313 };
314
315 let mut cmd = libc::F_SETLKW;
316 if (flag & libc::LOCK_NB) != 0 {
317 cmd = libc::F_SETLK;
318 }
319
320 let ret = unsafe { libc::fcntl(file.as_raw_fd(), cmd, &flock) };
321
322 if ret < 0 {
323 Err(Error::last_os_error())
324 } else {
325 Ok(())
326 }
327 }
328}
329
330#[cfg(windows)]
331mod sys {
332 use std::fs::File;
333 use std::io::{Error, Result};
334 use std::mem;
335 use std::os::windows::io::AsRawHandle;
336
337 use windows_sys::Win32::Foundation::HANDLE;
338 use windows_sys::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_LOCK_VIOLATION};
339 use windows_sys::Win32::Storage::FileSystem::{
340 LockFileEx, UnlockFile, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
341 };
342
343 pub(super) fn lock_shared(file: &File) -> Result<()> {
344 lock_file(file, 0)
345 }
346
347 pub(super) fn lock_exclusive(file: &File) -> Result<()> {
348 lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)
349 }
350
351 pub(super) fn try_lock_shared(file: &File) -> Result<()> {
352 lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)
353 }
354
355 pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
356 lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)
357 }
358
359 pub(super) fn error_contended(err: &Error) -> bool {
360 err.raw_os_error()
361 .map_or(false, |x| x == ERROR_LOCK_VIOLATION as i32)
362 }
363
364 pub(super) fn error_unsupported(err: &Error) -> bool {
365 err.raw_os_error()
366 .map_or(false, |x| x == ERROR_INVALID_FUNCTION as i32)
367 }
368
369 pub(super) fn unlock(file: &File) -> Result<()> {
370 unsafe {
371 let ret = UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, !0, !0);
372 if ret == 0 {
373 Err(Error::last_os_error())
374 } else {
375 Ok(())
376 }
377 }
378 }
379
380 fn lock_file(file: &File, flags: u32) -> Result<()> {
381 unsafe {
382 let mut overlapped = mem::zeroed();
383 let ret = LockFileEx(
384 file.as_raw_handle() as HANDLE,
385 flags,
386 0,
387 !0,
388 !0,
389 &mut overlapped,
390 );
391 if ret == 0 {
392 Err(Error::last_os_error())
393 } else {
394 Ok(())
395 }
396 }
397 }
398}