ic-host-fs 0.18.0

Local host filesystem reads, durable publication and file locking
Documentation
//! Regular descriptor locking, release and wait observations.

use std::{
    fs::File,
    io,
    time::{Duration, Instant},
};

#[cfg(all(test, unix))]
mod tests;

/// Acquire a blocking shared lock on a caller-owned regular descriptor.
///
/// Independent shared holders coexist. Opening, namespace custody, deadlines,
/// descriptor clones and final unlock/close policy remain caller-owned.
/// This does not reopen the file or impose an elapsed-time deadline.
///
/// # Errors
/// Returns metadata, non-regular descriptor, native lock or unsupported-host errors.
pub fn lock_shared(file: &File) -> io::Result<()> {
    require_regular_descriptor(file)?;
    #[cfg(unix)]
    {
        rustix::fs::flock(file, rustix::fs::FlockOperation::LockShared)
            .map_err(|error| io::Error::from_raw_os_error(error.raw_os_error()))
    }
    #[cfg(not(unix))]
    {
        Err(io::Error::from(io::ErrorKind::Unsupported))
    }
}

/// Acquire a blocking exclusive lock on a caller-owned regular descriptor.
///
/// Ownership and deadline policy are the same as [`lock_shared`]. Use
/// [`lock_exclusive_with_wait`] when contention needs caller observations.
///
/// # Errors
/// Returns metadata, non-regular descriptor, native lock or unsupported-host errors.
pub fn lock_exclusive(file: &File) -> io::Result<()> {
    require_regular_descriptor(file)?;
    #[cfg(unix)]
    {
        rustix::fs::flock(file, rustix::fs::FlockOperation::LockExclusive)
            .map_err(|error| io::Error::from_raw_os_error(error.raw_os_error()))
    }
    #[cfg(not(unix))]
    {
        Err(io::Error::from(io::ErrorKind::Unsupported))
    }
}

/// Release a caller-owned regular descriptor's lock without closing it.
///
/// Unlocking releases the shared kernel ownership of its duplicated or inherited
/// descriptors too. It does not release independently opened holders. The caller
/// chooses the final-owner boundary; this does not close or remove any file.
///
/// # Errors
/// Returns metadata, non-regular descriptor, native unlock or unsupported-host errors.
pub fn unlock(file: &File) -> io::Result<()> {
    require_regular_descriptor(file)?;
    #[cfg(unix)]
    {
        rustix::fs::flock(file, rustix::fs::FlockOperation::Unlock)
            .map_err(|error| io::Error::from_raw_os_error(error.raw_os_error()))
    }
    #[cfg(not(unix))]
    {
        Err(io::Error::from(io::ErrorKind::Unsupported))
    }
}

fn require_regular_descriptor(file: &File) -> io::Result<()> {
    if file.metadata()?.is_file() {
        Ok(())
    } else {
        Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "lock requires a regular file",
        ))
    }
}

/// Acquire an exclusive lock on a caller-owned regular descriptor with wait observations.
///
/// Each contention result calls `waiting` before sleeping for `poll_interval`.
/// Interrupted acquisition is retried without reporting contention. Uncontended
/// acquisition reports no wait. Returns elapsed acquisition time; the descriptor
/// retains the lock until the caller unlocks or closes it. The caller owns opening,
/// namespace, progress policy, deadlines and descriptor clones. This adds no path
/// admission or descendant ownership. Callback failure stops acquisition.
///
/// # Errors
/// Returns a zero polling interval, non-regular descriptor, metadata, callback,
/// lock acquisition or unsupported-platform error.
pub fn lock_exclusive_with_wait(
    file: &File,
    poll_interval: Duration,
    waiting: impl FnMut(Duration) -> io::Result<()>,
) -> io::Result<Duration> {
    if poll_interval.is_zero() || !file.metadata()?.is_file() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidInput,
            "lock requires a regular file and nonzero polling interval",
        ));
    }
    #[cfg(unix)]
    {
        wait_for_lock(poll_interval, waiting, || {
            rustix::fs::flock(file, rustix::fs::FlockOperation::NonBlockingLockExclusive)
                .map_err(|error| io::Error::from_raw_os_error(error.raw_os_error()))
        })
    }
    #[cfg(not(unix))]
    {
        let _ = waiting;
        Err(io::Error::new(
            io::ErrorKind::Unsupported,
            "descriptor lock waiting is unsupported",
        ))
    }
}

#[cfg(unix)]
fn wait_for_lock(
    poll_interval: Duration,
    mut waiting: impl FnMut(Duration) -> io::Result<()>,
    mut acquire: impl FnMut() -> io::Result<()>,
) -> io::Result<Duration> {
    let started = Instant::now();
    loop {
        match acquire() {
            Ok(()) => return Ok(started.elapsed()),
            Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
            Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
                waiting(started.elapsed())?;
                std::thread::sleep(poll_interval);
            }
            Err(error) => return Err(error),
        }
    }
}