use std::future::Future;
use std::io;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::application::{EngineError, EngineResult};
const ACCEPT_BACKOFF: std::time::Duration = std::time::Duration::from_millis(25);
#[must_use]
pub fn endpoint_from_env() -> Option<PathBuf> {
std::env::var(crate::config::APP_IPC_ENV)
.ok()
.filter(|value| !value.trim().is_empty())
.map(PathBuf::from)
}
fn exit_when_orphaned() {
std::thread::Builder::new()
.name(String::from("arcature-orphan-watch"))
.spawn(|| {
use std::io::Read as _;
let mut byte = [0_u8; 1];
match io::stdin().read(&mut byte) {
Ok(0) | Err(_) => std::process::exit(0),
Ok(_) => {}
}
})
.map(drop)
.unwrap_or_else(|error| {
eprintln!("arcature: could not watch for an orphaned supervisor: {error}");
});
}
#[derive(Clone, Debug)]
pub struct IpcAddr(Arc<Path>);
impl IpcAddr {
#[must_use]
pub fn path(&self) -> &Path {
&self.0
}
}
impl std::fmt::Display for IpcAddr {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}", self.0.display())
}
}
#[cfg(unix)]
pub struct IpcListener {
listener: tokio::net::UnixListener,
path: Arc<Path>,
}
#[cfg(windows)]
pub struct IpcListener {
next: tokio::net::windows::named_pipe::NamedPipeServer,
path: Arc<Path>,
}
impl IpcListener {
pub async fn bind(path: &Path) -> io::Result<Self> {
let owned: Arc<Path> = Arc::from(path);
#[cfg(unix)]
{
if tokio::fs::metadata(path).await.is_ok() {
tokio::fs::remove_file(path).await?;
}
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
Ok(Self {
listener: tokio::net::UnixListener::bind(path)?,
path: owned,
})
}
#[cfg(windows)]
{
let next = tokio::net::windows::named_pipe::ServerOptions::new()
.first_pipe_instance(true)
.create(path)?;
Ok(Self { next, path: owned })
}
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
}
#[cfg(unix)]
impl Drop for IpcListener {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
#[cfg(unix)]
impl axum::serve::Listener for IpcListener {
type Io = tokio::net::UnixStream;
type Addr = IpcAddr;
async fn accept(&mut self) -> (Self::Io, Self::Addr) {
loop {
match self.listener.accept().await {
Ok((io, _peer)) => return (io, IpcAddr(Arc::clone(&self.path))),
Err(error) => {
eprintln!("warning: ipc accept failed: {error}");
tokio::time::sleep(ACCEPT_BACKOFF).await;
}
}
}
}
fn local_addr(&self) -> io::Result<Self::Addr> {
Ok(IpcAddr(Arc::clone(&self.path)))
}
}
#[cfg(windows)]
impl axum::serve::Listener for IpcListener {
type Io = tokio::net::windows::named_pipe::NamedPipeServer;
type Addr = IpcAddr;
async fn accept(&mut self) -> (Self::Io, Self::Addr) {
use tokio::net::windows::named_pipe::ServerOptions;
loop {
if let Err(error) = self.next.connect().await {
eprintln!("warning: ipc accept failed: {error}");
tokio::time::sleep(ACCEPT_BACKOFF).await;
continue;
}
let replacement = loop {
match ServerOptions::new().create(&*self.path) {
Ok(server) => break server,
Err(error) => {
eprintln!("warning: could not open the next pipe instance: {error}");
tokio::time::sleep(ACCEPT_BACKOFF).await;
}
}
};
let io = std::mem::replace(&mut self.next, replacement);
return (io, IpcAddr(Arc::clone(&self.path)));
}
}
fn local_addr(&self) -> io::Result<Self::Addr> {
Ok(IpcAddr(Arc::clone(&self.path)))
}
}
fn http_url(addr: SocketAddr) -> String {
if addr.ip().is_unspecified() {
return format!("http://localhost:{}", addr.port());
}
match addr {
SocketAddr::V4(v4) => format!("http://{}:{}", v4.ip(), v4.port()),
SocketAddr::V6(v6) => format!("http://[{}]:{}", v6.ip(), v6.port()),
}
}
pub enum ServeTarget {
Tcp(tokio::net::TcpListener),
Ipc(IpcListener),
}
impl ServeTarget {
pub async fn bind(addr: SocketAddr) -> EngineResult<Self> {
match endpoint_from_env() {
Some(path) => {
exit_when_orphaned();
IpcListener::bind(&path)
.await
.map(Self::Ipc)
.map_err(|source| EngineError::BindListener {
address: path.display().to_string(),
source,
})
}
None => tokio::net::TcpListener::bind(addr)
.await
.map(Self::Tcp)
.map_err(|source| EngineError::BindListener {
address: addr.to_string(),
source,
}),
}
}
#[must_use]
pub fn describe(&self) -> String {
match self {
Self::Tcp(listener) => listener
.local_addr()
.map_or_else(|_| String::from("tcp"), http_url),
Self::Ipc(listener) => listener.path().display().to_string(),
}
}
pub async fn serve<S, F>(self, service: S, shutdown: F) -> EngineResult<()>
where
S: tower::Service<
crate::axum::extract::Request,
Response = crate::axum::response::Response,
Error = std::convert::Infallible,
> + Clone
+ Send
+ 'static,
S::Future: Send,
F: Future<Output = ()> + Send + 'static,
{
use crate::axum::ServiceExt as _;
match self {
Self::Tcp(listener) => {
axum::serve(listener, service.into_make_service())
.with_graceful_shutdown(shutdown)
.await
}
Self::Ipc(listener) => {
axum::serve(listener, service.into_make_service())
.with_graceful_shutdown(shutdown)
.await
}
}
.map_err(|source| EngineError::Serve { source })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn scratch_endpoint(label: &str) -> PathBuf {
let pid = std::process::id();
#[cfg(windows)]
{
PathBuf::from(format!(r"\\.\pipe\arcature-serve-ipc-{label}-{pid}"))
}
#[cfg(unix)]
{
std::env::temp_dir().join(format!("arcature-serve-ipc-{label}-{pid}.sock"))
}
}
#[test]
fn an_unset_endpoint_means_the_application_keeps_its_port() {
assert!(
Option::<String>::None
.filter(|value: &String| !value.trim().is_empty())
.is_none()
);
assert!(
Some(String::from(" "))
.filter(|value: &String| !value.trim().is_empty())
.is_none()
);
}
#[tokio::test]
async fn binding_an_endpoint_makes_it_connectable() {
let path = scratch_endpoint("connectable");
let listener = IpcListener::bind(&path)
.await
.expect("the endpoint should be creatable");
assert_eq!(listener.path(), path.as_path());
#[cfg(unix)]
tokio::net::UnixStream::connect(&path)
.await
.expect("a bound endpoint should accept a connection");
#[cfg(windows)]
tokio::net::windows::named_pipe::ClientOptions::new()
.open(&path)
.expect("a bound endpoint should accept a connection");
}
#[tokio::test]
async fn a_second_bind_of_a_live_endpoint_does_not_silently_share_it() {
let path = scratch_endpoint("exclusive");
let _first = IpcListener::bind(&path)
.await
.expect("the first bind should succeed");
#[cfg(windows)]
assert!(
IpcListener::bind(&path).await.is_err(),
"a second listener must not attach to a live pipe name"
);
}
#[tokio::test]
async fn an_ipc_target_describes_itself_by_its_endpoint() {
let path = scratch_endpoint("describe");
let target = ServeTarget::Ipc(
IpcListener::bind(&path)
.await
.expect("the endpoint should be creatable"),
);
assert_eq!(target.describe(), path.display().to_string());
}
#[tokio::test]
async fn a_tcp_target_describes_itself_as_a_clickable_url() {
let target = ServeTarget::bind("127.0.0.1:0".parse().expect("a literal address"))
.await
.expect("an ephemeral port should be bindable");
let described = target.describe();
assert!(
described.starts_with("http://127.0.0.1:"),
"expected a URL, got `{described}`"
);
assert!(
!described.ends_with(":0"),
"the line must name the port the kernel chose, not the one requested: `{described}`"
);
}
#[test]
fn a_wildcard_bind_is_reported_as_localhost() {
assert_eq!(
http_url("0.0.0.0:3000".parse().expect("a literal address")),
"http://localhost:3000"
);
assert_eq!(
http_url("[::]:3000".parse().expect("a literal address")),
"http://localhost:3000"
);
}
#[test]
fn an_ipv6_literal_keeps_its_brackets() {
assert_eq!(
http_url("[::1]:8080".parse().expect("a literal address")),
"http://[::1]:8080"
);
}
}