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:
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};