use iroh::endpoint::{Connection, Path};
use iroh::{Endpoint, EndpointAddr};
use std::sync::RwLock;
use std::time::Duration;
use crate::ConnectError;
use crate::connect::ConnectOptions;
use crate::lifecycle::{Lifecycle, PeerPath, aggregate};
use crate::status::PipeStatus;
use crate::ticket::Ticket;
use crate::transport;
pub(crate) struct Peer {
endpoint: Endpoint,
addr: EndpointAddr,
connection: RwLock<Option<Connection>>,
}
impl Peer {
pub(crate) async fn bind(ticket: &Ticket, opts: &ConnectOptions) -> Result<Self, ConnectError> {
let addr = transport::addr_from(ticket)?;
let net = transport::NetOptions {
port_mapping: opts.port_mapping,
discovery: opts.discovery,
};
let endpoint = transport::bind(opts.relay.as_deref(), None, net).await?;
Ok(Self {
endpoint,
addr,
connection: RwLock::new(None),
})
}
pub(crate) fn current(&self) -> Option<Connection> {
self.read().clone()
}
pub(crate) fn forget(&self, dead: &Connection) {
let mut held = self.write();
if held
.as_ref()
.is_some_and(|live| live.stable_id() == dead.stable_id())
{
*held = None;
}
}
pub(crate) async fn redial(&self) -> Option<PeerPath> {
let connection = self
.endpoint
.connect(self.addr.clone(), transport::ALPN)
.await
.ok()?;
let path = path_of(&connection);
*self.write() = Some(connection);
Some(path)
}
pub(crate) fn close(&self, reason: &[u8]) {
let dying = self.write().take();
if let Some(connection) = dying {
connection.close(0u32.into(), reason);
}
}
pub(crate) async fn close_endpoint(&self) {
self.endpoint.close().await;
}
fn read(&self) -> std::sync::RwLockReadGuard<'_, Option<Connection>> {
self.connection
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn write(&self) -> std::sync::RwLockWriteGuard<'_, Option<Connection>> {
self.connection
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
pub(crate) fn path_of(connection: &Connection) -> PeerPath {
connection
.paths()
.iter()
.find(Path::is_selected)
.map_or(PeerPath::Relayed, |path| {
if path.remote_addr().is_relay() {
PeerPath::Relayed
} else {
PeerPath::Direct
}
})
}
const FIRST_RETRY: Duration = Duration::from_millis(500);
const RETRY_CEILING: Duration = Duration::from_secs(30);
pub(crate) async fn keep_connected(peer: &Peer, lifecycle: &Lifecycle) {
let mut backoff = FIRST_RETRY;
let mut announced = false;
loop {
if let Some(live) = peer.current() {
tokio::select! {
biased;
() = lifecycle.wait_until_closed() => return,
_ = live.closed() => {}
}
peer.forget(&live);
lifecycle.set_status(PipeStatus::Idle);
tracing::info!("the peer went away, and this side is looking for it");
announced = true;
backoff = FIRST_RETRY;
}
let dialed = tokio::select! {
biased;
() = lifecycle.wait_until_closed() => return,
dialed = peer.redial() => dialed,
};
if let Some(path) = dialed {
let status = aggregate(&[path]);
lifecycle.set_status(status);
tracing::info!(path = status.as_str(), "the peer is back");
announced = false;
continue;
}
if !announced {
announced = true;
tracing::info!("the serve side did not answer, and this side is looking for it");
}
tracing::debug!(
backoff_ms = u64::try_from(backoff.as_millis()).unwrap_or(u64::MAX),
"a dial found nobody"
);
tokio::select! {
biased;
() = lifecycle.wait_until_closed() => return,
() = tokio::time::sleep(backoff) => {}
}
backoff = (backoff * 2).min(RETRY_CEILING);
}
}
#[cfg(test)]
#[path = "peer_tests.rs"]
mod peer_tests;