From c3aa8a4bb5873cb1dc2558dd8c37fc0ed0b532aa Mon Sep 17 00:00:00 2001 From: Sean Evans Date: Wed, 1 Apr 2026 13:49:12 -0400 Subject: [PATCH] Refactor daemon readiness socket parsing to explicit URI schemes --- README.md | 18 +++++ daemon/README.md | 18 +++++ daemon/openastrovizd/README.md | 23 ++++++ daemon/openastrovizd/src/daemon.rs | 113 +++++++++++++++++++++++++++-- 4 files changed, 165 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 98022db..4cedd99 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,24 @@ If you don’t have an NVIDIA GPU, skip `cuda/` and work on the **cpu‑simd** r The validated Vallado SGP4 implementation now lives in the [`core/`](core) crate and is exercised by regression tests against published state vectors. The earlier Keplerian proof‑of‑concept has been quarantined to [`docs/archive/poc_sgp.cpp`](docs/archive/poc_sgp.cpp) for historical reference. +### Daemon readiness socket format + +When using daemon startup readiness checks, `OPENASTROVIZD_SOCKET` now requires +an explicit scheme: + +* `tcp://:` for TCP socket readiness checks. +* `unix:///absolute/path/to/socket` for Unix domain socket/file readiness checks. +* `file:///absolute/path/to/socket-or-marker` for filesystem path readiness checks. + +Examples: + +```bash +OPENASTROVIZD_SOCKET=tcp://127.0.0.1:8765 +OPENASTROVIZD_SOCKET=unix:///tmp/openastrovizd.sock +OPENASTROVIZD_SOCKET=file:///tmp/openastrovizd.ready +OPENASTROVIZD_SOCKET=file:///C:/Temp/openastrovizd.ready # Windows +``` + --- ## 🧩 Contributing diff --git a/daemon/README.md b/daemon/README.md index 33b3e93..bc6c310 100644 --- a/daemon/README.md +++ b/daemon/README.md @@ -15,4 +15,22 @@ process ID to a file in the system temporary directory. Subsequent `status` checks read this file and verify that the process is still alive, providing a simple way to monitor the daemon. +## Environment variables for readiness + +When launching with readiness checks, `OPENASTROVIZD_SOCKET` must use one of +the following URI formats: + +- `tcp://host:port` +- `unix:///path/to/socket` +- `file:///path/to/ready/file` + +Examples: + +```bash +OPENASTROVIZD_SOCKET=tcp://localhost:8765 +OPENASTROVIZD_SOCKET=unix:///tmp/openastrovizd.sock +OPENASTROVIZD_SOCKET=file:///tmp/openastrovizd.ready +OPENASTROVIZD_SOCKET=file:///C:/Temp/openastrovizd.ready # Windows +``` + For detailed instructions and advanced options, see the [openastrovizd crate README](openastrovizd/README.md) or the project documentation. diff --git a/daemon/openastrovizd/README.md b/daemon/openastrovizd/README.md index 0765850..3ef00ce 100644 --- a/daemon/openastrovizd/README.md +++ b/daemon/openastrovizd/README.md @@ -40,3 +40,26 @@ $ cargo run -p openastrovizd -- bench cuda # benchmark the CUDA backend ``` The daemon is the link between the high‑level web interface and the low‑level compute kernels, serving orbit propagation results over local APIs. + +## Startup environment variables + +`openastrovizd start` supports these environment variables: + +- `OPENASTROVIZD_DAEMON_CMD`: override daemon executable path. +- `OPENASTROVIZD_DAEMON_ARGS`: override daemon arguments. +- `OPENASTROVIZD_CONFIG`: config file passed as `--config `. +- `OPENASTROVIZD_READY_TIMEOUT_MS`: readiness timeout (milliseconds). +- `OPENASTROVIZD_SOCKET`: readiness target URI with an explicit scheme: + - `tcp://host:port` + - `unix:///path/to/socket` + - `file:///path/to/ready/file` + +Examples: + +```bash +OPENASTROVIZD_SOCKET=tcp://127.0.0.1:8765 +OPENASTROVIZD_SOCKET=unix:///tmp/openastrovizd.sock +OPENASTROVIZD_SOCKET=file:///tmp/openastrovizd.ready +OPENASTROVIZD_SOCKET=file:///C:/Temp/openastrovizd.ready # Windows path +OPENASTROVIZD_READY_TIMEOUT_MS=8000 +``` diff --git a/daemon/openastrovizd/src/daemon.rs b/daemon/openastrovizd/src/daemon.rs index fe70367..3b76f41 100644 --- a/daemon/openastrovizd/src/daemon.rs +++ b/daemon/openastrovizd/src/daemon.rs @@ -1,8 +1,8 @@ use std::env; use std::fs; use std::io; -use std::net::TcpStream; -use std::path::{Path, PathBuf}; +use std::net::{TcpStream, ToSocketAddrs}; +use std::path::PathBuf; use std::process::{Child, Command, Stdio}; use std::thread; use std::time::{Duration, Instant}; @@ -78,6 +78,12 @@ struct DaemonConfig { readiness_timeout: Duration, } +#[derive(Debug, Clone)] +enum ReadinessTarget { + Tcp(String), + Path(PathBuf), +} + impl DaemonConfig { fn from_env() -> io::Result { let command = env::var("OPENASTROVIZD_DAEMON_CMD") @@ -200,6 +206,11 @@ fn process_running(pid: u32) -> bool { } fn wait_for_readiness(child: &mut Child, config: &DaemonConfig) -> io::Result<()> { + let readiness_target = config + .readiness_socket + .as_deref() + .map(parse_readiness_target) + .transpose()?; let start = Instant::now(); loop { @@ -218,8 +229,8 @@ fn wait_for_readiness(child: &mut Child, config: &DaemonConfig) -> io::Result<() return Err(io::Error::new(io::ErrorKind::Other, format!("{msg}{detail}"))); } - if let Some(socket) = &config.readiness_socket { - if socket_ready(socket) { + if let Some(target) = &readiness_target { + if readiness_target_ready(target) { return Ok(()); } } @@ -259,12 +270,77 @@ fn forward_child_stderr(child: &mut Child) { } } +#[cfg(test)] fn socket_ready(target: &str) -> bool { - if target.contains(':') { - TcpStream::connect(target).is_ok() + parse_readiness_target(target) + .map(|parsed| readiness_target_ready(&parsed)) + .unwrap_or(false) +} + +fn parse_readiness_target(target: &str) -> io::Result { + let (scheme, location) = target.split_once("://").ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "OPENASTROVIZD_SOCKET must include a scheme: tcp://, unix://, or file://", + ) + })?; + + match scheme { + "tcp" => { + let mut addrs = location.to_socket_addrs().map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("Invalid tcp socket address `{location}`: {e}"), + ) + })?; + if addrs.next().is_none() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Invalid tcp socket address `{location}`"), + )); + } + Ok(ReadinessTarget::Tcp(location.to_string())) + } + "file" | "unix" => { + if location.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("{}:// must include a filesystem path", scheme), + )); + } + Ok(ReadinessTarget::Path(path_from_uri(location))) + } + _ => Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "Unsupported OPENASTROVIZD_SOCKET scheme `{scheme}`; expected tcp://, unix://, or file://" + ), + )), + } +} + +#[cfg(windows)] +fn path_from_uri(location: &str) -> PathBuf { + let bytes = location.as_bytes(); + if bytes.len() >= 3 && bytes[0] == b'/' && bytes[2] == b':' { + PathBuf::from(&location[1..]) } else { - Path::new(target).exists() + PathBuf::from(location) + } +} + +#[cfg(not(windows))] +fn path_from_uri(location: &str) -> PathBuf { + PathBuf::from(location) +} + +fn readiness_target_ready(target: &ReadinessTarget) -> bool { + match target { + ReadinessTarget::Tcp(addr) => TcpStream::connect(addr).is_ok(), + ReadinessTarget::Path(path) => path.exists(), } +} + #[cfg(unix)] fn is_zombie(pid: u32) -> bool { let stat_path = format!("/proc/{pid}/stat"); @@ -496,4 +572,27 @@ mod tests { util::cleanup(); } + + #[test] + fn socket_ready_requires_scheme() { + assert!(!socket_ready("127.0.0.1:4242")); + assert!(!socket_ready("/tmp/openastrovizd.sock")); + } + + #[test] + fn parse_readiness_target_rejects_invalid_tcp_target() { + let err = parse_readiness_target("tcp://not a socket").expect_err("must reject bad tcp"); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + } + + #[test] + fn parse_readiness_target_accepts_file_and_unix_schemes() { + let file_target = + parse_readiness_target("file:///tmp/openastrovizd.sock").expect("file uri parses"); + let unix_target = + parse_readiness_target("unix:///tmp/openastrovizd.sock").expect("unix uri parses"); + + assert!(matches!(file_target, ReadinessTarget::Path(_))); + assert!(matches!(unix_target, ReadinessTarget::Path(_))); + } }