use std::fs::File;
use std::io;
#[derive(Debug)]
pub struct ExclusiveLock {
file: File,
}
impl ExclusiveLock {
pub fn file(&self) -> &File {
&self.file
}
}
impl Drop for ExclusiveLock {
fn drop(&mut self) {
let _ = imp::unlock(&self.file);
}
}
pub fn try_lock(file: File) -> io::Result<Option<ExclusiveLock>> {
Ok(try_lock_exclusive(&file)?.then_some(ExclusiveLock { file }))
}
fn try_lock_exclusive(file: &File) -> io::Result<bool> {
imp::try_lock_exclusive(file)
}
#[cfg(not(windows))]
mod imp {
pub(super) fn try_lock_exclusive(file: &std::fs::File) -> std::io::Result<bool> {
fs4::fs_std::FileExt::try_lock_exclusive(file)
}
pub(super) fn unlock(file: &std::fs::File) -> std::io::Result<()> {
fs4::fs_std::FileExt::unlock(file)
}
}
#[cfg(windows)]
mod imp {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Foundation::{ERROR_IO_PENDING, ERROR_LOCK_VIOLATION};
use windows_sys::Win32::Storage::FileSystem::{
LockFileEx, UnlockFileEx, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
};
use windows_sys::Win32::System::IO::{OVERLAPPED, OVERLAPPED_0, OVERLAPPED_0_0};
const LOCK_OFFSET_HIGH: u32 = u32::MAX;
fn locked_byte() -> OVERLAPPED {
OVERLAPPED {
Anonymous: OVERLAPPED_0 { Anonymous: OVERLAPPED_0_0 { Offset: 0, OffsetHigh: LOCK_OFFSET_HIGH } },
..Default::default()
}
}
pub(super) fn try_lock_exclusive(file: &std::fs::File) -> std::io::Result<bool> {
let mut overlapped = locked_byte();
let ok = unsafe {
LockFileEx(
file.as_raw_handle(),
LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
0,
1,
0,
&mut overlapped,
)
};
if ok != 0 {
return Ok(true);
}
let err = std::io::Error::last_os_error();
match err.raw_os_error() {
Some(code) if code == ERROR_LOCK_VIOLATION as i32 || code == ERROR_IO_PENDING as i32 => Ok(false),
_ => Err(err),
}
}
pub(super) fn unlock(file: &std::fs::File) -> std::io::Result<()> {
let mut overlapped = locked_byte();
let ok = unsafe { UnlockFileEx(file.as_raw_handle(), 0, 1, 0, &mut overlapped) };
if ok != 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
}
#[cfg(test)]
#[serial_test::parallel(process_spawn)]
mod tests {
use super::*;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
use std::time::{Duration, Instant};
fn open(path: &Path) -> File {
OpenOptions::new().create(true).truncate(false).read(true).write(true).open(path).unwrap()
}
fn relock_within(file: &File, lock: impl Fn(&File) -> io::Result<bool>) -> bool {
let t0 = Instant::now();
loop {
if lock(file).unwrap() {
return true;
}
if t0.elapsed() > Duration::from_secs(5) {
return false;
}
std::thread::sleep(Duration::from_millis(20));
}
}
#[test]
fn a_refused_locker_can_read_the_holders_body() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("x.lock");
let held = try_lock(open(&path)).unwrap().expect("uncontended");
held.file().write_all(b"4242\ntui\n").unwrap();
let other = open(&path);
assert!(!try_lock_exclusive(&other).unwrap(), "held");
assert!(try_lock(open(&path)).unwrap().is_none(), "held");
assert_eq!(std::fs::read_to_string(&path).unwrap(), "4242\ntui\n");
drop(held);
assert!(relock_within(&other, try_lock_exclusive), "released on drop");
}
#[test]
fn excludes_and_is_excluded_by_a_whole_file_lock() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("x.lock");
let whole = open(&path);
assert!(fs4::fs_std::FileExt::try_lock_exclusive(&whole).unwrap());
let ours = open(&path);
assert!(!try_lock_exclusive(&ours).unwrap(), "a whole-file lock excludes ours");
drop(whole);
assert!(relock_within(&ours, try_lock_exclusive));
let late = open(&path);
assert!(!fs4::fs_std::FileExt::try_lock_exclusive(&late).unwrap(), "ours excludes a whole-file lock");
}
}
#[cfg(all(test, unix))]
pub(crate) struct ParkedChild {
release: Option<io::PipeWriter>,
spawner: Option<std::thread::JoinHandle<io::Result<std::process::ExitStatus>>>,
}
#[cfg(all(test, unix))]
impl ParkedChild {
pub(crate) fn park() -> Self {
use std::io::Read;
use std::os::fd::AsRawFd;
use std::os::unix::process::CommandExt;
use std::time::{Duration, Instant};
let (mut forked, signal) = io::pipe().unwrap();
let (hold, release) = io::pipe().unwrap();
let (signal_fd, hold_fd) = (signal.as_raw_fd(), hold.as_raw_fd());
let mut child = Self {
release: Some(release),
spawner: Some(std::thread::spawn(move || {
let mut command = std::process::Command::new("true");
unsafe {
command.pre_exec(move || {
let interrupted = || io::Error::last_os_error().raw_os_error() == Some(libc::EINTR);
loop {
match libc::write(signal_fd, b"f".as_ptr().cast(), 1) {
1 => break,
-1 if interrupted() => {}
_ => libc::_exit(1),
}
}
let mut byte = 0u8;
while libc::read(hold_fd, (&mut byte as *mut u8).cast(), 1) < 0 && interrupted() {}
Ok(())
});
}
let status = command.status();
drop((signal, hold));
status
})),
};
let deadline = Instant::now() + Duration::from_secs(30);
loop {
let mut ready = libc::pollfd { fd: forked.as_raw_fd(), events: libc::POLLIN, revents: 0 };
let left = deadline.saturating_duration_since(Instant::now());
let timeout = left.as_millis().try_into().unwrap_or(i32::MAX);
match unsafe { libc::poll(&mut ready, 1, timeout) } {
1 => break,
0 => panic!("the forked child never parked"),
_ => assert_eq!(io::Error::last_os_error().raw_os_error(), Some(libc::EINTR)),
}
}
if let Err(e) = forked.read_exact(&mut [0; 1]) {
let spawn = child.release_and_reap();
panic!("the forked child never parked: {e}; spawn: {spawn:?}");
}
child
}
pub(crate) fn release(mut self) {
let status = self.release_and_reap().expect("not yet released");
assert!(status.expect("spawner panicked").expect("spawn failed").success());
}
fn release_and_reap(&mut self) -> Option<std::thread::Result<io::Result<std::process::ExitStatus>>> {
if let Some(mut release) = self.release.take() {
use std::io::Write;
let _ = release.write_all(b"r");
}
self.spawner.take().map(std::thread::JoinHandle::join)
}
}
#[cfg(all(test, unix))]
impl Drop for ParkedChild {
fn drop(&mut self) {
let _ = self.release_and_reap();
}
}
#[cfg(all(test, unix))]
mod fork_tests {
use super::*;
use std::fs::OpenOptions;
#[test]
#[serial_test::serial(process_spawn)]
fn a_descriptor_inherited_by_a_forked_child_does_not_outlive_the_lock() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("x.lock");
let open = || OpenOptions::new().create(true).truncate(false).read(true).write(true).open(&path).unwrap();
let held = try_lock(open()).unwrap().expect("uncontended");
let child = ParkedChild::park();
drop(held);
let relocked = try_lock(open()).unwrap();
child.release();
assert!(relocked.is_some(), "a forked child's inherited descriptor kept the lock");
}
}