//! Per-agent configuration registries: resource limits, topology (JSON files //! under `/var/lib/hyperhive/meta/`), the permissions and archetypes the //! swarm sets (outside the meta repo) and the shared wire-protocol size //! limits. Each submodule is re-exported at the crate root, so //! `crate::topology::…` etc. keep working unchanged. //! //! Note the two similarly-named modules: [`limits`] caps inline //! *message body* sizes on the sockets, while [`resource_limits`] holds //! per-agent CPU/memory caps for the container drop-in. pub mod archetypes; pub mod limits; pub mod permissions; pub mod resource_limits; pub mod topology; use std::io::{self, Write as _}; use std::path::Path; use serde::Serialize; use serde::de::DeserializeOwned; /// Read one of these JSON registries. A missing file is `T::default()` /// (the empty map or set); any other read failure, or content that /// doesn't parse, is an error, so a read-modify-write can't write back a /// registry that has lost every other agent's entry. fn read_map(path: &Path) -> io::Result { let raw = match std::fs::read_to_string(path) { Ok(raw) => raw, Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(T::default()), Err(e) => { return Err(io::Error::new( e.kind(), format!("read {}: {e}", path.display()), )); } }; serde_json::from_str(&raw).map_err(|e| { io::Error::new( io::ErrorKind::InvalidData, format!("parse {}: {e}", path.display()), ) }) } /// Serialise `value` as pretty JSON and replace `path` with it atomically: /// a temp file in the same directory (`rename(2)` is only atomic within a /// filesystem), fsynced, renamed over `path`, then the directory fsynced /// so the rename itself survives a crash. A reader sees the old file or /// the new one, never a truncated one. fn write_map(path: &Path, value: &T) -> io::Result<()> { let text = serde_json::to_string_pretty(value) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; let (Some(dir), Some(name)) = (path.parent(), path.file_name()) else { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("{} has no parent directory or file name", path.display()), )); }; std::fs::create_dir_all(dir)?; let tmp = dir.join(format!(".{}.tmp", name.to_string_lossy())); let mut file = std::fs::File::create(&tmp)?; file.write_all(format!("{text}\n").as_bytes())?; file.sync_all()?; drop(file); std::fs::rename(&tmp, path)?; std::fs::File::open(dir)?.sync_all() } #[cfg(test)] mod tests { use std::collections::BTreeMap; use super::*; type Map = BTreeMap>; #[test] fn write_map_round_trips_and_leaves_no_temp_file() { let dir = tempfile::tempdir().expect("tempdir"); let path = dir.path().join("map.json"); let map = BTreeMap::from([("alice".to_owned(), vec!["messaging".to_owned()])]); write_map(&path, &map).expect("first write"); let replaced = BTreeMap::from([("bob".to_owned(), vec!["inbox".to_owned()])]); write_map(&path, &replaced).expect("replacing write"); assert_eq!(read_map::(&path).expect("read"), replaced); let entries: Vec<_> = std::fs::read_dir(dir.path()) .expect("read_dir") .map(|e| e.expect("entry").file_name()) .collect(); assert_eq!(entries, vec![std::ffi::OsString::from("map.json")]); } #[test] fn read_map_of_a_missing_file_is_empty() { let dir = tempfile::tempdir().expect("tempdir"); let map = read_map::(&dir.path().join("absent.json")).expect("missing file is not an error"); assert!(map.is_empty()); } #[test] fn read_map_of_a_truncated_file_is_an_error() { let dir = tempfile::tempdir().expect("tempdir"); let path = dir.path().join("map.json"); std::fs::write(&path, "{\n \"alice\": [\"messag").expect("seed"); let err = read_map::(&path).expect_err("truncated JSON must not read as a map"); assert_eq!(err.kind(), io::ErrorKind::InvalidData); } }