use std::path::{Path, PathBuf};
use std::time::Duration;
use trusty_common::uds::{UdsRpcError, send_framed_request};
use trusty_common::webhook_relay::{RelayFrame, RelayResponse};
use super::spawn::SharedSupervisor;
use super::spool::SpoolEntry;
pub const DEFAULT_RELAY_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RelayOutcome {
Acked,
Refused {
reason: String,
},
Unreachable {
reason: String,
},
}
impl RelayOutcome {
pub fn is_acked(&self) -> bool {
matches!(self, RelayOutcome::Acked)
}
pub fn reason(&self) -> &str {
match self {
RelayOutcome::Acked => "acknowledged",
RelayOutcome::Refused { reason } | RelayOutcome::Unreachable { reason } => reason,
}
}
}
#[derive(Debug, Clone)]
pub struct UdsRelay {
socket: PathBuf,
timeout: Duration,
source: String,
supervisor: Option<SharedSupervisor>,
}
impl UdsRelay {
pub fn new(socket: impl Into<PathBuf>) -> Self {
Self {
socket: socket.into(),
timeout: DEFAULT_RELAY_TIMEOUT,
source: String::new(),
supervisor: None,
}
}
pub fn with_supervisor(
mut self,
source: impl Into<String>,
supervisor: SharedSupervisor,
) -> Self {
self.source = source.into();
self.supervisor = Some(supervisor);
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn socket(&self) -> &Path {
&self.socket
}
pub fn timeout(&self) -> Duration {
self.timeout
}
pub async fn deliver(&self, entry: &SpoolEntry) -> RelayOutcome {
let frame = RelayFrame::new(
&entry.delivery_id,
&entry.source,
&entry.event,
&entry.headers,
&entry.body_b64,
&entry.provenance,
entry.received_at_unix_ms,
entry.attempts,
);
if let Some(supervisor) = &self.supervisor
&& let Err(e) = supervisor.ensure_running(&self.source, &self.socket).await
{
return RelayOutcome::Unreachable {
reason: format!("could not start the {} target: {e}", self.source),
};
}
let response: Result<RelayResponse, UdsRpcError> =
send_framed_request(&self.socket, &frame, self.timeout).await;
match response {
Ok(resp) if resp.is_ack() => RelayOutcome::Acked,
Ok(resp) => RelayOutcome::Refused {
reason: refusal_reason(&resp),
},
Err(e) => RelayOutcome::Unreachable {
reason: format!("{e}"),
},
}
}
}
fn refusal_reason(resp: &RelayResponse) -> String {
if let Some(err) = &resp.error {
return format!(
"target rejected the frame: code {} — {}",
err.code, err.message
);
}
match resp.result.as_ref().and_then(|r| r.detail.clone()) {
Some(detail) => detail,
None if resp.result.is_some() => {
"target answered without an explicit \"ack\": true".to_string()
}
None => "target answered with neither a result nor an error".to_string(),
}
}