use std::future::Future;
use std::io;
use std::path::PathBuf;
#[cfg(unix)]
mod unix;
#[cfg(unix)]
pub use unix::{UnixIpc, UnixIpcListener, UnixIpcStream};
#[cfg(unix)]
pub type ActiveIpc = UnixIpc;
#[cfg(windows)]
mod windows;
#[cfg(windows)]
pub use windows::{WindowsIpc, WindowsIpcListener, WindowsIpcStream};
#[cfg(windows)]
pub type ActiveIpc = WindowsIpc;
pub trait Ipc: crate::sealed::Sealed {
type Listener: IpcListener<Stream = Self::Stream>;
type Stream: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static;
const LEAVES_STALE_ARTIFACT: bool;
fn bind_private(&self, id: &str) -> io::Result<Self::Listener>;
fn connect(&self, id: &str) -> impl Future<Output = io::Result<Self::Stream>> + Send;
fn authenticate_peer(&self, stream: &Self::Stream) -> Result<(), PeerReject>;
fn liveness(&self, id: &str) -> Liveness;
fn list_live(&self) -> Vec<String>;
fn artifact_path(&self, id: &str) -> Option<PathBuf>;
}
pub trait IpcListener: Send {
type Stream;
fn accept(&mut self) -> impl Future<Output = io::Result<Self::Stream>> + Send;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Liveness {
Live,
Dead,
}
#[derive(Debug)]
pub struct PeerReject(pub String);
impl std::fmt::Display for PeerReject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
#[test]
fn active_adapter_upholds_the_contract() {
let _env = crate::ENV_GUARD.lock().unwrap_or_else(|e| e.into_inner());
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
let ipc = crate::IPC;
let id = format!("hotl-ipc-test-{}", std::process::id());
assert_eq!(ipc.liveness(&id), Liveness::Dead, "nothing bound yet");
let mut listener = ipc.bind_private(&id).unwrap();
assert_eq!(ipc.liveness(&id), Liveness::Live);
assert!(ipc.list_live().contains(&id), "a bound endpoint is listed");
let server = tokio::spawn(async move {
loop {
let s = listener.accept().await.unwrap();
if crate::IPC.authenticate_peer(&s).is_err() {
continue;
}
let (r, mut w) = tokio::io::split(s);
let mut lines = BufReader::new(r).lines();
let Ok(Some(got)) = lines.next_line().await else {
continue; };
w.write_all(format!("echo:{got}\n").as_bytes())
.await
.unwrap();
w.flush().await.unwrap();
return;
}
});
let client = ipc.connect(&id).await.unwrap();
let (r, mut w) = tokio::io::split(client);
w.write_all(b"ping\n").await.unwrap();
w.flush().await.unwrap();
let mut lines = BufReader::new(r).lines();
assert_eq!(lines.next_line().await.unwrap().unwrap(), "echo:ping");
server.await.unwrap();
assert_eq!(
ipc.artifact_path(&id).is_some(),
<ActiveIpc as Ipc>::LEAVES_STALE_ARTIFACT
);
if let Some(p) = ipc.artifact_path(&id) {
let _ = std::fs::remove_file(p);
}
});
}
}