Skip to main content

toole_core/
discovery.rs

1use crate::{Peer, ToolError, UI};
2use std::collections::HashMap;
3use std::net::{IpAddr, Ipv4Addr, SocketAddr};
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7use tokio::net::UdpSocket;
8
9const DISCOVERY_PORT: u16 = 58199;
10const BROADCAST_INTERVAL: Duration = Duration::from_secs(3);
11const PEER_TIMEOUT: Duration = Duration::from_secs(9);
12
13// collecte les adresses broadcast de chaque interface réseau montée,
14// puis le broadcast illimité 255.255.255.255 en secours (souvent filtré en WiFi)
15// je l'expose en pub pour pouvoir la tester unitairement dans le crate tests/
16pub fn broadcast_targets() -> Vec<SocketAddr> {
17    let mut ips: Vec<Ipv4Addr> = Vec::new();
18
19    if let Ok(ifaces) = if_addrs::get_if_addrs() {
20        for iface in ifaces {
21            if iface.is_loopback() {
22                continue;
23            }
24            if let if_addrs::IfAddr::V4(v4) = iface.addr {
25                if let Some(bcast) = v4.broadcast {
26                    ips.push(bcast);
27                }
28                // si l'OS ne fournit pas le broadcast, on le déduit ip | ~masque
29                let bcast = Ipv4Addr::from(u32::from(v4.ip) | !u32::from(v4.netmask));
30                if bcast != Ipv4Addr::BROADCAST {
31                    ips.push(bcast);
32                }
33            }
34        }
35    }
36
37    ips.push(Ipv4Addr::BROADCAST);
38    ips.dedup();
39
40    ips.into_iter()
41        .map(|ip| SocketAddr::new(IpAddr::V4(ip), DISCOVERY_PORT))
42        .collect()
43}
44
45pub async fn start_discovery(
46    local_ip: String,
47    stop: Arc<AtomicBool>,
48    ui: Arc<dyn UI>,
49) -> Result<(), ToolError> {
50    let socket = Arc::new(UdpSocket::bind(format!("0.0.0.0:{DISCOVERY_PORT}")).await?);
51    socket.set_broadcast(true)?;
52    ui.log(&format!("Decouverte demarree sur le port {DISCOVERY_PORT}"));
53
54    let targets = broadcast_targets();
55    let me: IpAddr = local_ip.parse().unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST));
56
57    let my_id = crate::utils::device_id();
58    let mut last_seen: HashMap<String, Instant> = HashMap::new();
59    let mut interval = tokio::time::interval(BROADCAST_INTERVAL);
60    let mut buf = [0u8; 1024];
61
62    loop {
63        if stop.load(Ordering::Relaxed) {
64            break;
65        }
66
67        let now = Instant::now();
68        let expired: Vec<String> = last_seen
69            .iter()
70            .filter(|(_, t)| now.duration_since(**t) > PEER_TIMEOUT)
71            .map(|(h, _)| h.clone())
72            .collect();
73        for h in expired {
74            last_seen.remove(&h);
75            ui.peer_lost(&h);
76        }
77
78        tokio::select! {
79            _ = interval.tick() => {
80                let msg = b"TOOLE_DISCOVERY";
81                for dest in &targets {
82                    let _ = socket.send_to(msg, dest).await;
83                }
84            }
85            result = socket.recv_from(&mut buf) => {
86                if let Ok((len, addr)) = result {
87                    if addr.ip() == me {
88                        continue;
89                    }
90                    let msg = String::from_utf8_lossy(&buf[..len]);
91
92                    if msg == "TOOLE_DISCOVERY" {
93                        let response = format!("TOOLE_HERE:{}", my_id);
94                        let _ = socket.send_to(response.as_bytes(), addr).await;
95                    } else if let Some(id) = msg.strip_prefix("TOOLE_HERE:") {
96                        if id != my_id {
97                            let peer = Peer {
98                                id: id.to_string(),
99                                addr: addr.ip().to_string(),
100                            };
101                            // je ne notifie l'UI qu'à la première apparition
102                            // d'un pair : le remote répond à chaque broadcast
103                            // (3s) et à chaque cible → doublons sinon
104                            let is_new = !last_seen.contains_key(id);
105                            last_seen.insert(id.to_string(), Instant::now());
106                            if is_new {
107                                ui.peer_found(&peer);
108                            }
109                        }
110                    }
111                }
112            }
113            _ = tokio::time::sleep(Duration::from_millis(200)) => {
114                // je relance la boucle toutes les 200 ms pour re-tester `stop` :
115                // l'arrêt libère ainsi la socket en ~200 ms max (au lieu
116                // d'attendre le prochain broadcast), ce qui permet au refresh
117                // de relancer la découverte sans race sur le port UDP
118            }
119        }
120    }
121
122    Ok(())
123}