harn-vm 0.10.18

Async bytecode virtual machine for the Harn programming language
Documentation
use std::io;
use std::io::Write as _;
use std::path::Path;
use std::time::Duration;

use super::ScopedMutationTarget;

#[derive(Clone, Copy, Debug)]
pub(crate) struct AppendLockOptions {
    pub timeout: Duration,
    pub sync_data: bool,
}

pub(super) fn append_locked_unscoped(
    path: &Path,
    contents: &[u8],
    options: AppendLockOptions,
) -> io::Result<()> {
    if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
        std::fs::create_dir_all(parent)?;
    }
    let file = std::fs::OpenOptions::new()
        .create(true)
        .append(true)
        .read(true)
        .open(path)?;
    let _lock = lock_file_exclusive(&file, path, options.timeout)?;
    let mut writer = &file;
    writer.write_all(contents)?;
    if options.sync_data {
        file.sync_data()?;
    }
    Ok(())
}

#[cfg(unix)]
pub(super) fn append_locked_scoped_target(
    target: &ScopedMutationTarget,
    contents: &[u8],
    options: AppendLockOptions,
) -> io::Result<()> {
    use std::os::fd::AsRawFd;

    let (parent, file_name) = super::ensure_parent_dirs_scoped(target)?;
    let file = super::openat_file(
        parent.as_raw_fd(),
        &file_name,
        libc::O_RDWR | libc::O_CREAT | libc::O_APPEND | libc::O_CLOEXEC | libc::O_NOFOLLOW,
        0o666,
    )?;
    let full = target.root.join(&target.relative);
    let _lock = lock_file_exclusive(&file, &full, options.timeout)?;
    let mut writer = &file;
    writer.write_all(contents)?;
    if options.sync_data {
        file.sync_data()?;
    }
    Ok(())
}

#[cfg(windows)]
pub(super) fn append_locked_scoped_target(
    target: &ScopedMutationTarget,
    contents: &[u8],
    options: AppendLockOptions,
) -> io::Result<()> {
    let (parent, file_name) = super::win_scoped_parent(target, true)?;
    let full = parent.join(&file_name);
    super::win_reject_reparse_leaf(&full)?;
    append_locked_unscoped(&full, contents, options)
}

#[cfg(all(not(unix), not(windows)))]
pub(super) fn append_locked_scoped_target(
    target: &ScopedMutationTarget,
    contents: &[u8],
    options: AppendLockOptions,
) -> io::Result<()> {
    let full = target.root.join(&target.relative);
    if let Some(parent) = full.parent().filter(|p| !p.as_os_str().is_empty()) {
        std::fs::create_dir_all(parent)?;
    }
    append_locked_unscoped(&full, contents, options)
}

struct FileLockGuard<'a>(&'a std::fs::File);

impl Drop for FileLockGuard<'_> {
    fn drop(&mut self) {
        let _ = fs2::FileExt::unlock(self.0);
    }
}

fn lock_file_exclusive<'a>(
    file: &'a std::fs::File,
    path: &Path,
    timeout: Duration,
) -> io::Result<FileLockGuard<'a>> {
    let timeout_ms = duration_millis_i64(timeout);
    let deadline_ms = crate::stdlib::clock::now_monotonic_ms().saturating_add(timeout_ms);
    let mut backoff = Duration::from_millis(10);
    loop {
        match fs2::FileExt::try_lock_exclusive(file) {
            Ok(()) => return Ok(FileLockGuard(file)),
            Err(error) if lock_is_contended(&error) => {
                let now_ms = crate::stdlib::clock::now_monotonic_ms();
                if timeout.is_zero() || now_ms >= deadline_ms {
                    return Err(io::Error::new(
                        io::ErrorKind::TimedOut,
                        format!(
                            "timed out after {}ms waiting for append lock `{}`",
                            timeout.as_millis(),
                            path.display()
                        ),
                    ));
                }
                let remaining = Duration::from_millis(deadline_ms.saturating_sub(now_ms) as u64);
                wait_for_retry(backoff.min(remaining));
                backoff = (backoff * 2).min(Duration::from_millis(100));
            }
            Err(error) => return Err(error),
        }
    }
}

fn duration_millis_i64(duration: Duration) -> i64 {
    duration.as_millis().min(i64::MAX as u128) as i64
}

fn wait_for_retry(duration: Duration) {
    if duration.is_zero() {
        return;
    }
    if crate::stdlib::clock::is_mocked() {
        crate::stdlib::clock::advance(duration_millis_i64(duration));
        return;
    }
    std::thread::sleep(duration);
}

fn lock_is_contended(error: &io::Error) -> bool {
    error.kind() == io::ErrorKind::WouldBlock
        || error.raw_os_error() == fs2::lock_contended_error().raw_os_error()
}