use std::{
fmt::{self, Display, Formatter},
io,
net::SocketAddr,
path::Path,
pin::Pin,
task::{Context, Poll},
};
use axum::serve::Listener as AxumListener;
use thiserror::Error;
use tokio::{
io::{AsyncRead, AsyncWrite, ReadBuf},
net::{TcpListener, TcpStream, UnixListener, UnixStream},
};
use crate::config::ListenAddress;
pub struct Listener {
inner: Inner,
local_address: Address,
}
impl Listener {
pub async fn bind(address: &ListenAddress) -> Result<Self, Error> {
match address {
ListenAddress::Tcp(address) => Self::bind_tcp(*address).await,
ListenAddress::Unix(path) => Self::bind_unix(path),
}
}
#[must_use]
pub fn local_address(&self) -> &Address {
&self.local_address
}
#[must_use]
pub fn port(&self) -> Option<u16> {
self.local_address.port()
}
async fn bind_tcp(address: SocketAddr) -> Result<Self, Error> {
let listener = TcpListener::bind(address)
.await
.map_err(|source| Error::BindTcp { address, source })?;
let local_address = listener
.local_addr()
.map_err(|source| Error::ReadTcpAddress { source })?;
Ok(Self {
inner: Inner::Tcp(listener),
local_address: Address::Tcp(local_address),
})
}
fn bind_unix(path: &Path) -> Result<Self, Error> {
let listener = UnixListener::bind(path).map_err(|source| Error::BindUnix {
path: path.to_path_buf(),
source,
})?;
let local_address = listener
.local_addr()
.map_err(|source| Error::ReadUnixAddress { source })?;
Ok(Self {
inner: Inner::Unix(listener),
local_address: Address::Unix(local_address),
})
}
}
impl AxumListener for Listener {
type Addr = Address;
type Io = Connection;
#[inline]
async fn accept(&mut self) -> (Self::Io, Self::Addr) {
match &mut self.inner {
Inner::Tcp(listener) => {
let (connection, address) = AxumListener::accept(listener).await;
(Connection::Tcp(connection), Address::Tcp(address))
}
Inner::Unix(listener) => {
let (connection, address) = AxumListener::accept(listener).await;
(Connection::Unix(connection), Address::Unix(address))
}
}
}
#[inline]
fn local_addr(&self) -> io::Result<Self::Addr> {
Ok(self.local_address.clone())
}
}
enum Inner {
Tcp(TcpListener),
Unix(UnixListener),
}
#[derive(Clone, Debug)]
pub enum Address {
Tcp(SocketAddr),
Unix(tokio::net::unix::SocketAddr),
}
impl Address {
#[must_use]
pub fn port(&self) -> Option<u16> {
match self {
Self::Tcp(address) => Some(address.port()),
Self::Unix(_) => None,
}
}
#[must_use]
pub fn as_pathname(&self) -> Option<&Path> {
match self {
Self::Tcp(_) => None,
Self::Unix(address) => address.as_pathname(),
}
}
}
impl Display for Address {
#[inline]
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Tcp(address) => address.fmt(formatter),
Self::Unix(address) => match address.as_pathname() {
Some(path) => path.display().fmt(formatter),
None => write!(formatter, "{address:?}"),
},
}
}
}
#[derive(Debug)]
pub enum Connection {
Tcp(TcpStream),
Unix(UnixStream),
}
impl AsyncRead for Connection {
#[inline]
fn poll_read(
self: Pin<&mut Self>,
context: &mut Context<'_>,
buffer: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
match self.get_mut() {
Self::Tcp(connection) => Pin::new(connection).poll_read(context, buffer),
Self::Unix(connection) => Pin::new(connection).poll_read(context, buffer),
}
}
}
impl AsyncWrite for Connection {
#[inline]
fn poll_write(
self: Pin<&mut Self>,
context: &mut Context<'_>,
buffer: &[u8],
) -> Poll<Result<usize, io::Error>> {
match self.get_mut() {
Self::Tcp(connection) => Pin::new(connection).poll_write(context, buffer),
Self::Unix(connection) => Pin::new(connection).poll_write(context, buffer),
}
}
#[inline]
fn poll_flush(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
match self.get_mut() {
Self::Tcp(connection) => Pin::new(connection).poll_flush(context),
Self::Unix(connection) => Pin::new(connection).poll_flush(context),
}
}
#[inline]
fn poll_shutdown(
self: Pin<&mut Self>,
context: &mut Context<'_>,
) -> Poll<Result<(), io::Error>> {
match self.get_mut() {
Self::Tcp(connection) => Pin::new(connection).poll_shutdown(context),
Self::Unix(connection) => Pin::new(connection).poll_shutdown(context),
}
}
#[inline]
fn is_write_vectored(&self) -> bool {
match self {
Self::Tcp(connection) => connection.is_write_vectored(),
Self::Unix(connection) => connection.is_write_vectored(),
}
}
#[inline]
fn poll_write_vectored(
self: Pin<&mut Self>,
context: &mut Context<'_>,
buffers: &[io::IoSlice<'_>],
) -> Poll<Result<usize, io::Error>> {
match self.get_mut() {
Self::Tcp(connection) => Pin::new(connection).poll_write_vectored(context, buffers),
Self::Unix(connection) => Pin::new(connection).poll_write_vectored(context, buffers),
}
}
}
#[derive(Debug, Error)]
pub enum Error {
#[error("failed to bind TCP listener at {address}")]
BindTcp {
address: SocketAddr,
#[source]
source: io::Error,
},
#[error("failed to bind Unix listener at {path}")]
BindUnix {
path: std::path::PathBuf,
#[source]
source: io::Error,
},
#[error("failed to read TCP listener address")]
ReadTcpAddress {
#[source]
source: io::Error,
},
#[error("failed to read Unix listener address")]
ReadUnixAddress {
#[source]
source: io::Error,
},
}
#[cfg(test)]
mod tests {
use std::{
fs,
net::{IpAddr, Ipv4Addr, SocketAddr},
path::PathBuf,
process,
time::{SystemTime, UNIX_EPOCH},
};
use super::Listener;
use crate::config::ListenAddress;
#[tokio::test]
async fn reports_effective_tcp_port() {
let address = ListenAddress::Tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0));
let listener = Listener::bind(&address)
.await
.expect("ephemeral TCP listener should bind");
assert_ne!(listener.port(), Some(0));
assert!(listener.port().is_some());
}
#[tokio::test]
async fn reports_unix_socket_path() {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("current time should follow the Unix epoch")
.as_nanos();
let path =
std::env::temp_dir().join(format!("twelve-listener-{}-{unique}.sock", process::id()));
let socket = SocketFile(path.clone());
let listener = Listener::bind(&ListenAddress::Unix(path.clone()))
.await
.expect("Unix listener should bind");
assert_eq!(listener.port(), None);
assert_eq!(listener.local_address().as_pathname(), Some(path.as_path()));
drop(listener);
drop(socket);
}
struct SocketFile(PathBuf);
impl Drop for SocketFile {
fn drop(&mut self) {
match fs::remove_file(&self.0) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => panic!("failed to remove test socket: {error}"),
}
}
}
}