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 meta: String,
}
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, meta) = match outcome {
Ok(Ok(meta)) => (true, meta),
_ => (false, String::new()),
};
let entry = ReachEntry {
reachable,
rtt_ms: reachable.then(|| started.elapsed().as_millis() as u64),
probed_at: epoch_now_i64(),
meta,
};
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<String> {
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(v)) => Ok(pong_meta(&v)),
_ => anyhow::bail!("no pong from peer"),
}
}
fn pong_meta(pong: &serde_json::Value) -> String {
pong.get("meta")
.and_then(|m| m.as_str())
.filter(|s| s.len() <= crate::roster::presence::APP_METADATA_MAX_BYTES)
.unwrap_or_default()
.to_string()
}
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),
meta: e.meta.clone(),
principal: Some(mcpmesh_net::EndpointId::from_bytes(eid).principal()),
});
}
None => {
stale.push(eid);
out.push(mcpmesh_local_api::PeerReachability {
name: nickname,
reachable: false,
rtt_ms: None,
age_secs: None, meta: String::new(),
principal: Some(mcpmesh_net::EndpointId::from_bytes(eid).principal()),
});
}
}
}
for eid in stale {
let mesh = mesh.clone();
tokio::spawn(async move {
probe_peer(&mesh, eid).await;
});
}
out
}
#[cfg(test)]
mod tests {
use super::pong_meta;
use crate::roster::presence::APP_METADATA_MAX_BYTES;
#[test]
fn pong_meta_extracts_within_cap_and_drops_the_rest() {
assert_eq!(
pong_meta(&serde_json::json!({"stack_version": "1", "meta": "v=1.2.3"})),
"v=1.2.3"
);
assert_eq!(pong_meta(&serde_json::json!({"stack_version": "1"})), "");
assert_eq!(pong_meta(&serde_json::json!({"meta": 42})), "");
assert_eq!(pong_meta(&serde_json::json!({"meta": {"x": 1}})), "");
let at = "x".repeat(APP_METADATA_MAX_BYTES);
assert_eq!(pong_meta(&serde_json::json!({"meta": at.clone()})), at);
let over = "x".repeat(APP_METADATA_MAX_BYTES + 1);
assert_eq!(
pong_meta(&serde_json::json!({"meta": over})),
"",
"oversized meta dropped"
);
}
}