use std::sync::Arc;
use std::time::Duration;
use anyhow::Result;
use mcpmesh_net::ALPN_PING;
use mcpmesh_net::framing::{FrameReader, Inbound, write_frame};
use crate::util::epoch_now_i64;
use super::MeshState;
#[derive(Clone)]
pub struct ReachEntry {
pub reachable: bool,
pub rtt_ms: Option<u64>,
pub probed_at: i64,
}
pub const REACH_TTL_SECS: i64 = 20;
const PROBE_TIMEOUT: Duration = Duration::from_secs(3);
pub async fn probe_peer(mesh: &Arc<MeshState>, endpoint_id: [u8; 32]) -> ReachEntry {
let started = std::time::Instant::now();
let outcome = tokio::time::timeout(PROBE_TIMEOUT, probe_once(mesh, endpoint_id)).await;
let reachable = matches!(outcome, Ok(Ok(())));
let entry = ReachEntry {
reachable,
rtt_ms: reachable.then(|| started.elapsed().as_millis() as u64),
probed_at: epoch_now_i64(),
};
mesh.reachability
.lock()
.expect("reachability lock not poisoned")
.insert(endpoint_id, entry.clone());
entry
}
async fn probe_once(mesh: &Arc<MeshState>, endpoint_id: [u8; 32]) -> Result<()> {
let id = iroh::EndpointId::from_bytes(&endpoint_id)
.map_err(|e| anyhow::anyhow!("invalid endpoint id: {e}"))?;
let store = mesh.store.clone();
let last_addr = tokio::task::spawn_blocking(move || store.resolve(&endpoint_id))
.await
.map_err(|e| anyhow::anyhow!("join peer resolve for probe: {e}"))?
.ok()
.flatten()
.and_then(|e| e.last_addr);
let addr = super::dial::stored_dial_addr(last_addr.as_deref(), id);
let conn = mesh.endpoint.connect(addr, ALPN_PING).await?;
let (mut send, recv) = conn.open_bi().await?;
write_frame(&mut send, &serde_json::json!({ "ping": true })).await?;
let _ = send.finish();
let mut reader = FrameReader::new(
tokio::io::BufReader::new(recv),
mcpmesh_net::framing::MAX_FRAME_BYTES,
);
match reader.next().await? {
Some(Inbound::Frame(_)) => Ok(()), _ => anyhow::bail!("no pong from peer"),
}
}
pub fn reachability_of(mesh: &Arc<MeshState>) -> Vec<mcpmesh_local_api::PeerReachability> {
let now = epoch_now_i64();
let peers: Vec<(String, [u8; 32])> = mesh
.store
.list()
.unwrap_or_default()
.into_iter()
.map(|e| (e.nickname, e.endpoint_id))
.collect();
let cache = mesh
.reachability
.lock()
.expect("reachability lock not poisoned")
.clone();
let mut stale: Vec<[u8; 32]> = Vec::new();
let mut out = Vec::with_capacity(peers.len());
for (nickname, eid) in peers {
match cache.get(&eid) {
Some(e) => {
let age = (now - e.probed_at).max(0);
if age > REACH_TTL_SECS {
stale.push(eid);
}
out.push(mcpmesh_local_api::PeerReachability {
name: nickname,
reachable: e.reachable,
rtt_ms: e.rtt_ms,
age_secs: Some(age as u64),
});
}
None => {
stale.push(eid);
out.push(mcpmesh_local_api::PeerReachability {
name: nickname,
reachable: false,
rtt_ms: None,
age_secs: None, });
}
}
}
for eid in stale {
let mesh = mesh.clone();
tokio::spawn(async move {
probe_peer(&mesh, eid).await;
});
}
out
}