pinger-1.1.0/.cargo_vcs_info.json0000644000000001440000000000100123160ustar { "git": { "sha1": "7547db0d2d8c22cf15066d51fcd41dc57cb5b046" }, "path_in_vcs": "pinger" }pinger-1.1.0/Cargo.toml0000644000000020650000000000100103200ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "pinger" version = "1.1.0" authors = ["Tom Forbes "] description = "A small cross-platform library to execute the ping command and parse the output" license = "MIT" repository = "https://github.com/orf/pinger/" [dependencies.anyhow] version = "1.0.75" [dependencies.lazy-regex] version = "3.1.0" [dependencies.rand] version = "0.8.5" [dependencies.thiserror] version = "1.0.50" [dev-dependencies.os_info] version = "3.6.0" [target."cfg(windows)".dependencies.dns-lookup] version = "2.0.0" [target."cfg(windows)".dependencies.winping] version = "0.10.1" pinger-1.1.0/Cargo.toml.orig000064400000000000000000000007241046102023000140010ustar 00000000000000[package] name = "pinger" version = "1.1.0" authors = ["Tom Forbes "] edition = "2018" license = "MIT" description = "A small cross-platform library to execute the ping command and parse the output" repository = "https://github.com/orf/pinger/" [dependencies] anyhow = "1.0.75" thiserror = "1.0.50" rand = "0.8.5" lazy-regex = "3.1.0" [target.'cfg(windows)'.dependencies] winping = "0.10.1" dns-lookup = "2.0.0" [dev-dependencies] os_info = "3.6.0" pinger-1.1.0/src/bsd.rs000064400000000000000000000022761046102023000130230ustar 00000000000000use crate::{Parser, PingResult, Pinger}; use lazy_regex::*; use std::time::Duration; pub static RE: Lazy = lazy_regex!(r"time=(?:(?P[0-9]+).(?P[0-9]+)\s+ms)"); pub struct BSDPinger { interval: Duration, interface: Option, } impl Pinger for BSDPinger { type Parser = BSDParser; fn new(interval: Duration, interface: Option) -> Self { Self { interface, interval, } } fn ping_args(&self, target: String) -> (&str, Vec) { let mut args = vec![format!( "-i{:.1}", self.interval.as_millis() as f32 / 1_000_f32 )]; if let Some(interface) = &self.interface { args.push("-I".into()); args.push(interface.clone()); } args.push(target); ("ping", args) } } #[derive(Default)] pub struct BSDParser {} impl Parser for BSDParser { fn parse(&self, line: String) -> Option { if line.starts_with("PING ") { return None; } if line.starts_with("Request timeout") { return Some(PingResult::Timeout(line)); } self.extract_regex(&RE, line) } } pinger-1.1.0/src/fake.rs000064400000000000000000000026231046102023000131550ustar 00000000000000use crate::{Parser, PingResult, Pinger}; use rand::prelude::*; use std::sync::mpsc; use std::sync::mpsc::Receiver; use std::thread; use std::time::Duration; pub struct FakePinger { interval: Duration, } impl Pinger for FakePinger { type Parser = FakeParser; fn new(interval: Duration, _interface: Option) -> Self { Self { interval } } fn start(&self, _target: String) -> anyhow::Result> { let (tx, rx) = mpsc::channel(); let sleep_time = self.interval; thread::spawn(move || { let mut random = rand::thread_rng(); loop { let fake_seconds = random.gen_range(50..150); let ping_result = PingResult::Pong( Duration::from_millis(fake_seconds), format!("Fake ping line: {fake_seconds} ms"), ); if tx.send(ping_result).is_err() { break; } std::thread::sleep(sleep_time); } }); Ok(rx) } fn ping_args(&self, _target: String) -> (&str, Vec) { unimplemented!("ping_args not implemented for FakePinger") } } #[derive(Default)] pub struct FakeParser {} impl Parser for FakeParser { fn parse(&self, _line: String) -> Option { unimplemented!("parse for FakeParser not implemented") } } pinger-1.1.0/src/lib.rs000064400000000000000000000150351046102023000130160ustar 00000000000000#[cfg(unix)] use crate::linux::{detect_linux_ping, LinuxPingType}; /// Pinger /// This crate exposes a simple function to ping remote hosts across different operating systems. /// Example: /// ```no_run /// use pinger::{ping, PingResult}; /// /// let stream = ping("tomforb.es".to_string(), None).expect("Error pinging"); /// for message in stream { /// match message { /// PingResult::Pong(duration, line) => println!("{:?} (line: {})", duration, line), /// PingResult::Timeout(_) => println!("Timeout!"), /// PingResult::Unknown(line) => println!("Unknown line: {}", line), /// PingResult::PingExited(_code, _stderr) => {} /// } /// } /// ``` use anyhow::{Context, Result}; use lazy_regex::Regex; use std::fmt::Formatter; use std::io::{BufRead, BufReader}; use std::process::{Child, Command, ExitStatus, Stdio}; use std::sync::mpsc; use std::time::Duration; use std::{fmt, thread}; use thiserror::Error; pub mod linux; // pub mod alpine' pub mod macos; #[cfg(windows)] pub mod windows; mod bsd; mod fake; #[cfg(test)] mod test; pub fn run_ping(cmd: &str, args: Vec) -> Result { Command::new(cmd) .args(&args) .stdout(Stdio::piped()) .stderr(Stdio::piped()) // Required to ensure that the output is formatted in the way we expect, not // using locale specific delimiters. .env("LANG", "C") .env("LC_ALL", "C") .spawn() .with_context(|| format!("Failed to run ping with args {:?}", &args)) } pub trait Pinger { type Parser: Parser; fn new(interval: Duration, interface: Option) -> Self; fn start(&self, target: String) -> Result> { let (tx, rx) = mpsc::channel(); let (cmd, args) = self.ping_args(target); let mut child = run_ping(cmd, args)?; let stdout = child.stdout.take().context("child did not have a stdout")?; thread::spawn(move || { let parser = Self::Parser::default(); let reader = BufReader::new(stdout).lines(); for line in reader { match line { Ok(msg) => { if let Some(result) = parser.parse(msg) { if tx.send(result).is_err() { break; } } } Err(_) => break, } } let result = child.wait_with_output().expect("Child wasn't started?"); let decoded_stderr = String::from_utf8(result.stderr).expect("Error decoding stderr"); let _ = tx.send(PingResult::PingExited(result.status, decoded_stderr)); }); Ok(rx) } fn ping_args(&self, target: String) -> (&str, Vec) { ("ping", vec![target]) } } pub trait Parser: Default { fn parse(&self, line: String) -> Option; fn extract_regex(&self, regex: &Regex, line: String) -> Option { let cap = regex.captures(&line)?; let ms = cap .name("ms") .expect("No capture group named 'ms'") .as_str() .parse::() .ok()?; let ns = match cap.name("ns") { None => 0, Some(cap) => { let matched_str = cap.as_str(); let number_of_digits = matched_str.len() as u32; let fractional_ms = matched_str.parse::().ok()?; fractional_ms * (10u64.pow(6 - number_of_digits)) } }; let duration = Duration::from_millis(ms) + Duration::from_nanos(ns); Some(PingResult::Pong(duration, line)) } } #[derive(Debug)] pub enum PingResult { Pong(Duration, String), Timeout(String), Unknown(String), PingExited(ExitStatus, String), } impl fmt::Display for PingResult { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match &self { PingResult::Pong(duration, _) => write!(f, "{duration:?}"), PingResult::Timeout(_) => write!(f, "Timeout"), PingResult::Unknown(_) => write!(f, "Unknown"), PingResult::PingExited(status, stderr) => write!(f, "Exited({status}, {stderr})"), } } } #[derive(Error, Debug)] pub enum PingDetectionError { #[error("Could not detect ping. Stderr: {stderr:?}\nStdout: {stdout:?}")] UnknownPing { stderr: Vec, stdout: Vec, }, #[error(transparent)] CommandError(#[from] anyhow::Error), #[error("Installed ping is not supported: {alternative}")] NotSupported { alternative: String }, } #[derive(Error, Debug)] pub enum PingError { #[error("Could not detect ping command type")] UnsupportedPing(#[from] PingDetectionError), #[error("Invalid or unresolvable hostname {0}")] HostnameError(String), } /// Start pinging a an address. The address can be either a hostname or an IP address. pub fn ping(addr: String, interface: Option) -> Result> { ping_with_interval(addr, Duration::from_millis(200), interface) } /// Start pinging a an address. The address can be either a hostname or an IP address. pub fn ping_with_interval( addr: String, interval: Duration, interface: Option, ) -> Result> { if std::env::var("PINGER_FAKE_PING") .map(|e| e == "1") .unwrap_or(false) { let fake = fake::FakePinger::new(interval, interface); return fake.start(addr); } #[cfg(windows)] { let p = windows::WindowsPinger::new(interval, interface); return p.start(addr); } #[cfg(unix)] { if cfg!(target_os = "freebsd") || cfg!(target_os = "dragonfly") || cfg!(target_os = "openbsd") || cfg!(target_os = "netbsd") { let p = bsd::BSDPinger::new(interval, interface); p.start(addr) } else if cfg!(target_os = "macos") { let p = macos::MacOSPinger::new(interval, interface); p.start(addr) } else { match detect_linux_ping() { Ok(LinuxPingType::IPTools) => { let p = linux::LinuxPinger::new(interval, interface); p.start(addr) } Ok(LinuxPingType::BusyBox) => { let p = linux::AlpinePinger::new(interval, interface); p.start(addr) } Err(e) => Err(PingError::UnsupportedPing(e))?, } } } } pinger-1.1.0/src/linux.rs000064400000000000000000000066261046102023000134150ustar 00000000000000use crate::{run_ping, Parser, PingDetectionError, PingResult, Pinger}; use anyhow::Context; use lazy_regex::*; use std::time::Duration; #[derive(Debug, Eq, PartialEq)] pub enum LinuxPingType { BusyBox, IPTools, } pub fn detect_linux_ping() -> Result { let child = run_ping("ping", vec!["-V".to_string()])?; let output = child .wait_with_output() .context("Error getting ping stdout/stderr")?; let stdout = String::from_utf8(output.stdout).context("Error decoding ping stdout")?; let stderr = String::from_utf8(output.stderr).context("Error decoding ping stderr")?; if stderr.contains("BusyBox") { Ok(LinuxPingType::BusyBox) } else if stdout.contains("iputils") { Ok(LinuxPingType::IPTools) } else if stdout.contains("inetutils") { Err(PingDetectionError::NotSupported { alternative: "Please use iputils ping, not inetutils.".to_string(), }) } else { let first_two_lines_stderr: Vec = stderr.lines().take(2).map(str::to_string).collect(); let first_two_lines_stout: Vec = stdout.lines().take(2).map(str::to_string).collect(); Err(PingDetectionError::UnknownPing { stdout: first_two_lines_stout, stderr: first_two_lines_stderr, }) } } pub struct LinuxPinger { interval: Duration, interface: Option, } impl Pinger for LinuxPinger { type Parser = LinuxParser; fn new(interval: Duration, interface: Option) -> Self { Self { interval, interface, } } fn ping_args(&self, target: String) -> (&str, Vec) { // The -O flag ensures we "no answer yet" messages from ping // See https://superuser.com/questions/270083/linux-ping-show-time-out let mut args = vec![ "-O".to_string(), format!("-i{:.1}", self.interval.as_millis() as f32 / 1_000_f32), ]; if let Some(interface) = &self.interface { args.push("-I".into()); args.push(interface.clone()); } args.push(target); ("ping", args) } } pub struct AlpinePinger {} // Alpine doesn't support timeout notifications, so we don't add the -O flag here impl Pinger for AlpinePinger { type Parser = LinuxParser; fn new(_interval: Duration, _interface: Option) -> Self { Self {} } } pub static UBUNTU_RE: Lazy = lazy_regex!(r"(?i-u)time=(?P\d+)(?:\.(?P\d+))? *ms"); #[derive(Default)] pub struct LinuxParser {} impl Parser for LinuxParser { fn parse(&self, line: String) -> Option { if line.starts_with("64 bytes from") { return self.extract_regex(&UBUNTU_RE, line); } else if line.starts_with("no answer yet") { return Some(PingResult::Timeout(line)); } None } } #[cfg(test)] mod tests { #[test] #[cfg(target_os = "linux")] fn test_linux_detection() { use super::*; use os_info::Type; let ping_type = detect_linux_ping().expect("Error getting ping"); match os_info::get().os_type() { Type::Alpine => { assert_eq!(ping_type, LinuxPingType::BusyBox) } Type::Ubuntu => { assert_eq!(ping_type, LinuxPingType::IPTools) } _ => {} } } } pinger-1.1.0/src/macos.rs000064400000000000000000000025101046102023000133440ustar 00000000000000use crate::{Parser, PingResult, Pinger}; use lazy_regex::*; use std::net::Ipv6Addr; use std::time::Duration; pub static RE: Lazy = lazy_regex!(r"time=(?:(?P[0-9]+).(?P[0-9]+)\s+ms)"); pub struct MacOSPinger { interval: Duration, interface: Option, } impl Pinger for MacOSPinger { type Parser = MacOSParser; fn new(interval: Duration, interface: Option) -> Self { Self { interval, interface, } } fn ping_args(&self, target: String) -> (&str, Vec) { let cmd = match target.parse::() { Ok(_) => "ping6", Err(_) => "ping", }; let mut args = vec![ format!("-i{:.1}", self.interval.as_millis() as f32 / 1_000_f32), target, ]; if let Some(interface) = &self.interface { args.push("-b".into()); args.push(interface.clone()); } (cmd, args) } } #[derive(Default)] pub struct MacOSParser {} impl Parser for MacOSParser { fn parse(&self, line: String) -> Option { if line.starts_with("PING ") { return None; } if line.starts_with("Request timeout") { return Some(PingResult::Timeout(line)); } self.extract_regex(&RE, line) } } pinger-1.1.0/src/test.rs000064400000000000000000000044131046102023000132250ustar 00000000000000#[cfg(test)] mod tests { use crate::bsd::BSDParser; use crate::linux::LinuxParser; use crate::macos::MacOSParser; use crate::{Parser, PingResult}; #[cfg(windows)] use crate::windows::WindowsParser; fn test_parser(contents: &str) where T: Parser, { let parser = T::default(); let test_file: Vec<&str> = contents.split("-----").collect(); let input = test_file[0].trim().split('\n'); let expected: Vec<&str> = test_file[1].trim().split('\n').collect(); let parsed: Vec> = input.map(|l| parser.parse(l.to_string())).collect(); assert_eq!( parsed.len(), expected.len(), "Parsed: {:?}, Expected: {:?}", &parsed, &expected ); for (idx, (output, expected)) in parsed.into_iter().zip(expected).enumerate() { if let Some(value) = output { assert_eq!( format!("{value}").trim(), expected.trim(), "Failed at idx {idx}" ) } else { assert_eq!("None", expected.trim(), "Failed at idx {idx}") } } } #[test] fn macos() { test_parser::(include_str!("tests/macos.txt")); } #[test] fn freebsd() { test_parser::(include_str!("tests/bsd.txt")); } #[test] fn dragonfly() { test_parser::(include_str!("tests/bsd.txt")); } #[test] fn openbsd() { test_parser::(include_str!("tests/bsd.txt")); } #[test] fn netbsd() { test_parser::(include_str!("tests/bsd.txt")); } #[test] fn ubuntu() { test_parser::(include_str!("tests/ubuntu.txt")); } #[test] fn debian() { test_parser::(include_str!("tests/debian.txt")); } #[cfg(windows)] #[test] fn windows() { test_parser::(include_str!("tests/windows.txt")); } #[test] fn android() { test_parser::(include_str!("tests/android.txt")); } #[test] fn alpine() { test_parser::(include_str!("tests/alpine.txt")); } } pinger-1.1.0/src/tests/alpine.txt000064400000000000000000000004541046102023000150540ustar 00000000000000PING google.com (142.250.178.14): 56 data bytes 64 bytes from 142.250.178.14: seq=0 ttl=37 time=19.236 ms 64 bytes from 142.250.178.14: seq=1 ttl=37 time=19.319 ms 64 bytes from 142.250.178.14: seq=2 ttl=37 time=17.944 ms ping: sendto: Network unreachable ----- None 19.236ms 19.319ms 17.944ms None pinger-1.1.0/src/tests/android.txt000064400000000000000000000014211046102023000152170ustar 00000000000000PING google.com (172.217.173.46) 56(84) bytes of data. 64 bytes from bog02s12-in-f14.1e100.net (172.217.173.46): icmp_seq=1 ttl=110 time=106 ms 64 bytes from bog02s12-in-f14.1e100.net (172.217.173.46): icmp_seq=2 ttl=110 time=142 ms 64 bytes from bog02s12-in-f14.1e100.net (172.217.173.46): icmp_seq=3 ttl=110 time=244 ms 64 bytes from bog02s12-in-f14.1e100.net (172.217.173.46): icmp_seq=4 ttl=110 time=120 ms 64 bytes from bog02s12-in-f14.1e100.net (172.217.173.46): icmp_seq=5 ttl=110 time=122 ms 64 bytes from 172.217.173.46: icmp_seq=6 ttl=110 time=246 ms --- google.com ping statistics --- 6 packets transmitted, 6 received, 0% packet loss, time 5018ms rtt min/avg/max/mdev = 106.252/163.821/246.851/58.823 ms ----- None 106ms 142ms 244ms 120ms 122ms 246ms None None None Nonepinger-1.1.0/src/tests/bsd.txt000064400000000000000000000004171046102023000143530ustar 00000000000000PING google.com (216.58.198.174): 56 data bytes 64 bytes from 96.47.72.84: icmp_seq=0 ttl=50 time=111.525 ms ping: sendto: Host is down 64 bytes from 96.47.72.84: icmp_seq=1 ttl=50 time=110.395 ms ping: sendto: No route to host ----- None 111.525ms None 110.395ms None pinger-1.1.0/src/tests/debian.txt000064400000000000000000000005661046102023000150320ustar 00000000000000PING google.com (216.58.209.78): 56 data bytes 64 bytes from 216.58.209.78: icmp_seq=0 ttl=37 time=21.308 ms 64 bytes from 216.58.209.78: icmp_seq=1 ttl=37 time=15.769 ms ^C--- google.com ping statistics --- 8 packets transmitted, 8 packets received, 0% packet loss round-trip min/avg/max/stddev = 15.282/20.347/41.775/8.344 ms ----- None 21.308ms 15.769ms None None None pinger-1.1.0/src/tests/macos.txt000064400000000000000000000012551046102023000147060ustar 00000000000000PING google.com (216.58.209.78): 56 data bytes 64 bytes from 216.58.209.78: icmp_seq=0 ttl=119 time=14.621 ms 64 bytes from 216.58.209.78: icmp_seq=1 ttl=119 time=33.898 ms 64 bytes from 216.58.209.78: icmp_seq=2 ttl=119 time=17.305 ms 64 bytes from 216.58.209.78: icmp_seq=3 ttl=119 time=24.235 ms 64 bytes from 216.58.209.78: icmp_seq=4 ttl=119 time=15.242 ms 64 bytes from 216.58.209.78: icmp_seq=5 ttl=119 time=16.639 ms Request timeout for icmp_seq 19 Request timeout for icmp_seq 20 Request timeout for icmp_seq 21 64 bytes from 216.58.209.78: icmp_seq=30 ttl=119 time=16.943 ms ----- None 14.621ms 33.898ms 17.305ms 24.235ms 15.242ms 16.639ms Timeout Timeout Timeout 16.943ms pinger-1.1.0/src/tests/ubuntu.txt000064400000000000000000000021021046102023000151160ustar 00000000000000PING google.com (216.58.209.78) 56(84) bytes of data. 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=1 ttl=37 time=25.1 ms 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=2 ttl=37 time=19.4 ms 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=3 ttl=37 time=14.9 ms 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=4 ttl=37 time=22.8 ms 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=5 ttl=37 time=13.9 ms 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=6 ttl=37 time=77.6 ms 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=7 ttl=37 time=158 ms no answer yet for icmp_seq=8 no answer yet for icmp_seq=9 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=18 ttl=37 time=357 ms 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=19 ttl=37 time=85.2 ms 64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=20 ttl=37 time=17.8 ms ----- None 25.1ms 19.4ms 14.9ms 22.8ms 13.9ms 77.6ms 158ms Timeout Timeout 357ms 85.2ms 17.8ms pinger-1.1.0/src/tests/windows.txt000064400000000000000000000007001046102023000152700ustar 00000000000000pinging example.microsoft.com [192.168.239.132] with 32 bytes of data: Reply from 192.168.239.132: bytes=32 time=101ms TTL=124 Reply from 192.168.239.132: bytes=32 time=100ms TTL=124 Reply from 192.168.239.132: bytes=32 time=120ms TTL=124 Reply from 192.168.239.132: bytes=32 time=120ms TTL=124 Request timed out. Request timed out. Reply from 192.168.239.132: bytes=32 time=120ms TTL=124 ----- None 101ms 100ms 120ms 120ms Timeout Timeout 120ms pinger-1.1.0/src/windows.rs000064400000000000000000000046141046102023000137430ustar 00000000000000use crate::{Parser, PingError, PingResult, Pinger}; use anyhow::Result; use dns_lookup::lookup_host; use lazy_regex::*; use std::net::IpAddr; use std::sync::mpsc; use std::thread; use std::time::Duration; use winping::{Buffer, Pinger as WinPinger}; pub static RE: Lazy = lazy_regex!(r"(?ix-u)time=(?P\d+)(?:\.(?P\d+))?"); pub struct WindowsPinger { interval: Duration, } impl Pinger for WindowsPinger { type Parser = WindowsParser; fn new(interval: Duration, _interface: Option) -> Self { Self { interval } } fn start(&self, target: String) -> Result> { let interval = self.interval; let parsed_ip: IpAddr = match target.parse() { Err(_) => { let things = lookup_host(target.as_str())?; if things.is_empty() { Err(PingError::HostnameError(target)) } else { Ok(things[0]) } } Ok(addr) => Ok(addr), }?; let (tx, rx) = mpsc::channel(); thread::spawn(move || { let pinger = WinPinger::new().expect("Failed to create a WinPinger instance"); let mut buffer = Buffer::new(); loop { match pinger.send(parsed_ip.clone(), &mut buffer) { Ok(rtt) => { if tx .send(PingResult::Pong( Duration::from_millis(rtt as u64), "".to_string(), )) .is_err() { break; } } Err(_) => { // Fuck it. All errors are timeouts. Why not. if tx.send(PingResult::Timeout("".to_string())).is_err() { break; } } } thread::sleep(interval); } }); Ok(rx) } } #[derive(Default)] pub struct WindowsParser {} impl Parser for WindowsParser { fn parse(&self, line: String) -> Option { if line.contains("timed out") || line.contains("failure") { return Some(PingResult::Timeout(line)); } self.extract_regex(&RE, line) } }