use std::io::{Error, ErrorKind};
use std::sync::atomic::{AtomicU64, Ordering};
use bytes::Bytes;
use futures_util::{FutureExt, SinkExt, StreamExt};
use interprocess::local_socket::{
GenericNamespaced, ListenerOptions,
tokio::{RecvHalf, SendHalf, prelude::*},
};
use tokio_util::codec::{FramedRead, FramedWrite, length_delimited::LengthDelimitedCodec};
use tracing::info;
use super::{IoFuture, IpcConnection, IpcListener};
#[derive(Debug)]
pub struct PipeListener {
name: String,
listener: LocalSocketListener,
accept_counter: AtomicU64,
}
impl PipeListener {
pub fn new(name: &str) -> Result<Self, Error> {
let name_string = name.to_string();
let name = name.to_ns_name::<GenericNamespaced>()?;
let opts = ListenerOptions::new().name(name);
let listener = opts.create_tokio()?;
Ok(Self {
name: name_string,
listener,
accept_counter: AtomicU64::new(0),
})
}
}
impl IpcListener for PipeListener {
fn local_endpoint(&self) -> &str {
self.name.as_str()
}
fn accept(&self) -> IoFuture<'_, Box<dyn IpcConnection>> {
async move {
let stream = self.listener.accept().await?;
let id = self.accept_counter.fetch_add(1, Ordering::Relaxed);
let name = format!("{}#{}", self.name, id);
info!("Accepted a new pipe connection: {}", name);
Ok(Box::new(PipeConnection::new(stream, name)) as Box<dyn IpcConnection>)
}
.boxed()
}
}
#[derive(Debug)]
pub struct PipeConnection {
name: String,
tx: FramedWrite<SendHalf, LengthDelimitedCodec>,
rx: FramedRead<RecvHalf, LengthDelimitedCodec>,
}
impl PipeConnection {
fn new(stream: LocalSocketStream, name: String) -> Self {
let (rx, tx) = stream.split();
let codec = LengthDelimitedCodec::new();
Self {
name,
tx: FramedWrite::new(tx, codec.clone()),
rx: FramedRead::new(rx, codec),
}
}
}
impl IpcConnection for PipeConnection {
async fn connect(name: &str) -> Result<Self, Error> {
let stream = LocalSocketStream::connect(name.to_ns_name::<GenericNamespaced>()?).await?;
info!("Connected to pipe {}", name);
Ok(Self::new(stream, name.to_string()))
}
fn peer_endpoint(&self) -> &str {
self.name.as_str()
}
fn close(&mut self) -> IoFuture<'_, ()> {
self.tx.close().boxed()
}
fn send(&mut self, buf: Bytes) -> IoFuture<'_, ()> {
self.tx.send(buf).boxed()
}
fn recv(&mut self) -> IoFuture<'_, Bytes> {
async move {
let frame = self
.rx
.next()
.await
.ok_or_else(|| Error::from(ErrorKind::ConnectionAborted))??;
Ok(frame.freeze())
}
.boxed()
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
fn unique_pipe_name() -> String {
static COUNTER: AtomicU64 = AtomicU64::new(0);
format!(
"acktor-ipc-pipe-test-{}-{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
)
}
#[tokio::test]
async fn test_listener() -> anyhow::Result<()> {
let name = unique_pipe_name();
let listener = PipeListener::new(&name)?;
assert_eq!(listener.local_endpoint(), name);
PipeListener::new(&name).expect_err("second bind to the same name must fail");
let missing = unique_pipe_name();
PipeConnection::connect(&missing)
.await
.expect_err("connect to non-existent pipe must fail");
Ok(())
}
#[tokio::test]
async fn test_connection() -> anyhow::Result<()> {
let name = unique_pipe_name();
let listener = PipeListener::new(&name)?;
let server_name = name.clone();
let server = tokio::spawn(async move {
let mut c1 = listener.accept().await?;
assert_eq!(c1.peer_endpoint(), format!("{server_name}#0"));
let msg = c1.recv().await?;
c1.send(msg).await?;
c1.close().await?;
let c2 = listener.accept().await?;
assert_eq!(c2.peer_endpoint(), format!("{server_name}#1"));
drop(c2);
Ok::<_, anyhow::Error>(())
});
let mut client1 = PipeConnection::connect(&name).await?;
assert_eq!(client1.peer_endpoint(), name);
let payload = Bytes::from_static(b"hello");
client1.send(payload.clone()).await?;
assert_eq!(client1.recv().await?, payload);
client1.close().await?;
let mut client2 = PipeConnection::connect(&name).await?;
server.await??;
let err = client2
.recv()
.await
.expect_err("recv should fail after peer drop");
assert_eq!(err.kind(), ErrorKind::ConnectionAborted);
Ok(())
}
}