use super::HerdrTransport;
#[cfg(unix)]
use super::IPC_TIMEOUT;
use std::{io, path::Path};
#[cfg(unix)]
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy)]
pub(super) struct UnixSocketTransport;
impl HerdrTransport for UnixSocketTransport {
fn write_line(&self, socket_path: &Path, line: &str) -> io::Result<()> {
write_line_to_socket(socket_path, line)
}
}
#[cfg(unix)]
pub(super) fn write_line_to_socket(socket_path: &Path, line: &str) -> io::Result<()> {
use std::{
io::{ErrorKind, Write},
os::{
fd::{AsRawFd, FromRawFd, OwnedFd},
unix::net::UnixStream,
},
};
let (address, address_len) = unix_socket_address(socket_path)?;
let raw_fd = unsafe {
libc::socket(libc::AF_UNIX, libc::SOCK_STREAM, 0)
};
if raw_fd < 0 {
return Err(io::Error::last_os_error());
}
let owned_fd = unsafe {
OwnedFd::from_raw_fd(raw_fd)
};
let mut stream = UnixStream::from(owned_fd);
stream.set_nonblocking(true)?;
stream.set_write_timeout(Some(IPC_TIMEOUT))?;
let deadline = Instant::now()
.checked_add(IPC_TIMEOUT)
.ok_or_else(|| io::Error::new(ErrorKind::TimedOut, "HERDR socket deadline overflow"))?;
let result = unsafe {
libc::connect(
stream.as_raw_fd(),
(&address as *const libc::sockaddr_un).cast::<libc::sockaddr>(),
address_len,
)
};
let error = (result != 0).then(io::Error::last_os_error);
match classify_connect_result(
result,
error.as_ref().and_then(|error| error.raw_os_error()),
) {
ConnectDisposition::Connected => {}
ConnectDisposition::Pending => {
wait_for_unix_socket(stream.as_raw_fd(), libc::POLLOUT, deadline)?;
if let Some(error) = stream.take_error()? {
return Err(error);
}
}
ConnectDisposition::Failed => return Err(error.unwrap_or_else(io::Error::last_os_error)),
}
let bytes = line.as_bytes();
let mut written = 0;
while written < bytes.len() {
if Instant::now() >= deadline {
return Err(io::Error::new(
ErrorKind::TimedOut,
"HERDR socket write timed out",
));
}
match stream.write(&bytes[written..]) {
Ok(0) => {
return Err(io::Error::new(
ErrorKind::WriteZero,
"HERDR socket closed during write",
));
}
Ok(count) => written += count,
Err(error) if error.kind() == ErrorKind::Interrupted => {}
Err(error) if error.kind() == ErrorKind::WouldBlock => {
wait_for_unix_socket(stream.as_raw_fd(), libc::POLLOUT, deadline)?;
}
Err(error) => return Err(error),
}
}
Ok(())
}
#[cfg(unix)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ConnectDisposition {
Connected,
Pending,
Failed,
}
#[cfg(unix)]
pub(super) fn classify_connect_result(result: i32, raw_error: Option<i32>) -> ConnectDisposition {
if result == 0 {
ConnectDisposition::Connected
} else if raw_error.is_some_and(is_pending_connect_error) {
ConnectDisposition::Pending
} else {
ConnectDisposition::Failed
}
}
#[cfg(unix)]
fn is_pending_connect_error(code: i32) -> bool {
code == libc::EINTR
|| code == libc::EINPROGRESS
|| code == libc::EALREADY
|| code == libc::EAGAIN
|| code == libc::EWOULDBLOCK
}
#[cfg(unix)]
fn unix_socket_address(socket_path: &Path) -> io::Result<(libc::sockaddr_un, libc::socklen_t)> {
use std::{mem, os::unix::ffi::OsStrExt, ptr};
let bytes = socket_path.as_os_str().as_bytes();
if bytes.is_empty() || bytes.contains(&0) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"HERDR socket path must be a non-empty pathname without NUL bytes",
));
}
let mut address: libc::sockaddr_un = unsafe { mem::zeroed() };
address.sun_family = libc::AF_UNIX as libc::sa_family_t;
if bytes.len() >= address.sun_path.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"HERDR socket path exceeds the platform Unix-socket limit",
));
}
unsafe {
ptr::copy_nonoverlapping(
bytes.as_ptr(),
address.sun_path.as_mut_ptr().cast::<u8>(),
bytes.len(),
);
}
let address_len =
(mem::offset_of!(libc::sockaddr_un, sun_path) + bytes.len() + 1) as libc::socklen_t;
Ok((address, address_len))
}
#[cfg(unix)]
pub(super) fn poll_timeout_ms(remaining: Duration) -> i32 {
remaining
.as_nanos()
.div_ceil(1_000_000)
.clamp(1, i32::MAX as u128) as i32
}
#[cfg(unix)]
fn wait_for_unix_socket(
fd: std::os::fd::RawFd,
events: libc::c_short,
deadline: Instant,
) -> io::Result<()> {
loop {
let Some(remaining) = deadline.checked_duration_since(Instant::now()) else {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
"HERDR socket operation timed out",
));
};
let timeout_ms = poll_timeout_ms(remaining);
let mut poll_fd = libc::pollfd {
fd,
events,
revents: 0,
};
let result = unsafe {
libc::poll(&mut poll_fd, 1, timeout_ms)
};
if result > 0 {
if poll_fd.revents & libc::POLLNVAL != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"HERDR socket descriptor became invalid",
));
}
if poll_fd.revents & (events | libc::POLLERR | libc::POLLHUP) != 0 {
return Ok(());
}
} else if result == 0 {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
"HERDR socket operation timed out",
));
} else {
let error = io::Error::last_os_error();
if error.raw_os_error() != Some(libc::EINTR) {
return Err(error);
}
}
}
}
#[cfg(not(unix))]
pub(super) fn write_line_to_socket(_socket_path: &Path, _line: &str) -> io::Result<()> {
Ok(())
}