use super::util;
#[derive(Debug, Clone)]
pub struct PingStats {
pub sent: u32,
pub times_ms: Vec<f64>,
pub packet_loss_pct: f64,
}
impl PingStats {
pub fn all_lost(sent: u32) -> Self {
Self {
sent,
times_ms: Vec::new(),
packet_loss_pct: 100.0,
}
}
pub fn received(&self) -> u32 {
self.times_ms.len() as u32
}
pub fn avg_ms(&self) -> Option<f64> {
if self.times_ms.is_empty() {
None
} else {
Some(self.times_ms.iter().sum::<f64>() / self.times_ms.len() as f64)
}
}
pub fn min_ms(&self) -> Option<f64> {
self.times_ms.iter().cloned().reduce(f64::min)
}
pub fn max_ms(&self) -> Option<f64> {
self.times_ms.iter().cloned().reduce(f64::max)
}
pub fn jitter_ms(&self) -> Option<f64> {
if self.times_ms.len() > 1 {
let diffs: Vec<f64> = self
.times_ms
.windows(2)
.map(|w| (w[1] - w[0]).abs())
.collect();
Some(diffs.iter().sum::<f64>() / diffs.len() as f64)
} else {
None
}
}
pub fn merged_with(mut self, other: PingStats) -> PingStats {
self.sent += other.sent;
self.times_ms.extend(other.times_ms);
self.packet_loss_pct = if self.sent == 0 {
0.0
} else {
(self.sent.saturating_sub(self.received())) as f64 / self.sent as f64 * 100.0
};
self
}
}
pub fn ping_args(host: &str, count: u32) -> Vec<String> {
#[cfg(windows)]
{
vec![
"-n".to_string(),
count.to_string(),
"-w".to_string(),
"2000".to_string(),
host.to_string(),
]
}
#[cfg(target_os = "macos")]
{
vec![
"-c".to_string(),
count.to_string(),
"-W".to_string(),
"2000".to_string(),
host.to_string(),
]
}
#[cfg(all(unix, not(target_os = "macos")))]
{
vec![
"-c".to_string(),
count.to_string(),
"-W".to_string(),
"2".to_string(),
host.to_string(),
]
}
}
pub async fn run_ping(host: &str, count: u32) -> Option<String> {
let mut cmd = tokio::process::Command::new("ping");
cmd.args(ping_args(host, count));
let output = util::run_with_timeout(cmd, util::ping_budget(count)).await?;
if output.status.success() {
Some(String::from_utf8_lossy(&output.stdout).into_owned())
} else {
None
}
}
pub fn parse_ping(stdout: &str, sent: u32) -> PingStats {
let mut times: Vec<f64> = Vec::new();
let mut summary_loss: Option<f64> = None;
for line in stdout.lines() {
if let Some(time) = extract_time(line) {
times.push(time);
}
if (line.contains("loss") || line.contains("Lost")) && summary_loss.is_none() {
summary_loss = extract_loss_pct(line);
}
}
let packet_loss_pct = summary_loss.unwrap_or_else(|| {
if sent == 0 {
0.0
} else {
(sent.saturating_sub(times.len() as u32)) as f64 / sent as f64 * 100.0
}
});
PingStats {
sent,
times_ms: times,
packet_loss_pct,
}
}
fn extract_time(line: &str) -> Option<f64> {
if let Some(pos) = line.find("time=") {
let after = &line[pos + 5..];
let num: String = after
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect();
return num.parse().ok();
}
if let Some(pos) = line.find("time<") {
let after = &line[pos + 5..];
let num: String = after
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect();
return num.parse().ok();
}
None
}
fn extract_loss_pct(line: &str) -> Option<f64> {
if let Some(pos) = line.find('%') {
let before = &line[..pos];
let num_str: String = before
.chars()
.rev()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect::<String>()
.chars()
.rev()
.collect();
return num_str.parse().ok();
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(target_os = "macos")]
fn macos_ping_timeout_uses_milliseconds() {
assert_eq!(
ping_args("1.1.1.1", 4),
vec!["-c", "4", "-W", "2000", "1.1.1.1"]
);
}
#[test]
#[cfg(all(unix, not(target_os = "macos")))]
fn linux_ping_timeout_uses_seconds() {
assert_eq!(
ping_args("1.1.1.1", 4),
vec!["-c", "4", "-W", "2", "1.1.1.1"]
);
}
#[test]
#[cfg(windows)]
fn windows_ping_timeout_uses_milliseconds() {
assert_eq!(
ping_args("1.1.1.1", 4),
vec!["-n", "4", "-w", "2000", "1.1.1.1"]
);
}
const WINDOWS_TRANSCRIPT: &str = "\
Pinging 1.1.1.1 with 32 bytes of data:
Reply from 1.1.1.1: bytes=32 time=12ms TTL=58
Reply from 1.1.1.1: bytes=32 time=11ms TTL=58
Reply from 1.1.1.1: bytes=32 time=14ms TTL=58
Request timed out.
Ping statistics for 1.1.1.1:
Packets: Sent = 4, Received = 3, Lost = 1 (25% loss),
Approximate round trip times in milli-seconds:
Minimum = 11ms, Maximum = 14ms, Average = 12ms";
const LINUX_TRANSCRIPT: &str = "\
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_seq=1 ttl=58 time=12.3 ms
64 bytes from 1.1.1.1: icmp_seq=2 ttl=58 time=11.8 ms
64 bytes from 1.1.1.1: icmp_seq=4 ttl=58 time=13.1 ms
--- 1.1.1.1 ping statistics ---
4 packets transmitted, 3 received, 25% packet loss, time 3004ms
rtt min/avg/max/mdev = 11.800/12.400/13.100/0.535 ms";
const MACOS_TRANSCRIPT: &str = "\
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: icmp_seq=0 ttl=58 time=12.345 ms
64 bytes from 1.1.1.1: icmp_seq=1 ttl=58 time=11.872 ms
--- 1.1.1.1 ping statistics ---
2 packets transmitted, 2 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 11.872/12.108/12.345/0.236 ms";
#[test]
fn parses_windows_transcript() {
let stats = parse_ping(WINDOWS_TRANSCRIPT, 4);
assert_eq!(stats.received(), 3);
assert_eq!(stats.packet_loss_pct, 25.0);
assert_eq!(stats.min_ms(), Some(11.0));
assert_eq!(stats.max_ms(), Some(14.0));
}
#[test]
fn parses_linux_transcript() {
let stats = parse_ping(LINUX_TRANSCRIPT, 4);
assert_eq!(stats.received(), 3);
assert_eq!(stats.packet_loss_pct, 25.0);
assert!((stats.avg_ms().unwrap() - 12.4).abs() < 0.01);
}
#[test]
fn parses_macos_transcript() {
let stats = parse_ping(MACOS_TRANSCRIPT, 2);
assert_eq!(stats.received(), 2);
assert_eq!(stats.packet_loss_pct, 0.0);
assert!(stats.jitter_ms().unwrap() > 0.0);
}
#[test]
fn zero_reply_transcript_is_all_lost() {
let stats = parse_ping("Request timed out.\nRequest timed out.", 2);
assert_eq!(stats.received(), 0);
assert!(stats.avg_ms().is_none());
assert_eq!(stats.packet_loss_pct, 100.0);
}
#[test]
fn sub_millisecond_reply_parses() {
let stats = parse_ping("Reply from 192.168.1.1: bytes=32 time<1ms TTL=64", 1);
assert_eq!(stats.received(), 1);
assert_eq!(stats.times_ms[0], 1.0);
}
#[test]
fn merged_bursts_recompute_loss() {
let first = PingStats::all_lost(3);
let second = parse_ping("64 bytes from 1.1.1.1: icmp_seq=1 ttl=58 time=10.0 ms", 3);
let merged = first.merged_with(second);
assert_eq!(merged.sent, 6);
assert_eq!(merged.received(), 1);
assert!((merged.packet_loss_pct - 83.333).abs() < 0.01);
}
}