Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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://<host>:<port>` 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
Expand Down
18 changes: 18 additions & 0 deletions daemon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
23 changes: 23 additions & 0 deletions daemon/openastrovizd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>`.
- `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
```
111 changes: 104 additions & 7 deletions daemon/openastrovizd/src/daemon.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<Self> {
let command = env::var("OPENASTROVIZD_DAEMON_CMD").or_else(|_| default_binary_path())?;
Expand Down Expand Up @@ -199,6 +205,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 {
Expand All @@ -220,8 +231,8 @@ fn wait_for_readiness(child: &mut Child, config: &DaemonConfig) -> io::Result<()
));
}

if let Some(socket) = &config.readiness_socket {
if socket_ready(socket) {
if let Some(target) = &readiness_target {
if readiness_target_ready(target) {
return Ok(());
}
}
Expand Down Expand Up @@ -261,11 +272,74 @@ 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<ReadinessTarget> {
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(),
}
}

Expand Down Expand Up @@ -501,4 +575,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(_)));
}
}
Loading