use super::*;
use crate::peer::{self, Identity, Waiting};
use anyhow::Context;
use futures_util::{SinkExt, StreamExt};
use std::time::Duration;
use tokio::time::Instant;
use tokio_tungstenite::tungstenite::{client::IntoClientRequest, Message as WsMessage};
type Socket =
tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
const CONNECT_TIMEOUT: Duration = Duration::from_secs(20);
const SILENCE: Duration = Duration::from_secs(60);
const RETRY_EVERY: Duration = Duration::from_secs(600);
const FALLBACK_AFTER: u32 = 6;
const LOOK_EVERY: Duration = Duration::from_secs(60);
enum Left {
NoFriends,
Closed(String),
}
pub(crate) fn spawn_peer_link(app: Arc<App>) {
spawn_prints(app.clone());
tokio::spawn(async move {
let mut attempt: u32 = 0;
loop {
let Some(me) = friend_identity(&app).await else {
attempt = 0;
tokio::select! {
_ = tokio::time::sleep(LOOK_EVERY) => {},
_ = app.peers.wake.notified() => {},
}
continue;
};
match connect(&me).await {
Ok(socket) => {
if attempt > 0 {
eprintln!("snyvi: the relay is back");
}
attempt = 0;
match linked(&app, &me, socket).await {
Ok(Left::NoFriends) => continue,
Ok(Left::Closed(why)) => eprintln!("snyvi: the relay link closed: {why}"),
Err(e) => eprintln!("snyvi: the relay link: {e:#}"),
}
}
Err(e) => {
if attempt == 0 {
eprintln!("snyvi: the relay: {e:#}");
}
}
}
attempt += 1;
if attempt >= FALLBACK_AFTER {
let app_b = app.clone();
let me_b = me.clone();
let _ = tokio::task::spawn_blocking(move || {
flush_outbox(&app_b, None);
if let Err(e) = bring_in(&app_b, &me_b) {
eprintln!("snyvi: the relay, over HTTP: {e:#}");
}
})
.await;
}
tokio::select! {
_ = tokio::time::sleep(peer::backoff(attempt)) => {},
_ = app.peers.wake.notified() => {},
}
}
});
}
const PRINTS_FIRST: Duration = Duration::from_secs(20);
const PRINTS_EVERY: Duration = Duration::from_secs(600);
const PRINTS_ROUND: usize = 40;
fn spawn_prints(app: Arc<App>) {
tokio::spawn(async move {
tokio::time::sleep(PRINTS_FIRST).await;
loop {
if has_friends(&app) {
let app_b = app.clone();
let _ = tokio::task::spawn_blocking(move || print_folders(&app_b)).await;
}
tokio::time::sleep(PRINTS_EVERY).await;
}
});
}
pub(crate) fn print_folders(app: &App) -> usize {
let before = crate::store::now() - PRINT_KEPT;
let Ok(roots) = app.store.roots_to_print(before) else {
return 0;
};
let mut n = 0;
for root in roots.into_iter().take(PRINTS_ROUND) {
let dir = std::path::Path::new(&root);
let print = if dir.is_dir() {
crate::git::print(dir)
} else {
None
};
n += print.is_some() as usize;
let _ = app.store.set_print(&root, print.as_ref());
}
n
}
async fn friend_identity(app: &Arc<App>) -> Option<Identity> {
if !has_friends(app) {
return None;
}
let app_b = app.clone();
tokio::task::spawn_blocking(move || identity_if_any(&app_b))
.await
.ok()
.flatten()
}
fn has_friends(app: &App) -> bool {
app.store
.peers()
.map(|ps| ps.iter().any(|p| p.removed_at == 0))
.unwrap_or(false)
}
async fn connect(me: &Identity) -> anyhow::Result<Socket> {
let path = format!("/inbox/{}", me.address());
let mut req = peer::relay_ws(&me.address())
.into_client_request()
.context("the relay's address")?;
req.headers_mut().insert(
"x-snyvi-auth",
me.relay_auth("GET", &path)
.parse()
.context("the signature as a header")?,
);
let (socket, _) = tokio::time::timeout(CONNECT_TIMEOUT, tokio_tungstenite::connect_async(req))
.await
.map_err(|_| anyhow::anyhow!("no answer to the link in {} s", CONNECT_TIMEOUT.as_secs()))?
.context("opening the link")?;
Ok(socket)
}
async fn linked(app: &Arc<App>, me: &Identity, mut socket: Socket) -> anyhow::Result<Left> {
let app_b = app.clone();
let me_b = me.clone();
tokio::task::spawn_blocking(move || {
let _ = app_b.store.prune_peer_taken();
sweep_offers(&app_b);
read_held(&app_b, &me_b);
flush_outbox(&app_b, None);
})
.await?;
let mut ping = tokio::time::interval(peer::PING_EVERY);
ping.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
ping.tick().await;
let mut retry = tokio::time::interval(RETRY_EVERY);
retry.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
retry.tick().await;
let mut heard = Instant::now();
let mut pending: Option<Waiting> = None;
loop {
let silence = tokio::time::sleep_until(heard + SILENCE);
tokio::select! {
msg = socket.next() => {
let Some(msg) = msg else {
return Ok(Left::Closed("the relay hung up".into()));
};
let msg = msg.context("reading the link")?;
heard = Instant::now();
match msg {
WsMessage::Text(t) => {
if t.as_str() == "pong" {
continue;
}
let Ok(peer::Pushed::Frame(w)) = serde_json::from_str::<peer::Pushed>(t.as_str()) else {
continue;
};
if w.size as usize <= peer::INLINE_MAX {
pending = Some(w);
continue;
}
pending = None;
let me_b = me.clone();
let id = w.id.clone();
match tokio::task::spawn_blocking(move || peer::fetch(&me_b, &id)).await? {
Ok(bytes) => took(app, me, &mut socket, w, bytes).await?,
Err(e) => eprintln!("snyvi: a frame from the relay could not be fetched: {e:#}"),
}
}
WsMessage::Binary(b) => {
if let Some(w) = pending.take() {
took(app, me, &mut socket, w, b.to_vec()).await?;
}
}
WsMessage::Close(_) => return Ok(Left::Closed("closed by the relay".into())),
_ => {}
}
}
_ = ping.tick() => {
socket.send(WsMessage::Text("ping".into())).await.context("the ping")?;
}
_ = silence => {
let _ = socket.close(None).await;
return Ok(Left::Closed(format!("nothing heard in {} s", SILENCE.as_secs())));
}
_ = app.peers.wake.notified() => {
if !has_friends(app) {
let _ = socket.close(None).await;
return Ok(Left::NoFriends);
}
let app_b = app.clone();
tokio::task::spawn_blocking(move || {
sweep_offers(&app_b);
flush_outbox(&app_b, None);
})
.await?;
}
_ = retry.tick() => {
let app_b = app.clone();
tokio::task::spawn_blocking(move || flush_outbox(&app_b, None)).await?;
}
}
}
}
async fn took(
app: &Arc<App>,
me: &Identity,
socket: &mut Socket,
w: Waiting,
bytes: Vec<u8>,
) -> anyhow::Result<()> {
let app_b = app.clone();
let me_b = me.clone();
let id = w.id.clone();
match tokio::task::spawn_blocking(move || take_in(&app_b, &me_b, &w, bytes)).await? {
Ok(_) => socket
.send(WsMessage::Text(peer::ack_message(&id).into()))
.await
.context("the ack")?,
Err(e) => eprintln!("snyvi: a frame from the relay was left there: {e:#}"),
}
Ok(())
}