hardpass-vm 0.3.1

A small, reliable Ubuntu cloud-image VM manager built on QEMU.
Documentation
use std::ffi::OsString;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use fs2::FileExt;

pub(crate) struct FileLock {
    file: std::fs::File,
}

impl Drop for FileLock {
    fn drop(&mut self) {
        let _ = self.file.unlock();
    }
}

pub(crate) async fn lock_file(path: impl AsRef<Path>) -> Result<FileLock> {
    let path = path.as_ref().to_path_buf();
    tokio::task::spawn_blocking(move || lock_file_blocking(&path)).await?
}

pub(crate) fn sibling_lock_path(path: &Path) -> PathBuf {
    let file_name = path
        .file_name()
        .map(|name| {
            let mut name = OsString::from(name);
            name.push(".lock");
            name
        })
        .unwrap_or_else(|| OsString::from(".lock"));
    match path.parent() {
        Some(parent) => parent.join(file_name),
        None => PathBuf::from(file_name),
    }
}

fn lock_file_blocking(path: &Path) -> Result<FileLock> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .with_context(|| format!("create lock dir {}", parent.display()))?;
    }
    let file = std::fs::OpenOptions::new()
        .create(true)
        .truncate(false)
        .read(true)
        .write(true)
        .open(path)
        .with_context(|| format!("open lock file {}", path.display()))?;
    file.lock_exclusive()
        .with_context(|| format!("lock {}", path.display()))?;
    Ok(FileLock { file })
}

#[cfg(test)]
mod tests {
    use std::path::Path;
    use std::process::Stdio;
    use std::time::Duration;

    use tempfile::tempdir;
    use tokio::io::{AsyncBufReadExt, BufReader};
    use tokio::process::Command;
    use tokio::time::timeout;

    use super::{lock_file, sibling_lock_path};

    #[test]
    fn sibling_lock_path_adds_lock_suffix() {
        assert_eq!(
            sibling_lock_path(Path::new("/tmp/dev")),
            Path::new("/tmp/dev.lock")
        );
    }

    #[tokio::test]
    async fn exclusive_lock_blocks_other_processes() {
        let dir = tempdir().expect("tempdir");
        let lock_path = dir.path().join("instance.lock");
        let lock = lock_file(&lock_path).await.expect("parent lock");

        let mut child = Command::new("python3")
            .arg("-c")
            .arg(
                r#"
import fcntl
import pathlib
import sys

path = pathlib.Path(sys.argv[1])
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "a+") as fh:
    fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
    print("locked", flush=True)
"#,
            )
            .arg(&lock_path)
            .stdout(Stdio::piped())
            .spawn()
            .expect("spawn child");

        let stdout = child.stdout.take().expect("child stdout");
        let mut lines = BufReader::new(stdout).lines();

        assert!(
            timeout(Duration::from_millis(200), lines.next_line())
                .await
                .is_err(),
            "child acquired lock before parent released it"
        );

        drop(lock);

        let line = timeout(Duration::from_secs(5), lines.next_line())
            .await
            .expect("child lock wait timed out")
            .expect("read child stdout")
            .expect("child line");
        assert_eq!(line, "locked");

        let status = timeout(Duration::from_secs(5), child.wait())
            .await
            .expect("child exit wait timed out")
            .expect("wait for child");
        assert!(status.success());
    }
}