use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{UnixListener, UnixStream};
const MAX_FRAME_SIZE: u32 = 16 * 1024 * 1024;
pub struct IpcServer {
listener: UnixListener,
}
pub struct IpcConnection {
stream: UnixStream,
}
pub struct IpcClient {
stream: UnixStream,
}
impl IpcServer {
pub async fn bind(path: &std::path::Path) -> crate::core::Result<Self> {
match std::fs::remove_file(path) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => {
return Err(crate::core::AgnosaiError::Ipc(format!(
"failed to remove stale socket: {e}"
)));
}
}
let listener = UnixListener::bind(path)
.map_err(|e| crate::core::AgnosaiError::Ipc(format!("failed to bind socket: {e}")))?;
Ok(Self { listener })
}
pub async fn accept(&self) -> crate::core::Result<IpcConnection> {
let (stream, _addr) = self
.listener
.accept()
.await
.map_err(|e| crate::core::AgnosaiError::Ipc(format!("accept failed: {e}")))?;
Ok(IpcConnection { stream })
}
}
impl IpcConnection {
pub async fn send(&mut self, payload: &serde_json::Value) -> crate::core::Result<()> {
let data = serde_json::to_vec(payload)?;
write_frame(&mut self.stream, &data).await
}
pub async fn recv(&mut self) -> crate::core::Result<serde_json::Value> {
let data = read_frame(&mut self.stream).await?;
let value = serde_json::from_slice(&data)?;
Ok(value)
}
}
impl IpcClient {
pub async fn connect(path: &std::path::Path) -> crate::core::Result<Self> {
let stream = UnixStream::connect(path)
.await
.map_err(|e| crate::core::AgnosaiError::Ipc(format!("connect failed: {e}")))?;
Ok(Self { stream })
}
pub async fn send(&mut self, payload: &serde_json::Value) -> crate::core::Result<()> {
let data = serde_json::to_vec(payload)?;
write_frame(&mut self.stream, &data).await
}
pub async fn recv(&mut self) -> crate::core::Result<serde_json::Value> {
let data = read_frame(&mut self.stream).await?;
let value = serde_json::from_slice(&data)?;
Ok(value)
}
}
async fn write_frame(stream: &mut UnixStream, data: &[u8]) -> crate::core::Result<()> {
let len = u32::try_from(data.len()).map_err(|_| {
crate::core::AgnosaiError::Ipc(format!("frame too large: {} bytes", data.len()))
})?;
if len > MAX_FRAME_SIZE {
return Err(crate::core::AgnosaiError::Ipc(format!(
"frame size {len} exceeds maximum {MAX_FRAME_SIZE}"
)));
}
stream
.write_all(&len.to_be_bytes())
.await
.map_err(|e| crate::core::AgnosaiError::Ipc(format!("write length failed: {e}")))?;
stream
.write_all(data)
.await
.map_err(|e| crate::core::AgnosaiError::Ipc(format!("write payload failed: {e}")))?;
stream
.flush()
.await
.map_err(|e| crate::core::AgnosaiError::Ipc(format!("flush failed: {e}")))?;
Ok(())
}
async fn read_frame(stream: &mut UnixStream) -> crate::core::Result<Vec<u8>> {
let mut len_buf = [0u8; 4];
stream.read_exact(&mut len_buf).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::UnexpectedEof {
crate::core::AgnosaiError::Ipc("peer disconnected (EOF on length read)".into())
} else {
crate::core::AgnosaiError::Ipc(format!("read length failed: {e}"))
}
})?;
let len = u32::from_be_bytes(len_buf);
if len == 0 {
return Err(crate::core::AgnosaiError::Ipc(
"empty frame (zero-length)".into(),
));
}
if len > MAX_FRAME_SIZE {
return Err(crate::core::AgnosaiError::Ipc(format!(
"frame size {len} exceeds maximum {MAX_FRAME_SIZE}"
)));
}
let mut buf = vec![0u8; len as usize];
stream.read_exact(&mut buf).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::UnexpectedEof {
crate::core::AgnosaiError::Ipc(format!(
"peer disconnected mid-frame (expected {len} bytes)"
))
} else {
crate::core::AgnosaiError::Ipc(format!("read payload failed: {e}"))
}
})?;
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn socket_path(dir: &std::path::Path) -> std::path::PathBuf {
dir.join("test.sock")
}
#[tokio::test]
async fn server_bind_and_client_connect() {
let tmp = tempfile::tempdir().unwrap();
let path = socket_path(tmp.path());
let server = IpcServer::bind(&path).await.unwrap();
let client_handle = tokio::spawn({
let path = path.clone();
async move {
IpcClient::connect(&path).await.unwrap();
}
});
let _conn = server.accept().await.unwrap();
client_handle.await.unwrap();
}
#[tokio::test]
async fn send_receive_json_roundtrip() {
let tmp = tempfile::tempdir().unwrap();
let path = socket_path(tmp.path());
let server = IpcServer::bind(&path).await.unwrap();
let client_handle = tokio::spawn({
let path = path.clone();
async move {
let mut client = IpcClient::connect(&path).await.unwrap();
client.send(&json!({"hello": "world"})).await.unwrap();
let response = client.recv().await.unwrap();
assert_eq!(response, json!({"status": "ok"}));
}
});
let mut conn = server.accept().await.unwrap();
let msg = conn.recv().await.unwrap();
assert_eq!(msg, json!({"hello": "world"}));
conn.send(&json!({"status": "ok"})).await.unwrap();
client_handle.await.unwrap();
}
#[tokio::test]
async fn multiple_messages_same_connection() {
let tmp = tempfile::tempdir().unwrap();
let path = socket_path(tmp.path());
let server = IpcServer::bind(&path).await.unwrap();
let client_handle = tokio::spawn({
let path = path.clone();
async move {
let mut client = IpcClient::connect(&path).await.unwrap();
for i in 0..5 {
client.send(&json!({"seq": i})).await.unwrap();
let resp = client.recv().await.unwrap();
assert_eq!(resp, json!({"ack": i}));
}
}
});
let mut conn = server.accept().await.unwrap();
for i in 0..5 {
let msg = conn.recv().await.unwrap();
assert_eq!(msg, json!({"seq": i}));
conn.send(&json!({"ack": i})).await.unwrap();
}
client_handle.await.unwrap();
}
#[tokio::test]
async fn large_payload_over_64kb() {
let tmp = tempfile::tempdir().unwrap();
let path = socket_path(tmp.path());
let server = IpcServer::bind(&path).await.unwrap();
let big_string = "x".repeat(100_000);
let payload = json!({"data": big_string});
let client_handle = tokio::spawn({
let path = path.clone();
let payload = payload.clone();
async move {
let mut client = IpcClient::connect(&path).await.unwrap();
client.send(&payload).await.unwrap();
let echo = client.recv().await.unwrap();
assert_eq!(echo, payload);
}
});
let mut conn = server.accept().await.unwrap();
let msg = conn.recv().await.unwrap();
assert_eq!(msg, payload);
conn.send(&msg).await.unwrap();
client_handle.await.unwrap();
}
}