Skip to content

Commit

Permalink
Rollup merge of #130999 - cberner:flock_pr, r=joboet
Browse files Browse the repository at this point in the history
Implement file_lock feature

This adds lock(), lock_shared(), try_lock(), try_lock_shared(), and unlock() to File gated behind the file_lock feature flag

This is the initial implementation of #130994 for Unix and Windows platforms. I will follow it up with an implementation for WASI preview 2
  • Loading branch information
matthiaskrgr authored Nov 11, 2024
2 parents d4abc31 + 9330786 commit 95175f8
Show file tree
Hide file tree
Showing 10 changed files with 575 additions and 0 deletions.
217 changes: 217 additions & 0 deletions library/std/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,223 @@ impl File {
self.inner.datasync()
}

/// Acquire an exclusive advisory lock on the file. Blocks until the lock can be acquired.
///
/// This acquires an exclusive advisory lock; no other file handle to this file may acquire
/// another lock.
///
/// If this file handle, or a clone of it, already holds an advisory lock the exact behavior is
/// unspecified and platform dependent, including the possibility that it will deadlock.
/// However, if this method returns, then an exclusive lock is held.
///
/// If the file not open for writing, it is unspecified whether this function returns an error.
///
/// Note, this is an advisory lock meant to interact with [`lock_shared`], [`try_lock`],
/// [`try_lock_shared`], and [`unlock`]. Its interactions with other methods, such as [`read`]
/// and [`write`] are platform specific, and it may or may not cause non-lockholders to block.
///
/// # Platform-specific behavior
///
/// This function currently corresponds to the `flock` function on Unix with the `LOCK_EX` flag,
/// and the `LockFileEx` function on Windows with the `LOCKFILE_EXCLUSIVE_LOCK` flag. Note that,
/// this [may change in the future][changes].
///
/// [changes]: io#platform-specific-behavior
///
/// [`lock_shared`]: File::lock_shared
/// [`try_lock`]: File::try_lock
/// [`try_lock_shared`]: File::try_lock_shared
/// [`unlock`]: File::unlock
/// [`read`]: Read::read
/// [`write`]: Write::write
///
/// # Examples
///
/// ```no_run
/// #![feature(file_lock)]
/// use std::fs::File;
///
/// fn main() -> std::io::Result<()> {
/// let f = File::open("foo.txt")?;
/// f.lock()?;
/// Ok(())
/// }
/// ```
#[unstable(feature = "file_lock", issue = "130994")]
pub fn lock(&self) -> io::Result<()> {
self.inner.lock()
}

/// Acquire a shared advisory lock on the file. Blocks until the lock can be acquired.
///
/// This acquires a shared advisory lock; more than one file handle may hold a shared lock, but
/// none may hold an exclusive lock.
///
/// If this file handle, or a clone of it, already holds an advisory lock, the exact behavior is
/// unspecified and platform dependent, including the possibility that it will deadlock.
/// However, if this method returns, then a shared lock is held.
///
/// Note, this is an advisory lock meant to interact with [`lock`], [`try_lock`],
/// [`try_lock_shared`], and [`unlock`]. Its interactions with other methods, such as [`read`]
/// and [`write`] are platform specific, and it may or may not cause non-lockholders to block.
///
/// # Platform-specific behavior
///
/// This function currently corresponds to the `flock` function on Unix with the `LOCK_SH` flag,
/// and the `LockFileEx` function on Windows. Note that, this
/// [may change in the future][changes].
///
/// [changes]: io#platform-specific-behavior
///
/// [`lock`]: File::lock
/// [`try_lock`]: File::try_lock
/// [`try_lock_shared`]: File::try_lock_shared
/// [`unlock`]: File::unlock
/// [`read`]: Read::read
/// [`write`]: Write::write
///
/// # Examples
///
/// ```no_run
/// #![feature(file_lock)]
/// use std::fs::File;
///
/// fn main() -> std::io::Result<()> {
/// let f = File::open("foo.txt")?;
/// f.lock_shared()?;
/// Ok(())
/// }
/// ```
#[unstable(feature = "file_lock", issue = "130994")]
pub fn lock_shared(&self) -> io::Result<()> {
self.inner.lock_shared()
}

