From e9dc02a276f36b7393c147c8cc7bf0e488a264d7 Mon Sep 17 00:00:00 2001 From: Chris Denton Date: Sun, 25 Oct 2026 15:55:36 +0000 Subject: [PATCH] Support file locking on Solaris --- clippy.toml | 22 ++++++++ src/cli/self_update/stage.rs | 8 +-- src/utils/raw.rs | 106 ++++++++++++++++++++++++++++++++++- 3 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 clippy.toml diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000000..c5159f9d76 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,22 @@ +[[disallowed-methods]] +path = "std::fs::File::lock" +reason = "does not support Solaris" +replacement = "crate::utils::raw::lock" + +[[disallowed-methods]] +path = "std::fs::File::lock_shared" +reason = "does not support Solaris" +replacement = "crate::utils::raw::lock_shared" + +[[disallowed-methods]] +path = "std::fs::File::try_lock" +reason = "does not support Solaris" +replacement = "crate::utils::raw::try_lock" + +[[disallowed-methods]] +path = "std::fs::File::try_lock_shared" +reason = "needs a Solaris implementation in crate::utils::raw" + +[[disallowed-methods]] +path = "std::fs::File::unlock" +reason = "needs a Solaris implementation in crate::utils::raw" diff --git a/src/cli/self_update/stage.rs b/src/cli/self_update/stage.rs index a8e99c52bc..ba06c615b5 100644 --- a/src/cli/self_update/stage.rs +++ b/src/cli/self_update/stage.rs @@ -70,13 +70,13 @@ pub(super) struct SelfUpdateLock { impl SelfUpdateLock { pub(super) fn lock(process: &Process) -> anyhow::Result { let lock = Self::open(process)?; - lock._file.lock().context("failed to lock self-update")?; + utils::raw::lock(&lock._file).context("failed to lock self-update")?; Ok(lock) } fn try_lock(process: &Process) -> anyhow::Result> { let lock = Self::open(process)?; - match lock._file.try_lock() { + match utils::raw::try_lock(&lock._file) { Ok(()) => Ok(Some(lock)), Err(fs::TryLockError::WouldBlock) => Ok(None), Err(fs::TryLockError::Error(error)) => Err(error).context("failed to lock self-update"), @@ -331,11 +331,11 @@ mod tests { .unwrap(); assert!(matches!( - contender.try_lock(), + utils::raw::try_lock(&contender), Err(fs::TryLockError::WouldBlock) )); drop(lock); - contender.try_lock().unwrap(); + utils::raw::try_lock(&contender).unwrap(); } #[tokio::test] diff --git a/src/utils/raw.rs b/src/utils/raw.rs index 889a2e8a2e..49f3effc4a 100644 --- a/src/utils/raw.rs +++ b/src/utils/raw.rs @@ -66,7 +66,7 @@ pub fn path_exists>(path: P) -> bool { pub(crate) fn read_locked_file(path: &Path) -> io::Result { let mut file = File::open(path)?; - file.lock_shared()?; + lock_shared(&file)?; let size = file .metadata() @@ -110,7 +110,7 @@ pub(crate) fn write_locked_file(path: &Path, contents: &str) -> io::Result<()> { .truncate(false) .open(path)?; - file.lock()?; + lock(&file)?; file.set_len(0)?; file.seek(SeekFrom::Start(0))?; Write::write_all(&mut file, contents.as_bytes())?; @@ -389,6 +389,108 @@ pub(crate) mod windows { } } +/// File-locking support. +/// +/// On most platforms this is a thin wrapper around `File::lock` and `File::lock_shared`. +/// However, this isn't supported on Solaris[^1] so we instead implement it ourselves +/// using `fcntl`. +/// +/// [^1]: See [rust-lang/rust#157390](https://github.com/rust-lang/rust/issues/157390) for more +pub use flock::{lock, lock_shared, try_lock}; + +#[cfg(not(target_os = "solaris"))] +mod flock { + use std::{ + fs::{File, TryLockError}, + io, + }; + + pub fn lock(file: &File) -> io::Result<()> { + #[expect(clippy::disallowed_methods)] + file.lock() + } + + pub fn lock_shared(file: &File) -> io::Result<()> { + #[expect(clippy::disallowed_methods)] + file.lock_shared() + } + + pub fn try_lock(file: &File) -> Result<(), TryLockError> { + #[expect(clippy::disallowed_methods)] + file.try_lock() + } +} + +#[cfg(target_os = "solaris")] +mod flock { + // Code adapted from Cargo: + // https://github.com/rust-lang/cargo/blob/b57863321f8d0a0f76fcf9ad636b6e39b024d0c0/src/util/flock.rs + use std::{ + fs::{File, TryLockError}, + io, mem, + os::unix::io::AsRawFd, + }; + + pub fn lock(file: &File) -> io::Result<()> { + fcntl_lock(file, libc::F_WRLCK, FcntlCmd::F_SETLKW) + } + + pub fn lock_shared(file: &File) -> io::Result<()> { + fcntl_lock(file, libc::F_RDLCK, FcntlCmd::F_SETLKW) + } + + pub fn try_lock(file: &File) -> Result<(), TryLockError> { + match fcntl_lock(file, libc::F_WRLCK, FcntlCmd::F_SETLK) { + Ok(()) => Ok(()), + Err(e) if is_would_block(&e) => Err(TryLockError::WouldBlock), + Err(e) => Err(TryLockError::Error(e)), + } + } + + fn fcntl_lock(file: &File, lock_type: libc::c_short, cmd: FcntlCmd) -> io::Result<()> { + let mut lock = flock_for_whole_file(lock_type); + loop { + // SAFETY: + // fcntl takes a variable number of additonal arguments depending on `cmd`. + // In our case `cmd` is either F_SETLK or F_SETLKW which takes a single + // argument, which is a pointer to a mutable `flock`. + // We have exclusive ownership of `lock` so it's safe to pass. + let result = unsafe { libc::fcntl(file.as_raw_fd(), cmd as libc::c_int, &mut lock) }; + if result != -1 { + return Ok(()); + } + let error = io::Error::last_os_error(); + if cmd == FcntlCmd::F_SETLKW && error.kind() == io::ErrorKind::Interrupted { + continue; + } + return Err(error); + } + } + + #[repr(i32)] + #[derive(Copy, Clone, Eq, PartialEq)] + #[expect(nonstandard_style)] + enum FcntlCmd { + F_SETLK = libc::F_SETLK, + F_SETLKW = libc::F_SETLKW, + } + + fn flock_for_whole_file(lock_type: libc::c_short) -> libc::flock { + // SAFETY: libc::flock is a plain C data structure, so can be zeroed. + let mut lock = unsafe { mem::zeroed::() }; + lock.l_type = lock_type; + lock.l_whence = libc::SEEK_SET as libc::c_short; + lock.l_start = 0; + lock.l_len = 0; + lock + } + + fn is_would_block(error: &io::Error) -> bool { + matches!(error.raw_os_error(), Some(libc::EACCES | libc::EAGAIN)) + || error.kind() == io::ErrorKind::WouldBlock + } +} + #[cfg(test)] mod tests { use super::*;