1#[cfg(unix)]
7use crate::rt::{UnixListener, UnixStream};
8#[cfg(unix)]
9use std::os::unix::fs::FileTypeExt;
10#[cfg(unix)]
11use std::path::{Path, PathBuf};
12
13#[cfg(unix)]
23#[derive(Debug)]
24pub struct IpcListener {
25 listener: UnixListener,
26 path: PathBuf,
27}
28
29#[cfg(unix)]
30impl IpcListener {
31 #[must_use]
33 pub const fn listener(&self) -> &UnixListener {
34 &self.listener
35 }
36
37 #[must_use]
39 pub fn path(&self) -> &Path {
40 &self.path
41 }
42}
43
44#[cfg(unix)]
45impl std::ops::Deref for IpcListener {
46 type Target = UnixListener;
47
48 fn deref(&self) -> &Self::Target {
49 &self.listener
50 }
51}
52
53#[cfg(unix)]
54impl Drop for IpcListener {
55 fn drop(&mut self) {
56 if let Ok(metadata) = std::fs::symlink_metadata(&self.path)
60 && metadata.file_type().is_socket()
61 {
62 let _ = std::fs::remove_file(&self.path);
63 }
64 }
65}
66
67#[cfg(unix)]
68pub async fn connect<P: AsRef<Path>>(path: P) -> std::io::Result<UnixStream> {
81 UnixStream::connect(path).await
82}
83
84#[cfg(unix)]
85pub async fn bind<P: AsRef<Path>>(path: P) -> std::io::Result<IpcListener> {
100 let path_ref = path.as_ref();
101 if path_ref.exists() {
102 let metadata = std::fs::symlink_metadata(path_ref)?;
103 if metadata.file_type().is_socket() {
104 std::fs::remove_file(path_ref)?;
105 } else {
106 return Err(std::io::Error::new(
107 std::io::ErrorKind::AlreadyExists,
108 "IPC bind path exists and is not a Unix socket",
109 ));
110 }
111 }
112
113 let listener = UnixListener::bind(path_ref).await?;
114 Ok(IpcListener {
115 listener,
116 path: path_ref.to_path_buf(),
117 })
118}
119
120#[cfg(unix)]
121pub async fn accept(listener: &UnixListener) -> std::io::Result<UnixStream> {
123 let (stream, _addr) = listener.accept().await?;
124 Ok(stream)
125}
126
127#[cfg(test)]
128#[cfg(unix)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_ipc_connect_bind() {
134 crate::rt::LocalRuntime::new()
135 .unwrap()
136 .block_on(test_ipc_connect_bind_impl());
137 }
138
139 async fn test_ipc_connect_bind_impl() {
140 let path = "/tmp/monocoque_test_ipc.sock";
141
142 let _ = std::fs::remove_file(path);
144
145 let listener = bind(path).await.unwrap();
146
147 let accept_handle = crate::rt::spawn(async move { accept(&listener).await });
149
150 crate::rt::sleep(std::time::Duration::from_millis(10)).await;
152
153 let client = connect(path).await.unwrap();
155
156 let server = crate::rt::join(accept_handle).await.unwrap();
158
159 assert!(client.peer_addr().is_ok());
160 assert!(server.local_addr().is_ok());
161
162 drop(client);
164 drop(server);
165 let _ = std::fs::remove_file(path);
166 }
167
168 #[test]
169 fn drop_unlinks_socket_file() {
170 crate::rt::LocalRuntime::new()
171 .unwrap()
172 .block_on(drop_unlinks_socket_file_impl());
173 }
174
175 async fn drop_unlinks_socket_file_impl() {
176 let path =
177 std::env::temp_dir().join(format!("monocoque-ipc-unlink-{}.sock", std::process::id()));
178 let _ = std::fs::remove_file(&path);
179
180 let listener = bind(&path).await.unwrap();
181 assert!(path.exists(), "bind should create the socket node");
182
183 drop(listener);
184 assert!(
185 !path.exists(),
186 "dropping IpcListener must unlink the socket file"
187 );
188 }
189
190 #[test]
191 fn drop_leaves_non_socket_at_path_untouched() {
192 crate::rt::LocalRuntime::new()
193 .unwrap()
194 .block_on(drop_leaves_non_socket_at_path_untouched_impl());
195 }
196
197 async fn drop_leaves_non_socket_at_path_untouched_impl() {
198 let path =
201 std::env::temp_dir().join(format!("monocoque-ipc-guard-{}.sock", std::process::id()));
202 let _ = std::fs::remove_file(&path);
203
204 let listener = bind(&path).await.unwrap();
205 std::fs::remove_file(&path).unwrap();
206 std::fs::write(&path, b"not a socket").unwrap();
207
208 drop(listener);
209 assert!(
210 path.exists(),
211 "Drop must not remove a non-socket node at the bound path"
212 );
213 let _ = std::fs::remove_file(&path);
214 }
215
216 #[test]
217 fn bind_does_not_unlink_existing_regular_file() {
218 crate::rt::LocalRuntime::new()
219 .unwrap()
220 .block_on(bind_does_not_unlink_existing_regular_file_impl());
221 }
222
223 async fn bind_does_not_unlink_existing_regular_file_impl() {
224 let path = std::env::temp_dir().join(format!(
225 "monocoque-ipc-regular-file-{}.sock",
226 std::process::id()
227 ));
228
229 std::fs::write(&path, b"do not delete").unwrap();
230
231 let result = bind(&path).await;
232
233 assert!(
234 result.is_err(),
235 "binding over an existing regular file should fail instead of unlinking it"
236 );
237 assert!(
238 path.exists(),
239 "ipc::bind unlinked an existing regular file before binding"
240 );
241
242 let _ = std::fs::remove_file(path);
243 }
244}