/// Acquire an exclusive advisory lock on the file. Returns `Ok(false)` if the file is locked.
///
/// This acquires an exclusive advisory lock; no other file handle to this file may acquire
/// another lock.
///
/// If this file handle, or a clone of it, already holds an advisory lock, the exact behavior is
/// unspecified and platform dependent, including the possibility that it will deadlock.
/// However, if this method returns, then an exclusive lock is held.
///
/// If the file not open for writing, it is unspecified whether this function returns an error.
///
/// Note, this is an advisory lock meant to interact with [`lock`], [`lock_shared`],
/// [`try_lock_shared`], and [`unlock`]. Its interactions with other methods, such as [`read`]
/// and [`write`] are platform specific, and it may or may not cause non-lockholders to block.
///
/// # Platform-specific behavior
///
/// This function currently corresponds to the `flock` function on Unix with the `LOCK_EX` and
/// `LOCK_NB` flags, and the `LockFileEx` function on Windows with the `LOCKFILE_EXCLUSIVE_LOCK`
/// and `LOCKFILE_FAIL_IMMEDIATELY` flags. Note that, this
/// [may change in the future][changes].
///
/// [changes]: io#platform-specific-behavior
///
/// [`lock`]: File::lock
/// [`lock_shared`]: File::lock_shared
/// [`try_lock_shared`]: File::try_lock_shared
/// [`unlock`]: File::unlock
/// [`read`]: Read::read
/// [`write`]: Write::write
///
/// # Examples
///
/// ```no_run
/// #![feature(file_lock)]
/// use std::fs::File;
///
/// fn main() -> std::io::Result<()> {
/// let f = File::open("foo.txt")?;
/// f.try_lock()?;
/// Ok(())
/// }
/// ```
#[unstable(feature = "file_lock", issue = "130994")]
pub fn try_lock(&self) -> io::Result<bool> {
self.inner.try_lock()
}

/// Acquire a shared advisory lock on the file.
/// Returns `Ok(false)` if the file is exclusively locked.
///
/// This acquires a shared advisory lock; more than one file handle may hold a shared lock, but
/// none may hold an exclusive lock.
///
/// If this file handle, or a clone of it, already holds an advisory lock, the exact behavior is
/// unspecified and platform dependent, including the possibility that it will deadlock.
/// However, if this method returns, then a shared lock is held.
///
/// Note, this is an advisory lock meant to interact with [`lock`], [`try_lock`],
/// [`try_lock`], and [`unlock`]. Its interactions with other methods, such as [`read`]
/// and [`write`] are platform specific, and it may or may not cause non-lockholders to block.
///
/// # Platform-specific behavior
///
/// This function currently corresponds to the `flock` function on Unix with the `LOCK_SH` and
/// `LOCK_NB` flags, and the `LockFileEx` function on Windows with the
/// `LOCKFILE_FAIL_IMMEDIATELY` flag. Note that, this
/// [may change in the future][changes].
///
/// [changes]: io#platform-specific-behavior
///
/// [`lock`]: File::lock
/// [`lock_shared`]: File::lock_shared
/// [`try_lock`]: File::try_lock
/// [`unlock`]: File::unlock
/// [`read`]: Read::read
/// [`write`]: Write::write
///
/// # Examples
///
/// ```no_run
/// #![feature(file_lock)]
/// use std::fs::File;
///
/// fn main() -> std::io::Result<()> {
/// let f = File::open("foo.txt")?;
/// f.try_lock_shared()?;
/// Ok(())
/// }
/// ```
#[unstable(feature = "file_lock", issue = "130994")]
pub fn try_lock_shared(&self) -> io::Result<bool> {
self.inner.try_lock_shared()
}

/// Release all locks on the file.
///
/// All remaining locks are released when the file handle, and all clones of it, are dropped.
///
/// # Platform-specific behavior
///
/// This function currently corresponds to the `flock` function on Unix with the `LOCK_UN` flag,
/// and the `UnlockFile` function on Windows. Note that, this
/// [may change in the future][changes].
///
/// [changes]: io#platform-specific-behavior
///
/// # Examples
///
/// ```no_run
/// #![feature(file_lock)]
/// use std::fs::File;
///
/// fn main() -> std::io::Result<()> {
/// let f = File::open("foo.txt")?;
/// f.lock()?;
/// f.unlock()?;
/// Ok(())
/// }
/// ```
#[unstable(feature = "file_lock", issue = "130994")]
pub fn unlock(&self) -> io::Result<()> {
self.inner.unlock()
}

/// Truncates or extends the underlying file, updating the size of
/// this file to become `size`.
///
Expand Down
118 changes: 118 additions & 0 deletions library/std/src/fs/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,124 @@ fn file_test_io_seek_and_write() {
assert!(read_str == final_msg);
}

