//! `$FLOW_HOME` (default `~/.local/share/flow`): the pinned parts of each //! isolation tier, rootfs images by hash, scratch space and the runner's //! configuration, all described by `flow.toml`. //! //! ```toml //! [microvm] //! firecracker = { path = "bin/firecracker", blake3 = "…" } //! kernel = { path = "kernel/vmlinux", blake3 = "…" } //! init = { path = "bin/flow-init", blake3 = "…" } //! //! [runner] # only for `flow runner` //! hub = "https://hub.example:7343" //! token = "…" # a runner token from the hub's [flow] runner_tokens //! ca = "/path/to/hub-ca.pem" # when the hub's certificate is self-signed //! name = "earth-1" //! ``` use anyhow::{bail, Context, Result}; use serde::Deserialize; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Mutex; #[derive(Clone, Debug, Deserialize)] pub struct Pinned { pub path: PathBuf, pub blake3: String, } #[derive(Clone, Debug, Deserialize)] pub struct MicrovmConfig { pub firecracker: Pinned, pub kernel: Pinned, pub init: Pinned, } #[derive(Clone, Debug, Deserialize)] pub struct RunnerConfig { pub hub: String, pub token: String, pub ca: Option, pub name: Option, } #[derive(Clone, Debug, Default, Deserialize)] pub struct Config { pub microvm: Option, pub runner: Option, } pub struct Home { pub root: PathBuf, pub config: Config, /// Files already verified, by path, with the (len, mtime) they had then. verified: Mutex>, } impl Home { pub fn locate(explicit: Option<&Path>) -> Result { let root = match explicit { Some(p) => p.to_path_buf(), None => match std::env::var_os("FLOW_HOME") { Some(p) => PathBuf::from(p), None => PathBuf::from(std::env::var_os("HOME").context("HOME is not set")?).join(".local/share/flow"), }, }; let file = root.join("flow.toml"); let config = if file.is_file() { let text = std::fs::read_to_string(&file).with_context(|| format!("read {}", file.display()))?; toml::from_str(&text).with_context(|| format!("parse {}", file.display()))? } else { Config::default() }; for d in ["cas", "work", "repos"] { std::fs::create_dir_all(root.join(d)).with_context(|| format!("create {}", root.join(d).display()))?; } Ok(Home { root, config, verified: Mutex::new(HashMap::new()) }) } pub fn resolve(&self, p: &Path) -> PathBuf { if p.is_absolute() { p.to_path_buf() } else { self.root.join(p) } } /// Where a rootfs image with this hash lives. pub fn rootfs(&self, hash: &str) -> PathBuf { self.root.join("cas").join(format!("{hash}.ext4")) } /// A pinned file, checked against its BLAKE3 before use (again only /// when its size or modification time changed since). pub fn verified(&self, path: &Path, blake3_hex: &str) -> Result { let meta = std::fs::metadata(path).with_context(|| format!("{} is missing", path.display()))?; let stamp = (meta.len(), meta.modified()?); if self.verified.lock().unwrap().get(path) == Some(&stamp) { return Ok(path.to_path_buf()); } let mut got = hash_file(path)?; if got != blake3_hex { // The read may have come from bad cached pages rather than the // disk (oxygenflow#22: a RAM fault on Earth failed every job for // hours while the file was intact): drop the file's pages and // read it again from the disk before giving up. drop_cached(path); let again = hash_file(path)?; if again == blake3_hex { eprintln!( "warning: {} hashed {got} from the page cache but {again} from the disk: the cached copy was bad (check this machine's memory)", path.display() ); } got = again; } if got != blake3_hex { bail!("{} has BLAKE3 {got}, but {blake3_hex} is pinned", path.display()); } self.verified.lock().unwrap().insert(path.to_path_buf(), stamp); Ok(path.to_path_buf()) } } /// Ask the kernel to forget a file's cached pages, so the next read comes /// from the disk. Best effort: dirty pages and other platforms stay. fn drop_cached(path: &Path) { #[cfg(target_os = "linux")] if let Ok(f) = std::fs::File::open(path) { use std::os::fd::AsRawFd; // SAFETY: a valid descriptor; the advice only affects caching. unsafe { libc::posix_fadvise(f.as_raw_fd(), 0, 0, libc::POSIX_FADV_DONTNEED); } } #[cfg(not(target_os = "linux"))] let _ = path; } pub fn hash_file(path: &Path) -> Result { let mut h = blake3::Hasher::new(); h.update_reader(std::fs::File::open(path).with_context(|| format!("open {}", path.display()))?)?; Ok(h.finalize().to_hex().to_string()) }