lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 932ea04384e60fb50f317d26aa100b1ea4441559
parent 6df91556a60cb78abebb7912d05fc83a2301037a
Author: triesap <tyson@radroots.org>
Date:   Fri, 22 Aug 2025 16:41:06 -0700

runtime: update `radroots-runtime` adding json util for atomic read/write of state files

Diffstat:
MCargo.lock | 96++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
MCargo.toml | 5+++--
Mcrates/runtime/Cargo.toml | 5+++--
Acrates/runtime/src/json.rs | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/runtime/src/lib.rs | 8+++++---
5 files changed, 247 insertions(+), 12 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -423,7 +423,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom", + "getrandom 0.2.16", "once_cell", "tiny-keccak", ] @@ -540,6 +540,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] name = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -567,11 +583,23 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -852,6 +880,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] name = "litemap" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -900,7 +934,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -927,7 +961,7 @@ dependencies = [ "cbc", "chacha20", "chacha20poly1305", - "getrandom", + "getrandom 0.2.16", "instant", "scrypt", "secp256k1", @@ -1142,6 +1176,12 @@ dependencies = [ ] [[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] name = "radroots-core" version = "0.1.0" dependencies = [ @@ -1188,6 +1228,8 @@ dependencies = [ "clap", "config", "serde", + "serde_json", + "tempfile", "thiserror 1.0.69", "tokio", "toml", @@ -1234,7 +1276,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", ] [[package]] @@ -1331,6 +1373,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.60.2", +] + +[[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1512,6 +1567,19 @@ dependencies = [ ] [[package]] +name = "tempfile" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.60.2", +] + +[[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1875,6 +1943,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] name = "wasm-bindgen" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2180,6 +2257,15 @@ dependencies = [ ] [[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] name = "writeable" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -18,16 +18,17 @@ radroots-trade = { path = "crates/trade", version = "0.1.0", default-features = anyhow = { version = "1" } clap = { version = "4" } +config = { version = "0.14" } nostr = { version = "0.43.0" } serde = { version = "1", default-features = false } serde_json = { version = "1", default-features = false } rust_decimal = { version = "1", default-features = false } rust_decimal_macros = { version = "1" } +tempfile = { version = "3" } thiserror = { version = "1" } tokio = { version = "1" } +toml = { version = "0.8" } tracing = { version = "0.1" } tracing-subscriber = { version = "0.3" } tracing-appender = { version = "0.2" } typeshare = { version = "1" } -toml = { version = "0.8" } -config = { version = "0.14" } diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml @@ -15,9 +15,11 @@ anyhow = { workspace = true } clap = { workspace = true, features = ["derive", "env"], optional = true } config = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] } toml = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["fmt", "env-filter"] } -tracing-appender = { workspace = true } -\ No newline at end of file +tracing-appender = { workspace = true } +tempfile = { workspace = true } diff --git a/crates/runtime/src/json.rs b/crates/runtime/src/json.rs @@ -0,0 +1,145 @@ +use serde::{Serialize, de::DeserializeOwned}; +use std::{ + fs, + io::{self, Write}, + path::{Path, PathBuf}, +}; +use tempfile::NamedTempFile; +use thiserror::Error; + +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; + +#[derive(Debug, Error)] +pub enum RuntimeJsonError { + #[error("JSON file does not exist at {0}")] + NotFound(PathBuf), + + #[error("Failed to open JSON file at {0}: {1}")] + FileOpen(PathBuf, #[source] io::Error), + + #[error("Failed to parse JSON at {0}: {1}")] + FileParse(PathBuf, #[source] serde_json::Error), + + #[error("Failed to serialize JSON: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("I/O error during JSON write: {0}")] + Io(#[from] io::Error), + + #[error("Failed to persist JSON file to disk: {0}")] + Persist(#[from] tempfile::PersistError), +} + +#[derive(Debug, Clone)] +pub struct JsonWriteOptions { + pub pretty: bool, + pub mode_unix: Option<u32>, +} + +impl Default for JsonWriteOptions { + fn default() -> Self { + Self { + pretty: false, + mode_unix: Some(0o600), + } + } +} + +#[derive(Debug, Clone)] +pub struct JsonFile<T> { + pub value: T, + path: PathBuf, + options: JsonWriteOptions, +} + +impl<T> JsonFile<T> { + pub fn path(&self) -> &Path { + &self.path + } + + pub fn set_options(&mut self, options: JsonWriteOptions) { + self.options = options; + } +} + +impl<T> JsonFile<T> +where + T: Serialize + DeserializeOwned, +{ + pub fn load(path: impl AsRef<Path>) -> Result<Self, RuntimeJsonError> { + let p = path.as_ref().to_path_buf(); + if !p.exists() { + return Err(RuntimeJsonError::NotFound(p)); + } + let file = std::fs::File::open(&p).map_err(|e| RuntimeJsonError::FileOpen(p.clone(), e))?; + let reader = std::io::BufReader::new(file); + let value = serde_json::from_reader(reader) + .map_err(|e| RuntimeJsonError::FileParse(p.clone(), e))?; + Ok(Self { + value, + path: p, + options: JsonWriteOptions::default(), + }) + } + + pub fn load_or_create_with<F>(path: impl AsRef<Path>, init: F) -> Result<Self, RuntimeJsonError> + where + F: FnOnce() -> T, + { + let p = path.as_ref().to_path_buf(); + if p.exists() { + return Self::load(p); + } + let s = Self { + value: init(), + path: p, + options: JsonWriteOptions::default(), + }; + s.save()?; + Ok(s) + } + + pub fn save(&self) -> Result<(), RuntimeJsonError> { + self.save_as(&self.path) + } + + pub fn save_as(&self, new_path: impl AsRef<Path>) -> Result<(), RuntimeJsonError> { + let json = if self.options.pretty { + serde_json::to_string_pretty(&self.value)? + } else { + serde_json::to_string(&self.value)? + }; + atomic_write_json(new_path.as_ref(), json.as_bytes(), self.options.mode_unix)?; + Ok(()) + } + + pub fn modify<F>(&mut self, f: F) -> Result<(), RuntimeJsonError> + where + F: FnOnce(&mut T), + { + f(&mut self.value); + self.save() + } +} + +fn atomic_write_json( + path: &Path, + bytes: &[u8], + mode_unix: Option<u32>, +) -> Result<(), RuntimeJsonError> { + let dir = path.parent().unwrap_or_else(|| Path::new(".")); + fs::create_dir_all(dir).ok(); + + let mut tmp = NamedTempFile::new_in(dir)?; + tmp.write_all(bytes)?; + tmp.as_file_mut().sync_all()?; + + #[cfg(unix)] + if let Some(mode) = mode_unix { + fs::set_permissions(tmp.path(), fs::Permissions::from_mode(mode))?; + } + + tmp.persist(path)?; + Ok(()) +} diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs @@ -2,8 +2,9 @@ pub mod cli; pub mod config; pub mod error; -pub mod tracing; +pub mod json; pub mod signals; +pub mod tracing; #[cfg(feature = "cli")] pub use cli::{parse_and_load_path, parse_and_load_path_with_env_overrides}; @@ -12,9 +13,10 @@ pub use config::{ load_required_file, load_required_file_with_env, load_required_file_with_env_and_overrides, }; -pub use error::{RuntimeConfigError, RuntimeError, RuntimeTracingError}; #[cfg(feature = "cli")] pub use error::RuntimeCliError; +pub use error::{RuntimeConfigError, RuntimeError, RuntimeTracingError}; -pub use signals::{shutdown_signal}; +pub use json::{JsonFile, JsonWriteOptions, RuntimeJsonError}; +pub use signals::shutdown_signal; pub use tracing::{init, init_with};