#[test]
fn file_lock_multiple_shared() {
let tmpdir = tmpdir();
let filename = &tmpdir.join("file_lock_multiple_shared_test.txt");
let f1 = check!(File::create(filename));
let f2 = check!(OpenOptions::new().write(true).open(filename));

// Check that we can acquire concurrent shared locks
check!(f1.lock_shared());
check!(f2.lock_shared());
check!(f1.unlock());
check!(f2.unlock());
assert!(check!(f1.try_lock_shared()));
assert!(check!(f2.try_lock_shared()));
}

#[test]
fn file_lock_blocking() {
let tmpdir = tmpdir();
let filename = &tmpdir.join("file_lock_blocking_test.txt");
let f1 = check!(File::create(filename));
let f2 = check!(OpenOptions::new().write(true).open(filename));

// Check that shared locks block exclusive locks
check!(f1.lock_shared());
assert!(!check!(f2.try_lock()));
check!(f1.unlock());

// Check that exclusive locks block shared locks
check!(f1.lock());
assert!(!check!(f2.try_lock_shared()));
}

#[test]
fn file_lock_drop() {
let tmpdir = tmpdir();
let filename = &tmpdir.join("file_lock_dup_test.txt");
let f1 = check!(File::create(filename));
let f2 = check!(OpenOptions::new().write(true).open(filename));

// Check that locks are released when the File is dropped
check!(f1.lock_shared());
assert!(!check!(f2.try_lock()));
drop(f1);
assert!(check!(f2.try_lock()));
}

#[test]
fn file_lock_dup() {
let tmpdir = tmpdir();
let filename = &tmpdir.join("file_lock_dup_test.txt");
let f1 = check!(File::create(filename));
let f2 = check!(OpenOptions::new().write(true).open(filename));

// Check that locks are not dropped if the File has been cloned
check!(f1.lock_shared());
assert!(!check!(f2.try_lock()));
let cloned = check!(f1.try_clone());
drop(f1);
assert!(!check!(f2.try_lock()));
drop(cloned)
}

#[test]
#[cfg(windows)]
fn file_lock_double_unlock() {
let tmpdir = tmpdir();
let filename = &tmpdir.join("file_lock_double_unlock_test.txt");
let f1 = check!(File::create(filename));
let f2 = check!(OpenOptions::new().write(true).open(filename));

// On Windows a file handle may acquire both a shared and exclusive lock.
// Check that both are released by unlock()
check!(f1.lock());
check!(f1.lock_shared());
assert!(!check!(f2.try_lock()));
check!(f1.unlock());
assert!(check!(f2.try_lock()));
}

#[test]
#[cfg(windows)]
fn file_lock_blocking_async() {
use crate::thread::{sleep, spawn};
const FILE_FLAG_OVERLAPPED: u32 = 0x40000000;

let tmpdir = tmpdir();
let filename = &tmpdir.join("file_lock_blocking_async.txt");
let f1 = check!(File::create(filename));
let f2 =
check!(OpenOptions::new().custom_flags(FILE_FLAG_OVERLAPPED).write(true).open(filename));

check!(f1.lock());

// Ensure that lock() is synchronous when the file is opened for asynchronous IO
let t = spawn(move || {
check!(f2.lock());
});
sleep(Duration::from_secs(1));
assert!(!t.is_finished());
check!(f1.unlock());
t.join().unwrap();

// Ensure that lock_shared() is synchronous when the file is opened for asynchronous IO
let f2 =
check!(OpenOptions::new().custom_flags(FILE_FLAG_OVERLAPPED).write(true).open(filename));
check!(f1.lock());

// Ensure that lock() is synchronous when the file is opened for asynchronous IO
let t = spawn(move || {
check!(f2.lock_shared());
});
sleep(Duration::from_secs(1));
assert!(!t.is_finished());
check!(f1.unlock());
t.join().unwrap();
}

#[test]
fn file_test_io_seek_shakedown() {
// 01234567890123
Expand Down
20 changes: 20 additions & 0 deletions library/std/src/sys/pal/hermit/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,26 @@ impl File {
self.fsync()
}

pub fn lock(&self) -> io::Result<()> {
unsupported()
}

pub fn lock_shared(&self) -> io::Result<()> {
unsupported()
}

pub fn try_lock(&self) -> io::Result<bool> {
unsupported()
}

pub fn try_lock_shared(&self) -> io::Result<bool> {
unsupported()
}

pub fn unlock(&self) -> io::Result<()> {
unsupported()
}

pub fn truncate(&self, _size: u64) -> io::Result<()> {
Err(Error::from_raw_os_error(22))
}
Expand Down
Loading

0 comments on commit 95175f8

Please sign in to comment.