pinger-1.2.3/.cargo_vcs_info.json0000644000000001440000000000100123220ustar { "git": { "sha1": "891430c882ae0d928b5e386fd05cfe1624e493f7" }, "path_in_vcs": "pinger" }pinger-1.2.3/Cargo.lock0000644000000306530000000000100103050ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anyhow" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "dns-lookup" version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5766087c2235fec47fafa4cfecc81e494ee679d0fd4a59887ea0919bfb0e4fc" dependencies = [ "cfg-if", "libc", "socket2", "windows-sys 0.48.0", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "lazy-regex" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d12be4595afdf58bd19e4a9f4e24187da2a66700786ff660a418e9059937a4c" dependencies = [ "lazy-regex-proc_macros", "once_cell", "regex", ] [[package]] name = "lazy-regex-proc_macros" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44bcd58e6c97a7fcbaffcdc95728b393b8d98933bfadad49ed4097845b57ef0b" dependencies = [ "proc-macro2", "quote", "regex", "syn", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "os_info" version = "3.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" dependencies = [ "log", "serde", "windows-sys 0.52.0", ] [[package]] name = "pinger" version = "1.2.3" dependencies = [ "anyhow", "dns-lookup", "lazy-regex", "os_info", "rand", "thiserror", "winping", ] [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "regex" version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "serde" version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "socket2" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" version = "2.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thiserror" version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winapi_forked_icmpapi" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42aecb895d6340af9ccc8dab9aeabfeab6d5d7266c5fd172c8be7e07db71c1e3" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winping" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79ed0e3a789beb896b3de9fb7e93c76340f6f4adfab7770d6222b4b8625ef0aa" dependencies = [ "lazy_static", "static_assertions", "winapi_forked_icmpapi", ] pinger-1.2.3/Cargo.toml0000644000000021120000000000100103150ustar # 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.2.3" authors = ["Tom Forbes "] description = "A small cross-platform library to execute the ping command and parse the output" readme = "README.md" license = "MIT" repository = "https://github.com/orf/pinger/" [dependencies.anyhow] version = "1.0.81" [dependencies.lazy-regex] version = "3.1.0" [dependencies.rand] version = "0.8.5" [dependencies.thiserror] version = "1.0.58" [dev-dependencies.os_info] version = "3.8.1" [target."cfg(windows)".dependencies.dns-lookup] version = "2.0.0" [target."cfg(windows)".dependencies.winping] version = "0.10.1" pinger-1.2.3/Cargo.toml.orig000064400000000000000000000007241046102023000140050ustar 00000000000000[package] name = "pinger" version = "1.2.3" 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.81" thiserror = "1.0.58" 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.8.1" pinger-1.2.3/README.md000064400000000000000000000016151046102023000123750ustar 00000000000000# pinger > A small cross-platform library to execute the ping command and parse the output. This crate is primarily built for use with `gping`, but it can also be used as a standalone library. This allows you to reliably ping hosts without having to worry about process permissions, in a cross-platform manner on Windows, Linux and macOS. ## Usage A full example of using the library can be found in the `examples/` directory, but the interface is quite simple: ```rust use pinger::ping; fn ping_google() { let stream = ping("google.com", None).expect("Error pinging"); for message in stream { match message { pinger::PingResult::Pong(duration, _) => { println!("Duration: {:?}", duration) } _ => {} // Handle errors, log ping timeouts, etc. } } } ``` ## Adding pinger to your project. `cargo add pinger` pinger-1.2.3/examples/simple-ping.rs000064400000000000000000000014111046102023000155200ustar 00000000000000use pinger::ping_with_interval; pub fn main() { let target = "tomforb.es".to_string(); let interval = std::time::Duration::from_secs(1); let stream = ping_with_interval(target, interval, None).expect("Error pinging"); for message in stream { match message { pinger::PingResult::Pong(duration, line) => { println!("Duration: {:?}\t\t(raw: {:?})", duration, line) } pinger::PingResult::Timeout(line) => println!("Timeout! (raw: {line:?})"), pinger::PingResult::Unknown(line) => println!("Unknown line: {:?}", line), pinger::PingResult::PingExited(code, stderr) => { println!("Ping exited! Code: {:?}. Stderr: {:?}", code, stderr) } } } } pinger-1.2.3/src/bsd.rs000064400000000000000000000022761046102023000130270ustar 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.2.3/src/fake.rs000064400000000000000000000026231046102023000131610ustar 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.2.3/src/lib.rs000064400000000000000000000150351046102023000130220ustar 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.2.3/src/linux.rs000064400000000000000000000066261046102023000134210ustar 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.2.3/src/macos.rs000064400000000000000000000025101046102023000133500ustar 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.2.3/src/test.rs000064400000000000000000000044131046102023000132310ustar 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.2.3/src/tests/alpine.txt000064400000000000000000000004541046102023000150600ustar 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.2.3/src/tests/android.txt000064400000000000000000000014211046102023000152230ustar 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.2.3/src/tests/bsd.txt000064400000000000000000000004171046102023000143570ustar 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.2.3/src/tests/debian.txt000064400000000000000000000005661046102023000150360ustar 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.2.3/src/tests/macos.txt000064400000000000000000000012551046102023000147120ustar 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.2.3/src/tests/ubuntu.txt000064400000000000000000000021021046102023000151220ustar 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.2.3/src/tests/windows.txt000064400000000000000000000007001046102023000152740ustar 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.2.3/src/windows.rs000064400000000000000000000046141046102023000137470ustar 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) } }