use std::io;
use std::path::{Path, PathBuf};
#[cfg(windows)]
const PIPE_BUSY: i32 = 231;
#[cfg(windows)]
const BUSY_DEADLINE: std::time::Duration = std::time::Duration::from_millis(250);
#[cfg(windows)]
const BUSY_RETRY: std::time::Duration = std::time::Duration::from_millis(1);
#[derive(Debug, Clone)]
pub struct IpcEndpoint {
path: PathBuf,
}
#[cfg(unix)]
pub(crate) type IpcStream = tokio::net::UnixStream;
#[cfg(windows)]
pub(crate) type IpcStream = tokio::net::windows::named_pipe::NamedPipeClient;
impl IpcEndpoint {
#[must_use]
pub fn new(path: PathBuf) -> Self {
Self { path }
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
pub(crate) async fn connect(&self) -> io::Result<IpcStream> {
#[cfg(unix)]
{
tokio::net::UnixStream::connect(&self.path).await
}
#[cfg(windows)]
{
use tokio::net::windows::named_pipe::ClientOptions;
let deadline = std::time::Instant::now() + BUSY_DEADLINE;
loop {
match ClientOptions::new().open(&self.path) {
Ok(client) => return Ok(client),
Err(error) => {
if error.raw_os_error() != Some(PIPE_BUSY)
|| std::time::Instant::now() >= deadline
{
return Err(error);
}
}
}
tokio::time::sleep(BUSY_RETRY).await;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn endpoint_stores_path() {
let endpoint = IpcEndpoint::new(PathBuf::from("/tmp/arcature-vite-test.sock"));
assert_eq!(endpoint.path(), Path::new("/tmp/arcature-vite-test.sock"));
}
#[cfg(unix)]
#[tokio::test]
async fn connect_returns_error_when_socket_absent() {
let endpoint = IpcEndpoint::new(PathBuf::from(
"/tmp/arcature-vite-nonexistent-socket-test.sock",
));
let result = endpoint.connect().await;
assert!(result.is_err());
let error = result.expect_err("connect should fail for absent socket");
assert_eq!(
error.kind(),
io::ErrorKind::NotFound,
"expected NotFound for absent Unix socket"
);
}
#[cfg(unix)]
#[tokio::test]
async fn connect_succeeds_when_socket_listens() {
let listener =
tokio::net::UnixListener::bind("/tmp/arcature-vite-endpoint-listen-test.sock")
.expect("bind should succeed");
let endpoint = IpcEndpoint::new(PathBuf::from(
"/tmp/arcature-vite-endpoint-listen-test.sock",
));
let result = endpoint.connect().await;
assert!(result.is_ok());
drop(listener);
let _ = std::fs::remove_file("/tmp/arcature-vite-endpoint-listen-test.sock");
}
#[cfg(windows)]
#[tokio::test]
async fn a_busy_pipe_is_waited_out_rather_than_reported_as_a_missing_server() {
use tokio::net::windows::named_pipe::ServerOptions;
let name = format!(r"\\.\pipe\arcature-busy-wait-{}", std::process::id());
let occupied = ServerOptions::new()
.first_pipe_instance(true)
.create(&name)
.expect("the first pipe instance should be creatable");
let endpoint = IpcEndpoint::new(PathBuf::from(&name));
let taken = endpoint
.connect()
.await
.expect("the free instance should open");
let replacing = tokio::spawn({
let name = name.clone();
async move {
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
ServerOptions::new().create(&name)
}
});
let second = endpoint.connect().await;
assert!(
second.is_ok(),
"a busy pipe must be retried, not reported as an absent server"
);
drop((taken, occupied, second));
drop(
replacing
.await
.expect("the replacement task should not panic"),
);
}
#[cfg(windows)]
#[tokio::test]
async fn a_pipe_that_stays_busy_is_eventually_reported_rather_than_waited_on_forever() {
use tokio::net::windows::named_pipe::ServerOptions;
let name = format!(r"\\.\pipe\arcature-busy-forever-{}", std::process::id());
let occupied = ServerOptions::new()
.first_pipe_instance(true)
.create(&name)
.expect("the first pipe instance should be creatable");
let endpoint = IpcEndpoint::new(PathBuf::from(&name));
let taken = endpoint
.connect()
.await
.expect("the free instance should open");
let started = std::time::Instant::now();
let result = endpoint.connect().await;
assert!(result.is_err(), "the pipe never became free");
assert!(
started.elapsed() >= BUSY_DEADLINE,
"it gave up before the deadline"
);
drop((taken, occupied));
}
}