Skip to main content

acktor_ipc/ipc_method/
pipe.rs

1//! IPC method implementation using Unix domain sockets and Windows named pipes.
2
3use std::io::{Error, ErrorKind};
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use bytes::Bytes;
7use futures_util::{FutureExt, SinkExt, StreamExt};
8use interprocess::local_socket::{
9    GenericNamespaced, ListenerOptions,
10    tokio::{RecvHalf, SendHalf, prelude::*},
11};
12use tokio_util::codec::{FramedRead, FramedWrite, length_delimited::LengthDelimitedCodec};
13use tracing::info;
14
15use super::{IoFuture, IpcConnection, IpcListener};
16
17/// IPC listener implemented with Unix domain sockets and Windows named pipes.
18#[derive(Debug)]
19pub struct PipeListener {
20    name: String,
21    listener: LocalSocketListener,
22    accept_counter: AtomicU64,
23}
24
25impl PipeListener {
26    /// Constructs a new [`PipeListener`] with the given pipe name.
27    pub fn new(name: &str) -> Result<Self, Error> {
28        let name_string = name.to_string();
29        let name = name.to_ns_name::<GenericNamespaced>()?;
30
31        let opts = ListenerOptions::new().name(name);
32        let listener = opts.create_tokio()?;
33
34        Ok(Self {
35            name: name_string,
36            listener,
37            accept_counter: AtomicU64::new(0),
38        })
39    }
40}
41
42impl IpcListener for PipeListener {
43    fn local_endpoint(&self) -> &str {
44        self.name.as_str()
45    }
46
47    fn accept(&self) -> IoFuture<'_, Box<dyn IpcConnection>> {
48        async move {
49            let stream = self.listener.accept().await?;
50
51            let id = self.accept_counter.fetch_add(1, Ordering::Relaxed);
52            let name = format!("{}#{}", self.name, id);
53
54            info!("Accepted a new pipe connection: {}", name);
55
56            Ok(Box::new(PipeConnection::new(stream, name)) as Box<dyn IpcConnection>)
57        }
58        .boxed()
59    }
60}
61
62/// IPC connection implemented with Unix domain sockets and Windows named pipes.
63#[derive(Debug)]
64pub struct PipeConnection {
65    name: String,
66    tx: FramedWrite<SendHalf, LengthDelimitedCodec>,
67    rx: FramedRead<RecvHalf, LengthDelimitedCodec>,
68}
69
70impl PipeConnection {
71    fn new(stream: LocalSocketStream, name: String) -> Self {
72        let (rx, tx) = stream.split();
73        let codec = LengthDelimitedCodec::new();
74
75        Self {
76            name,
77            tx: FramedWrite::new(tx, codec.clone()),
78            rx: FramedRead::new(rx, codec),
79        }
80    }
81}
82
83impl IpcConnection for PipeConnection {
84    async fn connect(name: &str) -> Result<Self, Error> {
85        let stream = LocalSocketStream::connect(name.to_ns_name::<GenericNamespaced>()?).await?;
86
87        info!("Connected to pipe {}", name);
88
89        Ok(Self::new(stream, name.to_string()))
90    }
91
92    fn peer_endpoint(&self) -> &str {
93        self.name.as_str()
94    }
95
96    fn close(&mut self) -> IoFuture<'_, ()> {
97        self.tx.close().boxed()
98    }
99
100    fn send(&mut self, buf: Bytes) -> IoFuture<'_, ()> {
101        self.tx.send(buf).boxed()
102    }
103
104    fn recv(&mut self) -> IoFuture<'_, Bytes> {
105        async move {
106            let frame = self
107                .rx
108                .next()
109                .await
110                .ok_or_else(|| Error::from(ErrorKind::ConnectionAborted))??;
111
112            Ok(frame.freeze())
113        }
114        .boxed()
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use pretty_assertions::assert_eq;
121
122    use super::*;
123
124    fn unique_pipe_name() -> String {
125        static COUNTER: AtomicU64 = AtomicU64::new(0);
126        format!(
127            "acktor-ipc-pipe-test-{}-{}",
128            std::process::id(),
129            COUNTER.fetch_add(1, Ordering::Relaxed)
130        )
131    }
132
133    #[tokio::test]
134    async fn test_listener() -> anyhow::Result<()> {
135        // new + local_endpoint
136        let name = unique_pipe_name();
137        let listener = PipeListener::new(&name)?;
138        assert_eq!(listener.local_endpoint(), name);
139
140        // duplicate bind to the same name fails
141        PipeListener::new(&name).expect_err("second bind to the same name must fail");
142
143        // connect to an unbound name fails
144        let missing = unique_pipe_name();
145        PipeConnection::connect(&missing)
146            .await
147            .expect_err("connect to non-existent pipe must fail");
148
149        Ok(())
150    }
151
152    #[tokio::test]
153    async fn test_connection() -> anyhow::Result<()> {
154        let name = unique_pipe_name();
155        let listener = PipeListener::new(&name)?;
156        let server_name = name.clone();
157
158        let server = tokio::spawn(async move {
159            // 1st client: echo roundtrip; peer_endpoint uses the `#0` suffix
160            let mut c1 = listener.accept().await?;
161            assert_eq!(c1.peer_endpoint(), format!("{}#0", server_name));
162            let msg = c1.recv().await?;
163            c1.send(msg).await?;
164            c1.close().await?;
165
166            // 2nd client: accept counter increments to `#1`; drop without writing to exercise
167            // the client-side ConnectionAborted path
168            let c2 = listener.accept().await?;
169            assert_eq!(c2.peer_endpoint(), format!("{}#1", server_name));
170            drop(c2);
171
172            Ok::<_, anyhow::Error>(())
173        });
174
175        // client 1: full roundtrip
176        let mut client1 = PipeConnection::connect(&name).await?;
177        assert_eq!(client1.peer_endpoint(), name);
178        let payload = Bytes::from_static(b"hello");
179        client1.send(payload.clone()).await?;
180        assert_eq!(client1.recv().await?, payload);
181        client1.close().await?;
182
183        // client 2: server drops its connection, recv returns ConnectionAborted
184        let mut client2 = PipeConnection::connect(&name).await?;
185        server.await??;
186        let err = client2
187            .recv()
188            .await
189            .expect_err("recv should fail after peer drop");
190        assert_eq!(err.kind(), ErrorKind::ConnectionAborted);
191
192        Ok(())
193    }
194}