1use std::fs::{File, OpenOptions};
11use std::io;
12use std::io::{Read, Seek, SeekFrom, Write};
13use std::path::{Display, Path, PathBuf};
14
15use anyhow::Context as _;
16use sys::*;
17use crate::shell::{Shell, style};
18
19#[derive(Debug)]
32pub struct FileLock {
33 f: Option<File>,
34 path: PathBuf,
35}
36
37impl FileLock {
38 pub fn file(&self) -> &File {
40 self.f.as_ref().unwrap()
41 }
42
43 pub fn path(&self) -> &Path {
48 &self.path
49 }
50
51 pub fn parent(&self) -> &Path {
53 self.path.parent().unwrap()
54 }
55
56 pub fn remove_siblings(&self) -> anyhow::Result<()> {
61 let path = self.path();
62 for entry in path.parent().unwrap().read_dir()? {
63 let entry = entry?;
64 if Some(&entry.file_name()[..]) == path.file_name() {
65 continue;
66 }
67 let kind = entry.file_type()?;
68 if kind.is_dir() {
69 fs_err::remove_dir_all(entry.path())?;
70 } else {
71 fs_err::remove_file(entry.path())?;
72 }
73 }
74 Ok(())
75 }
76}
77
78impl Read for FileLock {
79 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
80 self.file().read(buf)
81 }
82}
83
84impl Seek for FileLock {
85 fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
86 self.file().seek(to)
87 }
88}
89
90impl Write for FileLock {
91 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
92 self.file().write(buf)
93 }
94
95 fn flush(&mut self) -> io::Result<()> {
96 self.file().flush()
97 }
98}
99
100impl Drop for FileLock {
101 fn drop(&mut self) {
102 if let Some(f) = self.f.take() {
103 if let Err(e) = unlock(&f) {
104 tracing::warn!("failed to release lock: {e:?}");
105 }
106 }
107 }
108}
109
110#[derive(Clone, Debug)]
143pub struct Filesystem {
144 root: PathBuf,
145}
146
147impl Filesystem {
148 pub fn new(path: PathBuf) -> Filesystem {
150 Filesystem { root: path }
151 }
152
153 pub fn join<T: AsRef<Path>>(&self, other: T) -> Filesystem {
156 Filesystem::new(self.root.join(other))
157 }
158
159 pub fn push<T: AsRef<Path>>(&mut self, other: T) {
161 self.root.push(other);
162 }
163
164 pub fn into_path_unlocked(self) -> PathBuf {
169 self.root
170 }
171
172 pub fn as_path_unlocked(&self) -> &Path {
177 &self.root
178 }
179
180 pub fn create_dir(&self) -> anyhow::Result<()> {
185 fs_err::create_dir_all(&self.root).map_err(anyhow::Error::from)
186 }
187
188 pub fn display(&self) -> Display<'_> {
191 self.root.display()
192 }
193
194 pub fn open_rw_exclusive_create<P>(
205 &self,
206 path: P,
207 shell: &mut Shell,
208 msg: &str,
209 ) -> anyhow::Result<FileLock>
210 where
211 P: AsRef<Path>,
212 {
213 let mut opts = OpenOptions::new();
214 opts.read(true).write(true).truncate(true).create(true);
215 let (path, f) = self.open(path.as_ref(), &opts, true)?;
216 acquire(shell, msg, &path, &|| try_lock_exclusive(&f), &|| {
217 lock_exclusive(&f)
218 })?;
219 Ok(FileLock { f: Some(f), path })
220 }
221
222 pub fn try_open_rw_exclusive_create<P: AsRef<Path>>(
227 &self,
228 path: P,
229 ) -> anyhow::Result<Option<FileLock>> {
230 let mut opts = OpenOptions::new();
231 opts.read(true).write(true).truncate(true).create(true);
232 let (path, f) = self.open(path.as_ref(), &opts, true)?;
233 if try_acquire(&path, &|| try_lock_exclusive(&f))? {
234 Ok(Some(FileLock { f: Some(f), path }))
235 } else {
236 Ok(None)
237 }
238 }
239
240 pub fn open_ro_shared<P>(&self, path: P, shell: &mut Shell, msg: &str) -> anyhow::Result<FileLock>
250 where
251 P: AsRef<Path>,
252 {
253 let (path, f) = self.open(path.as_ref(), &OpenOptions::new().read(true), false)?;
254 acquire(shell, msg, &path, &|| try_lock_shared(&f), &|| {
255 lock_shared(&f)
256 })?;
257 Ok(FileLock { f: Some(f), path })
258 }
259
260 pub fn open_ro_shared_create<P: AsRef<Path>>(
266 &self,
267 path: P,
268 shell: &mut Shell,
269 msg: &str,
270 ) -> anyhow::Result<FileLock> {
271 let mut opts = OpenOptions::new();
272 opts.read(true).write(true).create(true);
273 let (path, f) = self.open(path.as_ref(), &opts, true)?;
274 acquire(shell, msg, &path, &|| try_lock_shared(&f), &|| {
275 lock_shared(&f)
276 })?;
277 Ok(FileLock { f: Some(f), path })
278 }
279
280 pub fn try_open_ro_shared_create<P: AsRef<Path>>(
285 &self,
286 path: P,
287 ) -> anyhow::Result<Option<FileLock>> {
288 let mut opts = OpenOptions::new();
289 opts.read(true).write(true).create(true);
290 let (path, f) = self.open(path.as_ref(), &opts, true)?;
291 if try_acquire(&path, &|| try_lock_shared(&f))? {
292 Ok(Some(FileLock { f: Some(f), path }))
293 } else {
294 Ok(None)
295 }
296 }
297
298 fn open(
299 &self,
300 path: &Path,
301 opts: &OpenOptions,
302 create: bool,
303 ) -> anyhow::Result<(PathBuf, File)> {
304 let path = self.root.join(path);
305 let f = opts
306 .open(&path)
307 .or_else(|e| {
308 if e.kind() == io::ErrorKind::NotFound && create {
312 fs_err::create_dir_all(path.parent().unwrap())?;
313 Ok(opts.open(&path)?)
314 } else {
315 Err(anyhow::Error::from(e))
316 }
317 })
318 .with_context(|| format!("failed to open: {}", path.display()))?;
319 Ok((path, f))
320 }
321}
322
323impl PartialEq<Path> for Filesystem {
324 fn eq(&self, other: &Path) -> bool {
325 self.root == other
326 }
327}
328
329impl PartialEq<Filesystem> for Path {
330 fn eq(&self, other: &Filesystem) -> bool {
331 self == other.root
332 }
333}
334
335fn try_acquire(path: &Path, lock_try: &dyn Fn() -> io::Result<()>) -> anyhow::Result<bool> {
336 if is_on_nfs_mount(path) {
347 tracing::debug!("{path:?} appears to be an NFS mount, not trying to lock");
348 return Ok(true);
349 }
350
351 match lock_try() {
352 Ok(()) => return Ok(true),
353
354 Err(e) if error_unsupported(&e) => return Ok(true),
358
359 Err(e) => {
360 if !error_contended(&e) {
361 let e = anyhow::Error::from(e);
362 let cx = format!("failed to lock file: {}", path.display());
363 return Err(e.context(cx));
364 }
365 }
366 }
367 Ok(false)
368}
369
370fn acquire(
386 shell: &mut Shell,
387 msg: &str,
388 path: &Path,
389 lock_try: &dyn Fn() -> io::Result<()>,
390 lock_block: &dyn Fn() -> io::Result<()>,
391) -> anyhow::Result<()> {
392 if try_acquire(path, lock_try)? {
393 return Ok(());
394 }
395 let msg = format!("waiting for file lock on {}", msg);
396 shell
397 .status_with_color("Blocking", &msg, &style::NOTE)?;
398
399 lock_block().with_context(|| format!("failed to lock file: {}", path.display()))?;
400 Ok(())
401}
402
403#[cfg(all(target_os = "linux", not(target_env = "musl")))]
404fn is_on_nfs_mount(path: &Path) -> bool {
405 use std::ffi::CString;
406 use std::mem;
407 use std::os::unix::prelude::*;
408
409 let Ok(path) = CString::new(path.as_os_str().as_bytes()) else {
410 return false;
411 };
412
413 unsafe {
414 let mut buf: libc::statfs = mem::zeroed();
415 let r = libc::statfs(path.as_ptr(), &mut buf);
416
417 r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
418 }
419}
420
421#[cfg(any(not(target_os = "linux"), target_env = "musl"))]
422fn is_on_nfs_mount(_path: &Path) -> bool {
423 false
424}
425
426#[cfg(unix)]
427mod sys {
428 use std::fs::File;
429 use std::io::{Error, Result};
430 use std::os::unix::io::AsRawFd;
431
432 pub(super) fn lock_shared(file: &File) -> Result<()> {
433 flock(file, libc::LOCK_SH)
434 }
435
436 pub(super) fn lock_exclusive(file: &File) -> Result<()> {
437 flock(file, libc::LOCK_EX)
438 }
439
440 pub(super) fn try_lock_shared(file: &File) -> Result<()> {
441 flock(file, libc::LOCK_SH | libc::LOCK_NB)
442 }
443
444 pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
445 flock(file, libc::LOCK_EX | libc::LOCK_NB)
446 }
447
448 pub(super) fn unlock(file: &File) -> Result<()> {
449 flock(file, libc::LOCK_UN)
450 }
451
452 pub(super) fn error_contended(err: &Error) -> bool {
453 err.raw_os_error().map_or(false, |x| x == libc::EWOULDBLOCK)
454 }
455
456 pub(super) fn error_unsupported(err: &Error) -> bool {
457 match err.raw_os_error() {
458 #[allow(unreachable_patterns)]
461 Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
462 Some(libc::ENOSYS) => true,
463 _ => false,
464 }
465 }
466
467 #[cfg(not(target_os = "solaris"))]
468 fn flock(file: &File, flag: libc::c_int) -> Result<()> {
469 let ret = unsafe { libc::flock(file.as_raw_fd(), flag) };
470 if ret < 0 {
471 Err(Error::last_os_error())
472 } else {
473 Ok(())
474 }
475 }
476
477 #[cfg(target_os = "solaris")]
478 fn flock(file: &File, flag: libc::c_int) -> Result<()> {
479 let mut flock = libc::flock {
481 l_type: 0,
482 l_whence: 0,
483 l_start: 0,
484 l_len: 0,
485 l_sysid: 0,
486 l_pid: 0,
487 l_pad: [0, 0, 0, 0],
488 };
489 flock.l_type = if flag & libc::LOCK_UN != 0 {
490 libc::F_UNLCK
491 } else if flag & libc::LOCK_EX != 0 {
492 libc::F_WRLCK
493 } else if flag & libc::LOCK_SH != 0 {
494 libc::F_RDLCK
495 } else {
496 panic!("unexpected flock() operation")
497 };
498
499 let mut cmd = libc::F_SETLKW;
500 if (flag & libc::LOCK_NB) != 0 {
501 cmd = libc::F_SETLK;
502 }
503
504 let ret = unsafe { libc::fcntl(file.as_raw_fd(), cmd, &flock) };
505
506 if ret < 0 {
507 Err(Error::last_os_error())
508 } else {
509 Ok(())
510 }
511 }
512}
513
514#[cfg(windows)]
515mod sys {
516 use std::fs::File;
517 use std::io::{Error, Result};
518 use std::mem;
519 use std::os::windows::io::AsRawHandle;
520
521 use windows_sys::Win32::Foundation::HANDLE;
522 use windows_sys::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_LOCK_VIOLATION};
523 use windows_sys::Win32::Storage::FileSystem::{
524 LockFileEx, UnlockFile, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
525 };
526
527 pub(super) fn lock_shared(file: &File) -> Result<()> {
528 lock_file(file, 0)
529 }
530
531 pub(super) fn lock_exclusive(file: &File) -> Result<()> {
532 lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)
533 }
534
535 pub(super) fn try_lock_shared(file: &File) -> Result<()> {
536 lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)
537 }
538
539 pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
540 lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)
541 }
542
543 pub(super) fn error_contended(err: &Error) -> bool {
544 err.raw_os_error()
545 .map_or(false, |x| x == ERROR_LOCK_VIOLATION as i32)
546 }
547
548 pub(super) fn error_unsupported(err: &Error) -> bool {
549 err.raw_os_error()
550 .map_or(false, |x| x == ERROR_INVALID_FUNCTION as i32)
551 }
552
553 pub(super) fn unlock(file: &File) -> Result<()> {
554 unsafe {
555 let ret = UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, !0, !0);
556 if ret == 0 {
557 Err(Error::last_os_error())
558 } else {
559 Ok(())
560 }
561 }
562 }
563
564 fn lock_file(file: &File, flags: u32) -> Result<()> {
565 unsafe {
566 let mut overlapped = mem::zeroed();
567 let ret = LockFileEx(
568 file.as_raw_handle() as HANDLE,
569 flags,
570 0,
571 !0,
572 !0,
573 &mut overlapped,
574 );
575 if ret == 0 {
576 Err(Error::last_os_error())
577 } else {
578 Ok(())
579 }
580 }
581 }
582}