use anyhow::{Context, Result};
use std::path::Path;
use std::sync::Arc;
pub(crate) type RouterLost = Arc<dyn Fn(String) + Send + Sync>;
#[derive(Debug)]
pub(crate) struct EmbeddedRouter {
router: crate::router::Router,
watch: crate::router::RouterWatch,
}
impl EmbeddedRouter {
pub(crate) async fn close(self) -> Result<()> {
if let Err(error) = self.watch.close().await {
tracing::debug!("failed to close the router watch: {error}");
}
self.router
.close()
.await
.context("failed to close the embedded router")
}
}
pub(crate) async fn start_embedded_router(
execution: crate::identity::ExecutionId,
endpoint: String,
on_lost: RouterLost,
) -> Result<EmbeddedRouter> {
validate_endpoint(&endpoint)?;
prepare_endpoint_parent(&endpoint)?;
let router = crate::router::Router::open(execution, std::slice::from_ref(&endpoint))
.await
.with_context(|| format!("failed to open the embedded router on {endpoint}"))?;
let lost_endpoint = endpoint.clone();
let watch = crate::router::RouterWatch::open(&endpoint, move || {
tracing::error!("the router at {lost_endpoint} is gone; the robot graph is unreachable");
on_lost(format!(
"the embedded router at {lost_endpoint} went away while the session was running"
));
})
.await
.with_context(|| format!("failed to watch the embedded router on {endpoint}"))?;
Ok(EmbeddedRouter { router, watch })
}
fn validate_endpoint(endpoint: &str) -> Result<()> {
anyhow::ensure!(
!endpoint.contains('#'),
"router endpoint {endpoint} carries a per-endpoint config fragment; that would override \
the listen settings which make a successful open mean the endpoint is bound"
);
Ok(())
}
fn unixsock_stream_path(endpoint: &str) -> Option<&Path> {
endpoint.strip_prefix("unixsock-stream/").map(Path::new)
}
fn prepare_endpoint_parent(endpoint: &str) -> Result<()> {
let Some(parent) = unixsock_stream_path(endpoint).and_then(Path::parent) else {
return Ok(());
};
std::fs::create_dir_all(parent).with_context(|| {
format!(
"failed to create the router socket directory {}",
parent.display()
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_unix_socket_endpoint_yields_its_path() {
assert_eq!(
unixsock_stream_path("unixsock-stream//tmp/phoxal/router.sock"),
Some(Path::new("/tmp/phoxal/router.sock"))
);
assert_eq!(unixsock_stream_path("tcp/127.0.0.1:7447"), None);
}
#[test]
fn endpoint_parent_creation_is_a_pure_filesystem_operation() {
let root = tempfile::tempdir().expect("temp directory");
let socket = root.path().join("run/phoxal/router.sock");
let endpoint = format!("unixsock-stream/{}", socket.display());
prepare_endpoint_parent(&endpoint).expect("create endpoint parent");
assert!(socket.parent().expect("socket parent").is_dir());
assert!(
!socket.exists(),
"preparation must not bind or create the socket"
);
}
#[test]
fn an_endpoint_config_fragment_is_rejected_before_transport_open() {
let error = validate_endpoint("tcp/127.0.0.1:7447#exit_on_failure=false")
.expect_err("a per-endpoint config fragment must be rejected");
assert!(error.to_string().contains("config fragment"), "{error:#}");
validate_endpoint("tcp/127.0.0.1:7447").expect("a plain endpoint is accepted");
}
}