use std::io::{self, Read, Write};
use std::time::Duration;
use teksilo_automation::wire::{Endpoint, Transport};
#[cfg(unix)]
pub mod unix;
#[cfg(windows)]
pub mod windows;
pub trait TransportStream: Read + Write + Send {
fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()>;
}
pub trait TransportListener: Send {
fn accept(&mut self) -> io::Result<Box<dyn TransportStream>>;
}
pub struct BoundTransport {
pub listener: Box<dyn TransportListener>,
pub endpoint: Endpoint,
}
impl std::fmt::Debug for BoundTransport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BoundTransport")
.field("endpoint", &self.endpoint)
.finish_non_exhaustive()
}
}
pub fn bind(pid: u32) -> io::Result<BoundTransport> {
#[cfg(unix)]
{
unix::bind(pid)
}
#[cfg(windows)]
{
windows::bind(pid)
}
#[cfg(not(any(unix, windows)))]
{
let _ = pid;
Err(io::Error::new(
io::ErrorKind::Unsupported,
"the automation bridge has no transport on this platform",
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Liveness {
Live,
Busy,
Dead,
}
pub fn probe(endpoint: &Endpoint) -> Liveness {
let outcome: io::Result<Box<dyn TransportStream>> = match endpoint.transport {
#[cfg(unix)]
Transport::Unix => unix::connect(&endpoint.address),
#[cfg(windows)]
Transport::NamedPipe => windows::connect_within(&endpoint.address, PROBE_PATIENCE),
_ => return Liveness::Dead,
};
match outcome {
Ok(_) => Liveness::Live,
Err(e)
if e.kind() == io::ErrorKind::NotFound
|| e.kind() == io::ErrorKind::ConnectionRefused =>
{
Liveness::Dead
}
Err(_) => Liveness::Busy,
}
}
#[cfg(windows)]
const PROBE_PATIENCE: Duration = Duration::from_millis(300);
pub fn connect(endpoint: &Endpoint) -> io::Result<Box<dyn TransportStream>> {
match endpoint.transport {
#[cfg(unix)]
Transport::Unix => unix::connect(&endpoint.address),
#[cfg(windows)]
Transport::NamedPipe => windows::connect(&endpoint.address),
other => Err(io::Error::new(
io::ErrorKind::Unsupported,
format!("this build cannot open a {other:?} endpoint"),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_on_the_native_transport() {
let pid = std::process::id();
let mut bound = match bind(pid) {
Ok(b) => b,
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => return,
Err(e) => panic!("bind failed: {e}"),
};
assert_eq!(bound.endpoint.transport, Transport::native());
let endpoint = bound.endpoint.clone();
let server = std::thread::spawn(move || {
let mut peer = bound.listener.accept().expect("accept");
let mut got = [0u8; 5];
peer.read_exact(&mut got).expect("server read");
peer.write_all(b"pong!").expect("server write");
peer.flush().ok();
got
});
let mut client = connect(&endpoint).expect("connect");
client.write_all(b"ping!").expect("client write");
client.flush().expect("client flush");
let mut back = [0u8; 5];
client.read_exact(&mut back).expect("client read");
assert_eq!(&back, b"pong!");
assert_eq!(&server.join().unwrap(), b"ping!");
}
#[test]
fn read_timeout_is_reported_not_fatal() {
let pid = std::process::id().wrapping_add(1);
let mut bound = match bind(pid) {
Ok(b) => b,
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => return,
Err(e) => panic!("bind failed: {e}"),
};
let endpoint = bound.endpoint.clone();
let server = std::thread::spawn(move || {
let mut peer = bound.listener.accept().expect("accept");
peer.set_read_timeout(Some(Duration::from_millis(80)))
.expect("set timeout");
let mut buf = [0u8; 1];
let err = peer.read_exact(&mut buf).unwrap_err();
assert_eq!(
err.kind(),
io::ErrorKind::TimedOut,
"a silent peer must time out, not block forever: {err}"
);
peer.set_read_timeout(None).expect("clear timeout");
peer.read_exact(&mut buf).expect("read after timeout");
buf[0]
});
let mut client = connect(&endpoint).expect("connect");
std::thread::sleep(Duration::from_millis(250));
client.write_all(b"z").expect("client write");
client.flush().ok();
assert_eq!(server.join().unwrap(), b'z');
}
}