Skip to main content

purple_ssh/
ping.rs

1use std::net::{TcpStream, ToSocketAddrs};
2use std::sync::mpsc;
3use std::thread;
4use std::time::{Duration, Instant};
5
6use log::{debug, warn};
7
8use crate::event::AppEvent;
9
10/// Ping a single host by attempting a TCP connection on the configured port.
11/// Sends the result back via the channel.
12///
13/// DNS resolution runs in a nested thread with a 5s timeout via `recv_timeout`.
14/// If DNS hangs beyond 5s, the outer thread reports unreachable and exits,
15/// but the inner thread may linger until the OS DNS resolver times out
16/// (typically 30-60s). This is inherent to blocking `to_socket_addrs` with
17/// no cancellation support. Repeated pings to hosts with broken DNS can
18/// temporarily accumulate threads, but they will self-clean once the OS
19/// resolver gives up.
20pub fn ping_host(
21    alias: String,
22    hostname: String,
23    port: u16,
24    tx: mpsc::Sender<AppEvent>,
25    generation: u64,
26) {
27    thread::spawn(move || {
28        ping_host_inner(&alias, &hostname, port, &tx, generation);
29    });
30}
31
32/// Core ping logic shared by `ping_host` and `ping_all`.
33fn ping_host_inner(
34    alias: &str,
35    hostname: &str,
36    port: u16,
37    tx: &mpsc::Sender<AppEvent>,
38    generation: u64,
39) {
40    // Strip existing brackets from IPv6 addresses (e.g. "[::1]" -> "::1")
41    let clean = hostname.trim_start_matches('[').trim_end_matches(']');
42    let addr_str = if clean.contains(':') {
43        format!("[{}]:{}", clean, port)
44    } else {
45        format!("{}:{}", hostname, port)
46    };
47
48    // Run DNS + TCP connect in a child thread with an overall 5s timeout
49    // (to_socket_addrs has no built-in timeout and can hang on bad DNS)
50    let (done_tx, done_rx) = mpsc::channel();
51    let addr_str_clone = addr_str.clone();
52    thread::spawn(move || {
53        // NOTE: RTT includes DNS resolution time, not just TCP connect.
54        // A slow DNS resolver can inflate the measured RTT.
55        let start = Instant::now();
56        let connected = match addr_str_clone.to_socket_addrs() {
57            Ok(addrs) => addrs
58                .into_iter()
59                .any(|addr| TcpStream::connect_timeout(&addr, Duration::from_secs(3)).is_ok()),
60            Err(_) => false,
61        };
62        let rtt_ms = if connected {
63            Some(start.elapsed().as_millis().min(u32::MAX as u128) as u32)
64        } else {
65            None
66        };
67        let _ = done_tx.send(rtt_ms);
68    });
69
70    let rtt_ms = match done_rx.recv_timeout(Duration::from_secs(5)) {
71        Ok(rtt) => rtt,
72        Err(e) => {
73            warn!(
74                "[external] ping timeout: alias={} addr={} ({})",
75                alias, addr_str, e
76            );
77            None
78        }
79    };
80
81    debug!(
82        "[purple] ping result: alias={} addr={} rtt_ms={:?} gen={}",
83        alias, addr_str, rtt_ms, generation
84    );
85    let _ = tx.send(AppEvent::PingResult {
86        alias: alias.to_string(),
87        rtt_ms,
88        generation,
89    });
90}
91
92/// Ping all given hosts with a concurrency limit of 10.
93/// Spawns a coordinator thread that uses a semaphore-style channel
94/// to limit concurrent pings, preventing thread explosion on large host lists.
95pub fn ping_all(hosts: &[(String, String, u16)], tx: mpsc::Sender<AppEvent>, generation: u64) {
96    let hosts = hosts.to_vec();
97    debug!(
98        "[purple] ping_all: hosts={} gen={}",
99        hosts.len(),
100        generation
101    );
102    thread::spawn(move || {
103        let max_concurrent: usize = 10;
104        let (slot_tx, slot_rx) = mpsc::channel();
105        for _ in 0..max_concurrent {
106            let _ = slot_tx.send(());
107        }
108        for (alias, hostname, port) in hosts {
109            let _ = slot_rx.recv(); // wait for a slot
110            let slot_tx = slot_tx.clone();
111            let tx = tx.clone();
112            thread::spawn(move || {
113                ping_host_inner(&alias, &hostname, port, &tx, generation);
114                let _ = slot_tx.send(()); // release slot
115            });
116        }
117    });
118}