use std::io;
use std::time::{Duration, Instant};
#[cfg(unix)]
pub type Endpoint = std::path::Path;
#[cfg(windows)]
pub type Endpoint = str;
pub const CONNECT_BUDGET: Duration = Duration::from_secs(5);
pub const RETRY_INTERVAL: Duration = Duration::from_millis(50);
pub trait Duplex: Sized + peer::PeerCredentials {
type Reader: io::Read + Send + 'static;
type Writer: crate::transport::WriteHalf;
fn connect_once_until(endpoint: &Endpoint, deadline: Instant) -> Result<Self, ConnectError>;
fn connect_until(endpoint: &Endpoint, deadline: Instant) -> Result<Self, ConnectError> {
loop {
match Self::connect_once_until(endpoint, deadline) {
Ok(connected) => return Ok(connected),
Err(error) if !error.is_transient() => return Err(error),
Err(error) => {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(error);
}
#[expect(
clippy::disallowed_methods,
reason = "A missing endpoint has no event to await; retry connects within the caller deadline."
)]
std::thread::sleep(RETRY_INTERVAL.min(remaining));
}
}
}
}
fn connect_once(endpoint: &Endpoint) -> Result<Self, ConnectError> {
Self::connect_once_until(endpoint, Instant::now() + CONNECT_BUDGET)
}
fn connect(endpoint: &Endpoint) -> Result<Self, ConnectError> {
Self::connect_until(endpoint, Instant::now() + CONNECT_BUDGET)
}
fn set_read_deadline(&self, timeout: Option<Duration>) -> io::Result<()>;
fn split(self) -> io::Result<(Self::Reader, Self::Writer)>;
}
#[derive(Debug)]
pub enum Bound<L> {
Won(L),
AlreadyRunning,
}
#[cfg(unix)]
pub use unix::UnixDuplex as PlatformDuplex;
#[cfg(windows)]
pub use windows::WindowsDuplex as PlatformDuplex;
pub(crate) use process::wait_child;
pub use process::{ProcessHandle, process_has_exited, process_state};
#[cfg(all(unix, feature = "async"))]
pub use unix::shutdown_signal;
#[cfg(unix)]
pub use unix::terminate_legacy_peer;
#[cfg(all(windows, feature = "async"))]
pub use windows::shutdown_signal;
#[cfg(windows)]
pub use windows::terminate_legacy_peer;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProcessState {
Alive,
Exited,
Unknown,
}
#[cfg(unix)]
pub use unix_socket as socket;
#[cfg(windows)]
pub use windows_socket as socket;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ConnectError {
ConnectTimeout,
PermissionDenied,
EndpointTooLong,
Failed(io::ErrorKind),
}
impl std::fmt::Display for ConnectError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ConnectTimeout => f.write_str("daemon connection timed out"),
Self::PermissionDenied => f.write_str("daemon endpoint permission denied"),
Self::EndpointTooLong => {
f.write_str("daemon endpoint path is too long for a Unix socket")
}
Self::Failed(kind) => write!(f, "daemon connection failed: {kind}"),
}
}
}
impl std::error::Error for ConnectError {}
impl ConnectError {
pub fn is_transient(&self) -> bool {
matches!(self, Self::ConnectTimeout)
}
}
pub fn is_transient(error: &io::Error) -> bool {
use io::ErrorKind::{
BrokenPipe, ConnectionAborted, ConnectionRefused, ConnectionReset, Interrupted,
NotConnected, NotFound, TimedOut, UnexpectedEof, WouldBlock,
};
matches!(
error.kind(),
NotFound
| ConnectionRefused
| ConnectionReset
| ConnectionAborted
| NotConnected
| UnexpectedEof
| BrokenPipe
| TimedOut
| WouldBlock
| Interrupted
) || busy_pipe(error)
}
#[cfg(windows)]
use windows::busy_pipe;
#[cfg(unix)]
fn busy_pipe(_: &io::Error) -> bool {
false
}
pub(crate) fn connect_error(error: &io::Error) -> ConnectError {
if error.kind() == io::ErrorKind::PermissionDenied {
ConnectError::PermissionDenied
} else if is_transient(error) {
ConnectError::ConnectTimeout
} else {
ConnectError::Failed(error.kind())
}
}
pub mod peer;
mod process;
#[cfg(unix)]
pub mod unix;
#[cfg(unix)]
pub mod unix_socket;
#[cfg(windows)]
pub mod windows;
#[cfg(test)]
fn test_handshake(write: &mut impl io::Write, read: &mut impl io::Read, _: &str) -> io::Result<()> {
write.write_all(b"hello\n")?;
write.flush()?;
let mut reply = [0; 6];
read.read_exact(&mut reply)?;
if reply != *b"ready\n" {
return Err(io::Error::other("invalid test reply"));
}
Ok(())
}
#[cfg(windows)]
pub mod windows_socket;
#[cfg(windows)]
pub mod pipe_name;
#[cfg(all(test, not(windows)))]
mod pipe_name;
#[cfg(any(all(windows, feature = "launch"), test))]
pub(crate) mod command_line;
#[cfg(all(windows, feature = "launch"))]
pub(crate) mod windows_spawn;
#[cfg(test)]
mod connect_error_tests {
use super::{ConnectError, connect_error};
use std::io::{self, ErrorKind};
#[test]
fn an_absent_or_departing_peer_is_worth_retrying() {
for kind in [
ErrorKind::NotFound,
ErrorKind::ConnectionRefused,
ErrorKind::TimedOut,
ErrorKind::WouldBlock,
ErrorKind::Interrupted,
ErrorKind::ConnectionReset,
ErrorKind::ConnectionAborted,
ErrorKind::NotConnected,
ErrorKind::UnexpectedEof,
ErrorKind::BrokenPipe,
] {
assert!(super::is_transient(&kind.into()), "{kind:?}");
let error = connect_error(&kind.into());
assert_eq!(error, ConnectError::ConnectTimeout, "{kind:?}");
assert!(error.is_transient());
}
for kind in [
ErrorKind::PermissionDenied,
ErrorKind::InvalidInput,
ErrorKind::InvalidData,
ErrorKind::AddrInUse,
ErrorKind::Other,
] {
assert!(!super::is_transient(&kind.into()), "{kind:?}");
}
assert_eq!(
connect_error(&ErrorKind::PermissionDenied.into()),
ConnectError::PermissionDenied
);
let other = connect_error(&ErrorKind::InvalidInput.into());
assert_eq!(other, ConnectError::Failed(ErrorKind::InvalidInput));
assert!(!other.is_transient());
assert!(!ConnectError::PermissionDenied.is_transient());
assert!(!ConnectError::EndpointTooLong.is_transient());
}
#[test]
fn a_busy_pipe_is_retried_only_on_windows() {
let busy = connect_error(&io::Error::from_raw_os_error(231));
if cfg!(windows) {
assert_eq!(busy, ConnectError::ConnectTimeout);
} else {
assert_ne!(busy, ConnectError::ConnectTimeout);
}
assert_eq!(
super::is_transient(&io::Error::from_raw_os_error(231)),
cfg!(windows)
);
}
#[test]
fn a_temporary_connection_failure_is_retried_until_the_endpoint_accepts() {
use super::{Duplex, Endpoint, PlatformDuplex, peer::PeerCredentials};
use std::sync::atomic::{AtomicUsize, Ordering};
static ATTEMPTS: AtomicUsize = AtomicUsize::new(0);
struct StartsAfterOneAttempt;
impl PeerCredentials for StartsAfterOneAttempt {
fn credentials(&self) -> io::Result<crate::peer::Credentials> {
unreachable!()
}
}
impl Duplex for StartsAfterOneAttempt {
type Reader = <PlatformDuplex as Duplex>::Reader;
type Writer = <PlatformDuplex as Duplex>::Writer;
fn connect_once_until(
_: &Endpoint,
_: std::time::Instant,
) -> Result<Self, ConnectError> {
if ATTEMPTS.fetch_add(1, Ordering::Relaxed) == 0 {
Err(ConnectError::ConnectTimeout)
} else {
Ok(Self)
}
}
fn set_read_deadline(&self, _: Option<std::time::Duration>) -> io::Result<()> {
unreachable!()
}
fn split(self) -> io::Result<(Self::Reader, Self::Writer)> {
unreachable!()
}
}
#[cfg(unix)]
let endpoint = std::path::Path::new("unused");
#[cfg(windows)]
let endpoint = "unused";
assert!(
StartsAfterOneAttempt::connect_until(
endpoint,
std::time::Instant::now() + std::time::Duration::from_secs(1)
)
.is_ok()
);
assert_eq!(ATTEMPTS.load(Ordering::Relaxed), 2);
}
#[test]
fn every_failure_says_what_happened() {
let cases = [
(ConnectError::ConnectTimeout, "daemon connection timed out"),
(
ConnectError::PermissionDenied,
"daemon endpoint permission denied",
),
(
ConnectError::EndpointTooLong,
"daemon endpoint path is too long for a Unix socket",
),
(
ConnectError::Failed(ErrorKind::InvalidInput),
"daemon connection failed: invalid input parameter",
),
];
for (error, text) in cases {
assert_eq!(error.to_string(), text);
}
}
}
mod readiness;