#[cfg(unix)]
use crate::rt::{UnixListener, UnixStream};
#[cfg(unix)]
use std::os::unix::fs::FileTypeExt;
#[cfg(unix)]
use std::path::{Path, PathBuf};
#[cfg(unix)]
#[derive(Debug)]
pub struct IpcListener {
listener: UnixListener,
path: PathBuf,
}
#[cfg(unix)]
impl IpcListener {
#[must_use]
pub const fn listener(&self) -> &UnixListener {
&self.listener
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
}
#[cfg(unix)]
impl std::ops::Deref for IpcListener {
type Target = UnixListener;
fn deref(&self) -> &Self::Target {
&self.listener
}
}
#[cfg(unix)]
impl Drop for IpcListener {
fn drop(&mut self) {
if let Ok(metadata) = std::fs::symlink_metadata(&self.path)
&& metadata.file_type().is_socket()
{
let _ = std::fs::remove_file(&self.path);
}
}
}
#[cfg(unix)]
pub async fn connect<P: AsRef<Path>>(path: P) -> std::io::Result<UnixStream> {
UnixStream::connect(path).await
}
#[cfg(unix)]
pub async fn bind<P: AsRef<Path>>(path: P) -> std::io::Result<IpcListener> {
let path_ref = path.as_ref();
if path_ref.exists() {
let metadata = std::fs::symlink_metadata(path_ref)?;
if metadata.file_type().is_socket() {
std::fs::remove_file(path_ref)?;
} else {
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"IPC bind path exists and is not a Unix socket",
));
}
}
let listener = UnixListener::bind(path_ref).await?;
Ok(IpcListener {
listener,
path: path_ref.to_path_buf(),
})
}
#[cfg(unix)]
pub async fn accept(listener: &UnixListener) -> std::io::Result<UnixStream> {
let (stream, _addr) = listener.accept().await?;
Ok(stream)
}
#[cfg(test)]
#[cfg(unix)]
mod tests {
use super::*;
#[test]
fn test_ipc_connect_bind() {
crate::rt::LocalRuntime::new()
.unwrap()
.block_on(test_ipc_connect_bind_impl());
}
async fn test_ipc_connect_bind_impl() {
let path = "/tmp/monocoque_test_ipc.sock";
let _ = std::fs::remove_file(path);
let listener = bind(path).await.unwrap();
let accept_handle = crate::rt::spawn(async move { accept(&listener).await });
crate::rt::sleep(std::time::Duration::from_millis(10)).await;
let client = connect(path).await.unwrap();
let server = crate::rt::join(accept_handle).await.unwrap();
assert!(client.peer_addr().is_ok());
assert!(server.local_addr().is_ok());
drop(client);
drop(server);
let _ = std::fs::remove_file(path);
}
#[test]
fn drop_unlinks_socket_file() {
crate::rt::LocalRuntime::new()
.unwrap()
.block_on(drop_unlinks_socket_file_impl());
}
async fn drop_unlinks_socket_file_impl() {
let path =
std::env::temp_dir().join(format!("monocoque-ipc-unlink-{}.sock", std::process::id()));
let _ = std::fs::remove_file(&path);
let listener = bind(&path).await.unwrap();
assert!(path.exists(), "bind should create the socket node");
drop(listener);
assert!(
!path.exists(),
"dropping IpcListener must unlink the socket file"
);
}
#[test]
fn drop_leaves_non_socket_at_path_untouched() {
crate::rt::LocalRuntime::new()
.unwrap()
.block_on(drop_leaves_non_socket_at_path_untouched_impl());
}
async fn drop_leaves_non_socket_at_path_untouched_impl() {
let path =
std::env::temp_dir().join(format!("monocoque-ipc-guard-{}.sock", std::process::id()));
let _ = std::fs::remove_file(&path);
let listener = bind(&path).await.unwrap();
std::fs::remove_file(&path).unwrap();
std::fs::write(&path, b"not a socket").unwrap();
drop(listener);
assert!(
path.exists(),
"Drop must not remove a non-socket node at the bound path"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn bind_does_not_unlink_existing_regular_file() {
crate::rt::LocalRuntime::new()
.unwrap()
.block_on(bind_does_not_unlink_existing_regular_file_impl());
}
async fn bind_does_not_unlink_existing_regular_file_impl() {
let path = std::env::temp_dir().join(format!(
"monocoque-ipc-regular-file-{}.sock",
std::process::id()
));
std::fs::write(&path, b"do not delete").unwrap();
let result = bind(&path).await;
assert!(
result.is_err(),
"binding over an existing regular file should fail instead of unlinking it"
);
assert!(
path.exists(),
"ipc::bind unlinked an existing regular file before binding"
);
let _ = std::fs::remove_file(path);
}
}