flock.rs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. // taken from https://github.com/rust-lang/cargo/blob/b0c9586f4cbf426914df47c65de38ea323772c74/src/cargo/util/flock.rs
  5. #![allow(dead_code)]
  6. use std::fs::{create_dir_all, File, OpenOptions};
  7. use std::io;
  8. use std::io::{Read, Seek, SeekFrom, Write};
  9. use std::path::{Path, PathBuf};
  10. use crate::Result;
  11. use anyhow::Context as _;
  12. use sys::*;
  13. #[derive(Debug)]
  14. pub struct FileLock {
  15. f: Option<File>,
  16. path: PathBuf,
  17. state: State,
  18. }
  19. #[derive(PartialEq, Debug)]
  20. enum State {
  21. Unlocked,
  22. Shared,
  23. Exclusive,
  24. }
  25. impl FileLock {
  26. /// Returns the underlying file handle of this lock.
  27. pub fn file(&self) -> &File {
  28. self.f.as_ref().unwrap()
  29. }
  30. /// Returns the underlying path that this lock points to.
  31. ///
  32. /// Note that special care must be taken to ensure that the path is not
  33. /// referenced outside the lifetime of this lock.
  34. pub fn path(&self) -> &Path {
  35. assert_ne!(self.state, State::Unlocked);
  36. &self.path
  37. }
  38. /// Returns the parent path containing this file
  39. pub fn parent(&self) -> &Path {
  40. assert_ne!(self.state, State::Unlocked);
  41. self.path.parent().unwrap()
  42. }
  43. }
  44. impl Read for FileLock {
  45. fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
  46. self.file().read(buf)
  47. }
  48. }
  49. impl Seek for FileLock {
  50. fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
  51. self.file().seek(to)
  52. }
  53. }
  54. impl Write for FileLock {
  55. fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
  56. self.file().write(buf)
  57. }
  58. fn flush(&mut self) -> io::Result<()> {
  59. self.file().flush()
  60. }
  61. }
  62. impl Drop for FileLock {
  63. fn drop(&mut self) {
  64. if self.state != State::Unlocked {
  65. if let Some(f) = self.f.take() {
  66. let _ = unlock(&f);
  67. }
  68. }
  69. }
  70. }
  71. /// Opens exclusive access to a file, returning the locked version of a
  72. /// file.
  73. ///
  74. /// This function will create a file at `path` if it doesn't already exist
  75. /// (including intermediate directories), and then it will acquire an
  76. /// exclusive lock on `path`. If the process must block waiting for the
  77. /// lock, the `msg` is logged.
  78. ///
  79. /// The returned file can be accessed to look at the path and also has
  80. /// read/write access to the underlying file.
  81. pub fn open_rw<P>(path: P, msg: &str) -> Result<FileLock>
  82. where
  83. P: AsRef<Path>,
  84. {
  85. open(
  86. path.as_ref(),
  87. OpenOptions::new().read(true).write(true).create(true),
  88. State::Exclusive,
  89. msg,
  90. )
  91. }
  92. /// Opens shared access to a file, returning the locked version of a file.
  93. ///
  94. /// This function will fail if `path` doesn't already exist, but if it does
  95. /// then it will acquire a shared lock on `path`. If the process must block
  96. /// waiting for the lock, the `msg` is logged.
  97. ///
  98. /// The returned file can be accessed to look at the path and also has read
  99. /// access to the underlying file. Any writes to the file will return an
  100. /// error.
  101. pub fn open_ro<P>(path: P, msg: &str) -> Result<FileLock>
  102. where
  103. P: AsRef<Path>,
  104. {
  105. open(
  106. path.as_ref(),
  107. OpenOptions::new().read(true),
  108. State::Shared,
  109. msg,
  110. )
  111. }
  112. fn open(path: &Path, opts: &OpenOptions, state: State, msg: &str) -> Result<FileLock> {
  113. // If we want an exclusive lock then if we fail because of NotFound it's
  114. // likely because an intermediate directory didn't exist, so try to
  115. // create the directory and then continue.
  116. let f = opts
  117. .open(path)
  118. .or_else(|e| {
  119. if e.kind() == io::ErrorKind::NotFound && state == State::Exclusive {
  120. create_dir_all(path.parent().unwrap())?;
  121. Ok(opts.open(path)?)
  122. } else {
  123. Err(anyhow::Error::from(e))
  124. }
  125. })
  126. .with_context(|| format!("failed to open: {}", path.display()))?;
  127. match state {
  128. State::Exclusive => {
  129. acquire(msg, path, &|| try_lock_exclusive(&f), &|| {
  130. lock_exclusive(&f)
  131. })?;
  132. }
  133. State::Shared => {
  134. acquire(msg, path, &|| try_lock_shared(&f), &|| lock_shared(&f))?;
  135. }
  136. State::Unlocked => {}
  137. }
  138. Ok(FileLock {
  139. f: Some(f),
  140. path: path.to_path_buf(),
  141. state,
  142. })
  143. }
  144. /// Acquires a lock on a file in a "nice" manner.
  145. ///
  146. /// Almost all long-running blocking actions in Cargo have a status message
  147. /// associated with them as we're not sure how long they'll take. Whenever a
  148. /// conflicted file lock happens, this is the case (we're not sure when the lock
  149. /// will be released).
  150. ///
  151. /// This function will acquire the lock on a `path`, printing out a nice message
  152. /// to the console if we have to wait for it. It will first attempt to use `try`
  153. /// to acquire a lock on the crate, and in the case of contention it will emit a
  154. /// status message based on `msg` to `config`'s shell, and then use `block` to
  155. /// block waiting to acquire a lock.
  156. ///
  157. /// Returns an error if the lock could not be acquired or if any error other
  158. /// than a contention error happens.
  159. fn acquire(
  160. msg: &str,
  161. path: &Path,
  162. lock_try: &dyn Fn() -> io::Result<()>,
  163. lock_block: &dyn Fn() -> io::Result<()>,
  164. ) -> Result<()> {
  165. // File locking on Unix is currently implemented via `flock`, which is known
  166. // to be broken on NFS. We could in theory just ignore errors that happen on
  167. // NFS, but apparently the failure mode [1] for `flock` on NFS is **blocking
  168. // forever**, even if the "non-blocking" flag is passed!
  169. //
  170. // As a result, we just skip all file locks entirely on NFS mounts. That
  171. // should avoid calling any `flock` functions at all, and it wouldn't work
  172. // there anyway.
  173. //
  174. // [1]: https://github.com/rust-lang/cargo/issues/2615
  175. if is_on_nfs_mount(path) {
  176. return Ok(());
  177. }
  178. match lock_try() {
  179. Ok(()) => return Ok(()),
  180. // In addition to ignoring NFS which is commonly not working we also
  181. // just ignore locking on filesystems that look like they don't
  182. // implement file locking.
  183. Err(e) if error_unsupported(&e) => return Ok(()),
  184. Err(e) => {
  185. if !error_contended(&e) {
  186. let e = anyhow::Error::from(e);
  187. let cx = format!("failed to lock file: {}", path.display());
  188. return Err(e.context(cx));
  189. }
  190. }
  191. }
  192. let msg = format!("waiting for file lock on {msg}");
  193. log::info!(action = "Blocking"; "{}", &msg);
  194. lock_block().with_context(|| format!("failed to lock file: {}", path.display()))?;
  195. return Ok(());
  196. #[cfg(all(target_os = "linux", not(target_env = "musl")))]
  197. fn is_on_nfs_mount(path: &Path) -> bool {
  198. use std::ffi::CString;
  199. use std::mem;
  200. use std::os::unix::prelude::*;
  201. let path = match CString::new(path.as_os_str().as_bytes()) {
  202. Ok(path) => path,
  203. Err(_) => return false,
  204. };
  205. unsafe {
  206. let mut buf: libc::statfs = mem::zeroed();
  207. let r = libc::statfs(path.as_ptr(), &mut buf);
  208. r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
  209. }
  210. }
  211. #[cfg(any(not(target_os = "linux"), target_env = "musl"))]
  212. fn is_on_nfs_mount(_path: &Path) -> bool {
  213. false
  214. }
  215. }
  216. #[cfg(unix)]
  217. mod sys {
  218. use std::fs::File;
  219. use std::io::{Error, Result};
  220. use std::os::unix::io::AsRawFd;
  221. pub(super) fn lock_shared(file: &File) -> Result<()> {
  222. flock(file, libc::LOCK_SH)
  223. }
  224. pub(super) fn lock_exclusive(file: &File) -> Result<()> {
  225. flock(file, libc::LOCK_EX)
  226. }
  227. pub(super) fn try_lock_shared(file: &File) -> Result<()> {
  228. flock(file, libc::LOCK_SH | libc::LOCK_NB)
  229. }
  230. pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
  231. flock(file, libc::LOCK_EX | libc::LOCK_NB)
  232. }
  233. pub(super) fn unlock(file: &File) -> Result<()> {
  234. flock(file, libc::LOCK_UN)
  235. }
  236. pub(super) fn error_contended(err: &Error) -> bool {
  237. err.raw_os_error().map_or(false, |x| x == libc::EWOULDBLOCK)
  238. }
  239. pub(super) fn error_unsupported(err: &Error) -> bool {
  240. match err.raw_os_error() {
  241. // Unfortunately, depending on the target, these may or may not be the same.
  242. // For targets in which they are the same, the duplicate pattern causes a warning.
  243. #[allow(unreachable_patterns)]
  244. Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
  245. Some(libc::ENOSYS) => true,
  246. _ => false,
  247. }
  248. }
  249. #[cfg(not(target_os = "solaris"))]
  250. fn flock(file: &File, flag: libc::c_int) -> Result<()> {
  251. let ret = unsafe { libc::flock(file.as_raw_fd(), flag) };
  252. if ret < 0 {
  253. Err(Error::last_os_error())
  254. } else {
  255. Ok(())
  256. }
  257. }
  258. #[cfg(target_os = "solaris")]
  259. fn flock(file: &File, flag: libc::c_int) -> Result<()> {
  260. // Solaris lacks flock(), so simply succeed with a no-op
  261. Ok(())
  262. }
  263. }
  264. #[cfg(windows)]
  265. mod sys {
  266. use std::fs::File;
  267. use std::io::{Error, Result};
  268. use std::mem;
  269. use std::os::windows::io::AsRawHandle;
  270. use windows_sys::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_LOCK_VIOLATION, HANDLE};
  271. use windows_sys::Win32::Storage::FileSystem::{
  272. LockFileEx, UnlockFile, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY, LOCK_FILE_FLAGS,
  273. };
  274. pub(super) fn lock_shared(file: &File) -> Result<()> {
  275. lock_file(file, 0)
  276. }
  277. pub(super) fn lock_exclusive(file: &File) -> Result<()> {
  278. lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)
  279. }
  280. pub(super) fn try_lock_shared(file: &File) -> Result<()> {
  281. lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)
  282. }
  283. pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
  284. lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)
  285. }
  286. pub(super) fn error_contended(err: &Error) -> bool {
  287. err
  288. .raw_os_error()
  289. .map_or(false, |x| x == ERROR_LOCK_VIOLATION as i32)
  290. }
  291. pub(super) fn error_unsupported(err: &Error) -> bool {
  292. err
  293. .raw_os_error()
  294. .map_or(false, |x| x == ERROR_INVALID_FUNCTION as i32)
  295. }
  296. pub(super) fn unlock(file: &File) -> Result<()> {
  297. let ret = unsafe { UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, !0, !0) };
  298. if ret == 0 {
  299. Err(Error::last_os_error())
  300. } else {
  301. Ok(())
  302. }
  303. }
  304. fn lock_file(file: &File, flags: LOCK_FILE_FLAGS) -> Result<()> {
  305. let ret = unsafe {
  306. let mut overlapped = mem::zeroed();
  307. LockFileEx(
  308. file.as_raw_handle() as HANDLE,
  309. flags,
  310. 0,
  311. !0,
  312. !0,
  313. &mut overlapped,
  314. )
  315. };
  316. if ret == 0 {
  317. Err(Error::last_os_error())
  318. } else {
  319. Ok(())
  320. }
  321. }
  322. }