use crate::error::IpcError;
use tracing::{debug, trace};
const MAX_FRAME_SIZE: u32 = 16 * 1024 * 1024;
type Result<T> = std::result::Result<T, IpcError>;
#[cfg(unix)]
mod platform {
use tokio::net::{UnixListener, UnixStream};
pub(super) struct Listener {
inner: UnixListener,
}
impl Listener {
pub fn bind(path: &std::path::Path) -> std::io::Result<Self> {
let _ = std::fs::remove_file(path);
Ok(Self {
inner: UnixListener::bind(path)?,
})
}
pub async fn accept(&self) -> std::io::Result<Stream> {
let (stream, _) = self.inner.accept().await?;
Ok(Stream { inner: stream })
}
}
pub(super) struct Stream {
inner: UnixStream,
}
impl Stream {
pub async fn connect(path: &std::path::Path) -> std::io::Result<Self> {
Ok(Self {
inner: UnixStream::connect(path).await?,
})
}
pub async fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
use tokio::io::AsyncReadExt;
self.inner.read_exact(buf).await.map(|_| ())
}
pub async fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
use tokio::io::AsyncWriteExt;
self.inner.write_all(buf).await
}
}
}
#[cfg(windows)]
mod platform {
use tokio::net::windows::named_pipe::{ClientOptions, ServerOptions};
pub(super) struct Listener {
pipe_name: String,
}
impl Listener {
pub fn bind(path: &std::path::Path) -> std::io::Result<Self> {
let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("majra");
let pipe_name = format!(r"\\.\pipe\majra-{}", name);
let _server = ServerOptions::new()
.first_pipe_instance(true)
.create(&pipe_name)?;
Ok(Self { pipe_name })
}
pub async fn accept(&self) -> std::io::Result<Stream> {
let server = ServerOptions::new().create(&self.pipe_name)?;
server.connect().await?;
Ok(Stream {
inner: StreamInner::Server(server),
})
}
}
pub(super) struct Stream {
inner: StreamInner,
}
enum StreamInner {
Server(tokio::net::windows::named_pipe::NamedPipeServer),
Client(tokio::net::windows::named_pipe::NamedPipeClient),
}
impl Stream {
pub async fn connect(path: &std::path::Path) -> std::io::Result<Self> {
let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("majra");
let pipe_name = format!(r"\\.\pipe\majra-{}", name);
let client = ClientOptions::new().open(&pipe_name)?;
Ok(Self {
inner: StreamInner::Client(client),
})
}
pub async fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
use tokio::io::AsyncReadExt;
match &mut self.inner {
StreamInner::Server(s) => s.read_exact(buf).await.map(|_| ()),
StreamInner::Client(c) => c.read_exact(buf).await.map(|_| ()),
}
}
pub async fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
use tokio::io::AsyncWriteExt;
match &mut self.inner {
StreamInner::Server(s) => s.write_all(buf).await,
StreamInner::Client(c) => c.write_all(buf).await,
}
}
}
}
pub struct IpcServer {
listener: platform::Listener,
}
impl IpcServer {
pub fn bind(path: &std::path::Path) -> Result<Self> {
let listener = platform::Listener::bind(path)?;
debug!(path = %path.display(), "ipc: server bound");
Ok(Self { listener })
}
pub async fn accept(&self) -> Result<IpcConnection> {
let stream = self.listener.accept().await?;
debug!("ipc: connection accepted");
Ok(IpcConnection { stream })
}
}
pub struct IpcConnection {
stream: platform::Stream,
}
impl IpcConnection {
pub async fn connect(path: &std::path::Path) -> Result<Self> {
let stream = platform::Stream::connect(path).await?;
Ok(Self { stream })
}
pub async fn send(&mut self, payload: &serde_json::Value) -> Result<()> {
let data = serde_json::to_vec(payload)?;
let len = u32::try_from(data.len()).map_err(|_| IpcError::FrameTooLarge {
size: u32::MAX,
max: MAX_FRAME_SIZE,
})?;
if len > MAX_FRAME_SIZE {
return Err(IpcError::FrameTooLarge {
size: len,
max: MAX_FRAME_SIZE,
});
}
self.stream.write_all(&len.to_be_bytes()).await?;
self.stream.write_all(&data).await?;
trace!(bytes = len, "ipc: frame sent");
Ok(())
}
pub async fn recv(&mut self) -> Result<serde_json::Value> {
let mut len_buf = [0u8; 4];
match self.stream.read_exact(&mut len_buf).await {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
trace!("ipc: connection closed by peer");
return Err(IpcError::ConnectionClosed);
}
Err(e) => return Err(e.into()),
}
let len = u32::from_be_bytes(len_buf);
if len > MAX_FRAME_SIZE {
return Err(IpcError::FrameTooLarge {
size: len,
max: MAX_FRAME_SIZE,
});
}
let mut buf = vec![0u8; len as usize];
self.stream.read_exact(&mut buf).await?;
let value = serde_json::from_slice(&buf)?;
trace!(bytes = len, "ipc: frame received");
Ok(value)
}
}
pub type IpcClient = IpcConnection;
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn tmp_socket() -> PathBuf {
let id = uuid::Uuid::new_v4();
std::env::temp_dir().join(format!("majra-test-{id}.sock"))
}
#[tokio::test]
async fn roundtrip() {
let path = tmp_socket();
let server = IpcServer::bind(&path).unwrap();
let handle = tokio::spawn({
let path = path.clone();
async move {
let mut client = IpcClient::connect(&path).await.unwrap();
client
.send(&serde_json::json!({"hello": "world"}))
.await
.unwrap();
let resp = client.recv().await.unwrap();
assert_eq!(resp["echo"], "world");
}
});
let mut conn = server.accept().await.unwrap();
let msg = conn.recv().await.unwrap();
assert_eq!(msg["hello"], "world");
conn.send(&serde_json::json!({"echo": "world"}))
.await
.unwrap();
handle.await.unwrap();
let _ = std::fs::remove_file(&path);
}
#[tokio::test]
async fn connection_closed() {
let path = tmp_socket();
let server = IpcServer::bind(&path).unwrap();
let handle = tokio::spawn({
let path = path.clone();
async move {
let _client = IpcClient::connect(&path).await.unwrap();
}
});
let mut conn = server.accept().await.unwrap();
handle.await.unwrap();
let result = conn.recv().await;
assert!(matches!(result, Err(IpcError::ConnectionClosed)));
let _ = std::fs::remove_file(&path);
}
#[tokio::test]
async fn write_frame_too_large() {
let path = tmp_socket();
let server = IpcServer::bind(&path).unwrap();
let handle = tokio::spawn({
let path = path.clone();
async move {
let mut client = IpcConnection::connect(&path).await.unwrap();
let big = serde_json::json!({"data": "x".repeat(1000)});
client.send(&big).await.unwrap();
}
});
let mut conn = server.accept().await.unwrap();
let msg = conn.recv().await.unwrap();
assert_eq!(msg["data"].as_str().unwrap().len(), 1000);
handle.await.unwrap();
let _ = std::fs::remove_file(&path);
}
#[cfg(unix)]
#[tokio::test]
async fn read_frame_too_large() {
use tokio::io::AsyncWriteExt;
use tokio::net::UnixStream;
let path = tmp_socket();
let server = IpcServer::bind(&path).unwrap();
let handle = tokio::spawn({
let path = path.clone();
async move {
let mut stream = UnixStream::connect(&path).await.unwrap();
let fake_len: u32 = super::MAX_FRAME_SIZE + 1;
stream.write_all(&fake_len.to_be_bytes()).await.unwrap();
}
});
let mut conn = server.accept().await.unwrap();
let result = conn.recv().await;
assert!(matches!(result, Err(IpcError::FrameTooLarge { .. })));
handle.await.unwrap();
let _ = std::fs::remove_file(&path);
}
}