use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub enum BindError {
InvalidEndpoint(String),
AlreadyBound(String),
Io(std::io::Error),
Other(String),
}
impl fmt::Display for BindError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidEndpoint(s) => write!(f, "invalid endpoint: {s}"),
Self::AlreadyBound(s) => write!(f, "endpoint already bound: {s}"),
Self::Io(e) => write!(f, "bind io error: {e}"),
Self::Other(s) => write!(f, "bind error: {s}"),
}
}
}
impl Error for BindError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Io(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for BindError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
#[derive(Debug)]
pub enum Never {}
pub type IpcListener = interprocess::local_socket::Listener;
pub type Endpoint = str;
pub trait BrokeredBackend {
type State: Send + 'static;
fn bind(endpoint: &Endpoint) -> Result<IpcListener, BindError>;
fn serve(listener: IpcListener) -> Never;
}
#[allow(unreachable_code)]
pub fn bootstrap<B: BrokeredBackend>(endpoint: &Endpoint) -> Result<(), BindError> {
let listener = B::bind(endpoint)?;
match B::serve(listener) {}
}
#[cfg(test)]
mod tests {
use super::*;
use interprocess::local_socket::ListenerOptions;
struct StubBackend;
impl BrokeredBackend for StubBackend {
type State = ();
fn bind(endpoint: &Endpoint) -> Result<IpcListener, BindError> {
#[cfg(windows)]
let name = {
use interprocess::local_socket::{GenericNamespaced, ToNsName};
let bare = format!("rp-brokered-backend-stub-{endpoint}");
ToNsName::to_ns_name::<GenericNamespaced>(bare.as_str())?
.into_owned()
};
#[cfg(unix)]
let name = {
use interprocess::local_socket::{GenericFilePath, ToFsName};
let path = std::env::temp_dir()
.join(format!("rp-brokered-backend-stub-{endpoint}.sock"));
let _ = std::fs::remove_file(&path);
ToFsName::to_fs_name::<GenericFilePath>(
path.to_string_lossy().as_ref(),
)?
.into_owned()
};
let listener = ListenerOptions::new().name(name).create_sync()?;
Ok(listener)
}
fn serve(_listener: IpcListener) -> Never {
panic!("StubBackend::serve called");
}
}
#[test]
fn brokered_backend_bind_returns_listener_from_endpoint_only() {
fn _shape_check<B: BrokeredBackend>() -> fn(&Endpoint) -> Result<IpcListener, BindError> {
B::bind
}
let _ = _shape_check::<StubBackend>();
}
#[test]
fn bind_alone_yields_a_listening_endpoint() {
let listener = StubBackend::bind("bind-alone").expect("bind succeeds");
drop(listener);
}
#[test]
fn bootstrap_calls_bind_then_serve() {
let result = std::panic::catch_unwind(|| bootstrap::<StubBackend>("bootstrap-ordering"));
let payload = result.expect_err("bootstrap should reach the serve panic");
let message = payload
.downcast_ref::<&str>()
.copied()
.or_else(|| payload.downcast_ref::<String>().map(String::as_str))
.unwrap_or("");
assert!(
message.contains("StubBackend::serve called"),
"expected to reach serve, got panic payload: {message:?}"
);
}
#[test]
fn bootstrap_propagates_bind_failure_without_invoking_serve() {
struct FailingBackend;
impl BrokeredBackend for FailingBackend {
type State = ();
fn bind(_endpoint: &Endpoint) -> Result<IpcListener, BindError> {
Err(BindError::Other("synthetic failure".into()))
}
fn serve(_listener: IpcListener) -> Never {
panic!("serve must not run when bind fails");
}
}
let result =
std::panic::catch_unwind(|| bootstrap::<FailingBackend>("bootstrap-failure"));
let inner = result.expect("bind error should be returned, not a panic");
match inner {
Err(BindError::Other(msg)) => assert_eq!(msg, "synthetic failure"),
other => panic!("expected BindError::Other, got: {other:?}"),
}
}
}