diff --git a/changelog/2170.added.md b/changelog/2170.added.md new file mode 100644 index 0000000000..6d089a120a --- /dev/null +++ b/changelog/2170.added.md @@ -0,0 +1,2 @@ +Added newtype `Flock` to automatically unlock a held flock upon drop. +Added `Flockable` trait to represent valid types for `Flock`. diff --git a/src/fcntl.rs b/src/fcntl.rs index 6bda0c09e2..8f68b498e7 100644 --- a/src/fcntl.rs +++ b/src/fcntl.rs @@ -10,11 +10,14 @@ use libc::{self, c_int, c_uint, size_t, ssize_t}; ))] use std::ffi::CStr; use std::ffi::OsString; +#[cfg(not(any(target_os = "redox", target_os = "solaris")))] +use std::ops::{Deref, DerefMut}; #[cfg(not(target_os = "redox"))] use std::os::raw; use std::os::unix::ffi::OsStringExt; use std::os::unix::io::RawFd; -// For splice and copy_file_range +#[cfg(not(any(target_os = "redox", target_os = "solaris")))] +use std::os::unix::io::{AsRawFd, OwnedFd}; #[cfg(any( target_os = "netbsd", apple_targets, @@ -23,10 +26,7 @@ use std::os::unix::io::RawFd; ))] use std::path::PathBuf; #[cfg(any(linux_android, target_os = "freebsd"))] -use std::{ - os::unix::io::{AsFd, AsRawFd}, - ptr, -}; +use std::{os::unix::io::AsFd, ptr}; #[cfg(feature = "fs")] use crate::{sys::stat::Mode, NixPath, Result}; @@ -578,7 +578,6 @@ pub fn fcntl(fd: RawFd, arg: FcntlArg) -> Result { Errno::result(res) } -// TODO: convert to libc_enum #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] pub enum FlockArg { @@ -591,6 +590,7 @@ pub enum FlockArg { } #[cfg(not(any(target_os = "redox", target_os = "solaris")))] +#[deprecated(since = "0.28.0", note = "`fcntl::Flock` should be used instead.")] pub fn flock(fd: RawFd, arg: FlockArg) -> Result<()> { use self::FlockArg::*; @@ -611,6 +611,122 @@ pub fn flock(fd: RawFd, arg: FlockArg) -> Result<()> { Errno::result(res).map(drop) } + +/// Represents valid types for flock. +/// +/// # Safety +/// Types implementing this must not be `Clone`. +#[cfg(not(any(target_os = "redox", target_os = "solaris")))] +pub unsafe trait Flockable: AsRawFd {} + +/// Represents an owned flock, which unlocks on drop. +/// +/// See [flock(2)](https://linux.die.net/man/2/flock) for details on locking semantics. +#[cfg(not(any(target_os = "redox", target_os = "solaris")))] +#[derive(Debug)] +pub struct Flock(T); + +#[cfg(not(any(target_os = "redox", target_os = "solaris")))] +impl Drop for Flock { + fn drop(&mut self) { + let res = Errno::result(unsafe { libc::flock(self.0.as_raw_fd(), libc::LOCK_UN) }); + if res.is_err() && !std::thread::panicking() { + panic!("Failed to remove flock: {}", res.unwrap_err()); + } + } +} + +#[cfg(not(any(target_os = "redox", target_os = "solaris")))] +impl Deref for Flock { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} +#[cfg(not(any(target_os = "redox", target_os = "solaris")))] +impl DerefMut for Flock { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[cfg(not(any(target_os = "redox", target_os = "solaris")))] +impl Flock { + /// Obtain a/an flock. + /// + /// # Example + /// ``` + /// # use std::io::Write; + /// # use std::fs::File; + /// # use nix::fcntl::{Flock, FlockArg}; + /// # fn do_stuff(file: File) { + /// let mut file = match Flock::lock(file, FlockArg::LockExclusive) { + /// Ok(l) => l, + /// Err(_) => return, + /// }; + /// + /// // Do stuff + /// let data = "Foo bar"; + /// _ = file.write(data.as_bytes()); + /// _ = file.sync_data(); + /// # } + pub fn lock(t: T, args: FlockArg) -> std::result::Result { + let flags = match args { + FlockArg::LockShared => libc::LOCK_SH, + FlockArg::LockExclusive => libc::LOCK_EX, + FlockArg::LockSharedNonblock => libc::LOCK_SH | libc::LOCK_NB, + FlockArg::LockExclusiveNonblock => libc::LOCK_EX | libc::LOCK_NB, + FlockArg::Unlock | FlockArg::UnlockNonblock => return Err((t, Errno::EINVAL)), + }; + match Errno::result(unsafe { libc::flock(t.as_raw_fd(), flags) }) { + Ok(_) => Ok(Self(t)), + Err(errno) => Err((t, errno)), + } + } + + /// Remove the lock and return the object wrapped within. + /// + /// # Example + /// ``` + /// # use std::fs::File; + /// # use nix::fcntl::{Flock, FlockArg}; + /// fn do_stuff(file: File) -> nix::Result<()> { + /// let mut lock = match Flock::lock(file, FlockArg::LockExclusive) { + /// Ok(l) => l, + /// Err((_,e)) => return Err(e), + /// }; + /// + /// // Do critical section + /// + /// // Unlock + /// let file = match lock.unlock() { + /// Ok(f) => f, + /// Err((_, e)) => return Err(e), + /// }; + /// + /// // Do anything else + /// + /// Ok(()) + /// } + pub fn unlock(self) -> std::result::Result { + let inner = unsafe { match Errno::result(libc::flock(self.0.as_raw_fd(), libc::LOCK_UN)) { + Ok(_) => std::ptr::read(&self.0), + Err(errno) => return Err((self, errno)), + }}; + + std::mem::forget(self); + Ok(inner) + } +} + +// Safety: `File` is not [std::clone::Clone]. +#[cfg(not(any(target_os = "redox", target_os = "solaris")))] +unsafe impl Flockable for std::fs::File {} + +// Safety: `OwnedFd` is not [std::clone::Clone]. +#[cfg(not(any(target_os = "redox", target_os = "solaris")))] +unsafe impl Flockable for OwnedFd {} } #[cfg(linux_android)] diff --git a/test/test_fcntl.rs b/test/test_fcntl.rs index 788b7a8a0d..df323c4394 100644 --- a/test/test_fcntl.rs +++ b/test/test_fcntl.rs @@ -630,3 +630,57 @@ fn test_f_kinfo() { assert_ne!(res, -1); assert_eq!(path, tmp.path()); } + +/// Test `Flock` and associated functions. +/// +#[cfg(not(any(target_os = "redox", target_os = "solaris")))] +mod test_flock { + use nix::fcntl::*; + use tempfile::NamedTempFile; + + /// Verify that `Flock::lock()` correctly obtains a lock, and subsequently unlocks upon drop. + #[test] + fn verify_lock_and_drop() { + // Get 2 `File` handles to same underlying file. + let file1 = NamedTempFile::new().unwrap(); + let file2 = file1.reopen().unwrap(); + let file1 = file1.into_file(); + + // Lock first handle + let lock1 = Flock::lock(file1, FlockArg::LockExclusive).unwrap(); + + // Attempt to lock second handle + let file2 = match Flock::lock(file2, FlockArg::LockExclusiveNonblock) { + Ok(_) => panic!("Expected second exclusive lock to fail."), + Err((f, _)) => f, + }; + + // Drop first lock + std::mem::drop(lock1); + + // Attempt to lock second handle again (but successfully) + if Flock::lock(file2, FlockArg::LockExclusiveNonblock).is_err() { + panic!("Expected locking to be successful."); + } + } + + /// Verify that `Flock::unlock()` correctly obtains unlocks. + #[test] + fn verify_unlock() { + // Get 2 `File` handles to same underlying file. + let file1 = NamedTempFile::new().unwrap(); + let file2 = file1.reopen().unwrap(); + let file1 = file1.into_file(); + + // Lock first handle + let lock1 = Flock::lock(file1, FlockArg::LockExclusive).unwrap(); + + // Unlock and retain file so any erroneous flocks also remain present. + let _file1 = lock1.unlock().unwrap(); + + // Attempt to lock second handle. + if Flock::lock(file2, FlockArg::LockExclusiveNonblock).is_err() { + panic!("Expected locking to be successful."); + } + } +}