use crate::{
ProcessId,
local::{ConnectError, Duplex, Endpoint, PlatformDuplex, peer::PeerCredentials},
peer::{ExpectedProcess, Policy, Rejected, SameUser},
transport::SplitIo,
wire::{self, Control, Health, Hello, operation},
};
use std::{fmt, io, time::Instant};
#[derive(Debug)]
#[non_exhaustive]
pub enum RequestError {
Unavailable(ConnectError),
Peer(Rejected),
Credentials(io::Error),
ProcessMismatch {
peer: ProcessId,
reported: u32,
},
Late,
Protocol(io::Error),
}
impl RequestError {
pub fn is_transient(&self) -> bool {
matches!(self, Self::Unavailable(error) if error.is_transient())
}
}
impl fmt::Display for RequestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unavailable(error) => write!(f, "control endpoint unavailable: {error}"),
Self::Peer(rejected) => write!(f, "control peer refused: {rejected}"),
Self::ProcessMismatch { peer, reported } => write!(
f,
"control reply names process {reported}, but the connection belongs to {peer}"
),
Self::Late => f.write_str("control reply arrived after the deadline"),
Self::Credentials(error) => write!(f, "control peer credentials failed: {error}"),
Self::Protocol(error) => write!(f, "control exchange failed: {error}"),
}
}
}
impl std::error::Error for RequestError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Unavailable(error) => Some(error),
Self::Peer(rejected) => Some(rejected),
Self::Protocol(error) | Self::Credentials(error) => Some(error),
Self::ProcessMismatch { .. } | Self::Late => None,
}
}
}
pub fn health(
endpoint: &Endpoint,
offer: &Hello,
expected: Option<ProcessId>,
deadline: Instant,
) -> Result<Health, RequestError> {
request(endpoint, offer, operation::HEALTH, expected, deadline)
}
pub fn drain(
endpoint: &Endpoint,
offer: &Hello,
expected: Option<ProcessId>,
deadline: Instant,
) -> Result<Health, RequestError> {
request(endpoint, offer, operation::DRAIN, expected, deadline)
}
fn request(
endpoint: &Endpoint,
offer: &Hello,
operation: u32,
expected: Option<ProcessId>,
deadline: Instant,
) -> Result<Health, RequestError> {
let stream = PlatformDuplex::connect_once_until(endpoint, deadline)
.map_err(RequestError::Unavailable)?;
let evidence = stream.credentials().map_err(RequestError::Credentials)?;
match expected {
Some(pid) => ExpectedProcess::new(move || Some(pid))
.grant(&evidence)
.map(drop),
None => SameUser.grant(&evidence),
}
.map_err(RequestError::Peer)?;
let peer = evidence
.pid
.ok_or(RequestError::Peer(Rejected::Unreported))?;
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(RequestError::Late);
}
stream
.set_read_deadline(Some(remaining))
.map_err(RequestError::Protocol)?;
let (read, write) = stream.split().map_err(RequestError::Protocol)?;
let mut session =
wire::Session::connect(SplitIo { read, write }, offer).map_err(RequestError::Protocol)?;
let message = Control {
operation,
..Default::default()
};
session.send(&message).map_err(RequestError::Protocol)?;
let reply = session.receive().map_err(RequestError::Protocol)?;
let health = Health::from_response(&reply, &message).map_err(RequestError::Protocol)?;
if health.process_id != peer.get() {
return Err(RequestError::ProcessMismatch {
peer,
reported: health.process_id,
});
}
if Instant::now() >= deadline {
return Err(RequestError::Late);
}
Ok(health)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_an_endpoint_nothing_served_yet_is_worth_retrying() {
assert!(RequestError::Unavailable(ConnectError::ConnectTimeout).is_transient());
assert!(!RequestError::Unavailable(ConnectError::PermissionDenied).is_transient());
assert!(!RequestError::Unavailable(ConnectError::EndpointTooLong).is_transient());
assert!(!RequestError::Late.is_transient());
assert!(!RequestError::Peer(Rejected::OtherUser).is_transient());
assert!(!RequestError::Protocol(io::ErrorKind::Other.into()).is_transient());
}
#[test]
fn nested_control_errors_keep_their_sources() {
for error in [
RequestError::Unavailable(ConnectError::Failed(io::ErrorKind::InvalidInput)),
RequestError::Peer(Rejected::OtherUser),
RequestError::Credentials(io::Error::other("credential query denied")),
RequestError::Protocol(io::Error::other("bad frame")),
] {
assert!(std::error::Error::source(&error).is_some());
assert!(!error.is_transient());
}
assert!(
std::error::Error::source(&RequestError::ProcessMismatch {
peer: ProcessId::new(7).unwrap(),
reported: 9,
})
.is_none()
);
}
#[test]
fn every_failure_says_what_happened() {
let peer = ProcessId::new(7).unwrap();
let cases: [(RequestError, &str); 6] = [
(
RequestError::Credentials(io::Error::other("denied")),
"control peer credentials failed: denied",
),
(
RequestError::Unavailable(ConnectError::ConnectTimeout),
"control endpoint unavailable: daemon connection timed out",
),
(
RequestError::Peer(Rejected::OtherUser),
"control peer refused: the peer runs as another OS user",
),
(
RequestError::ProcessMismatch { peer, reported: 9 },
"control reply names process 9, but the connection belongs to 7",
),
(
RequestError::Late,
"control reply arrived after the deadline",
),
(
RequestError::Protocol(io::Error::other("bad frame")),
"control exchange failed: bad frame",
),
];
for (error, text) in cases {
assert_eq!(error.to_string(), text);
}
assert!(std::error::Error::source(&RequestError::Late).is_none());
assert!(std::error::Error::source(&RequestError::Peer(Rejected::OtherUser)).is_some());
}
}