pinger-2.0.0/.cargo_vcs_info.json0000644000000001440000000000100123160ustar { "git": { "sha1": "5c3d50cde32be86b8dcd4888722a73e510e1673a" }, "path_in_vcs": "pinger" }pinger-2.0.0/Cargo.lock0000644000000320470000000000100103000ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[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.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[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 = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "indexmap" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "lazy-regex" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d8e41c97e6bc7ecb552016274b99fbb5d035e8de288c582d9b933af6677bfda" dependencies = [ "lazy-regex-proc_macros", "once_cell", "regex", ] [[package]] name = "lazy-regex-proc_macros" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76e1d8b05d672c53cb9c7b920bbba8783845ae4f0b076e02a3db1d02c81b4163" dependencies = [ "proc-macro2", "quote", "regex", "syn 2.0.90", ] [[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.168" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[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 = "ntest" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb183f0a1da7a937f672e5ee7b7edb727bf52b8a52d531374ba8ebb9345c0330" dependencies = [ "ntest_test_cases", "ntest_timeout", ] [[package]] name = "ntest_test_cases" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16d0d3f2a488592e5368ebbe996e7f1d44aa13156efad201f5b4d84e150eaa93" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "ntest_timeout" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc7c92f190c97f79b4a332f5e81dcf68c8420af2045c936c9be0bc9de6f63b5" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "os_info" version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ca711d8b83edbb00b44d504503cd247c9c0bd8b0fa2694f2a1a3d8165379ce" dependencies = [ "log", "serde", "windows-sys", ] [[package]] name = "pinger" version = "2.0.0" dependencies = [ "anyhow", "lazy-regex", "ntest", "os_info", "rand", "thiserror", "winping", ] [[package]] name = "ppv-lite86" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro-crate" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 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.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "serde" version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", "syn 2.0.90", ] [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thiserror" version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f5383f3e0071702bf93ab5ee99b52d26936be9dedd9413067cbdcddcb6141a" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f357fcec90b3caef6623a099691be676d033b40a058ac95d2a6ade6fa0c943" dependencies = [ "proc-macro2", "quote", "syn 2.0.90", ] [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "toml_datetime", "winnow", ] [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[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.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[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.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[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.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[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.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] [[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", ] [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn 2.0.90", ] pinger-2.0.0/Cargo.toml0000644000000025140000000000100103170ustar # 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 = "2.0.0" authors = ["Tom Forbes "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false 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/" [lib] name = "pinger" path = "src/lib.rs" [[example]] name = "simple-ping" path = "examples/simple-ping.rs" [dependencies.lazy-regex] version = "3.3.0" [dependencies.rand] version = "0.8.5" optional = true [dependencies.thiserror] version = "2.0.8" [dev-dependencies.anyhow] version = "1.0.94" [dev-dependencies.ntest] version = "0.9.3" [dev-dependencies.os_info] version = "3.9.0" [features] default = [] fake-ping = ["rand"] [target."cfg(windows)".dependencies.winping] version = "0.10.1" pinger-2.0.0/Cargo.toml.orig000064400000000000000000000010331046102023000137730ustar 00000000000000[package] name = "pinger" version = "2.0.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] thiserror = "2.0.8" lazy-regex = "3.3.0" rand = { version = "0.8.5", optional = true } [target.'cfg(windows)'.dependencies] winping = "0.10.1" [dev-dependencies] os_info = "3.9.0" ntest = "0.9.3" anyhow = "1.0.94" [features] default = [] fake-ping = ["rand"] pinger-2.0.0/README.md000064400000000000000000000017531046102023000123740ustar 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 std::time::Duration; use pinger::{ping, PingOptions}; fn ping_google() { let options = PingOptions::new("google.com", Duration::from_secs(1), None); let stream = ping(options).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-2.0.0/examples/simple-ping.rs000064400000000000000000000015341046102023000155220ustar 00000000000000use pinger::{ping, PingOptions}; const LIMIT: usize = 3; pub fn main() { let target = "tomforb.es".to_string(); let interval = std::time::Duration::from_millis(500); let options = PingOptions::new(target, interval, None); let stream = ping(options).expect("Error pinging"); for message in stream.into_iter().take(LIMIT) { 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) => { panic!("Ping exited! Code: {:?}. Stderr: {:?}", code, stderr) } } } } pinger-2.0.0/src/bsd.rs000064400000000000000000000024251046102023000130170ustar 00000000000000use crate::{extract_regex, PingCreationError, PingOptions, PingResult, Pinger}; use lazy_regex::*; pub static RE: Lazy = lazy_regex!(r"time=(?:(?P[0-9]+).(?P[0-9]+)\s+ms)"); pub struct BSDPinger { options: PingOptions, } pub(crate) fn parse_bsd(line: String) -> Option { if line.starts_with("PING ") { return None; } if line.starts_with("Request timeout") { return Some(PingResult::Timeout(line)); } extract_regex(&RE, line) } impl Pinger for BSDPinger { fn from_options(options: PingOptions) -> Result where Self: Sized, { Ok(Self { options }) } fn parse_fn(&self) -> fn(String) -> Option { parse_bsd } fn ping_args(&self) -> (&str, Vec) { let mut args = vec![format!( "-i{:.1}", self.options.interval.as_millis() as f32 / 1_000_f32 )]; if let Some(interface) = &self.options.interface { args.push("-I".into()); args.push(interface.clone()); } if let Some(raw_args) = &self.options.raw_arguments { args.extend(raw_args.iter().cloned()); } args.push(self.options.target.to_string()); ("ping", args) } } pinger-2.0.0/src/fake.rs000064400000000000000000000025371046102023000131610ustar 00000000000000use crate::{PingCreationError, PingOptions, PingResult, Pinger}; use rand::prelude::*; use std::sync::mpsc; use std::sync::mpsc::Receiver; use std::thread; use std::time::Duration; pub struct FakePinger { options: PingOptions, } impl Pinger for FakePinger { fn from_options(options: PingOptions) -> Result where Self: Sized, { Ok(Self { options }) } fn parse_fn(&self) -> fn(String) -> Option { unimplemented!("parse for FakeParser not implemented") } fn ping_args(&self) -> (&str, Vec) { unimplemented!("ping_args not implemented for FakePinger") } fn start(&self) -> Result, PingCreationError> { let (tx, rx) = mpsc::channel(); let sleep_time = self.options.interval; thread::spawn(move || { let mut random = 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) } } pinger-2.0.0/src/lib.rs000064400000000000000000000161151046102023000130160ustar 00000000000000#[cfg(unix)] use crate::linux::LinuxPinger; /// Pinger /// This crate exposes a simple function to ping remote hosts across different operating systems. /// Example: /// ```no_run /// use std::time::Duration; /// use pinger::{ping, PingResult, PingOptions}; /// let options = PingOptions::new("tomforb.es".to_string(), Duration::from_secs(1), None); /// let stream = ping(options).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 lazy_regex::Regex; use std::ffi::OsStr; use std::fmt::{Debug, Formatter}; use std::io::{BufRead, BufReader}; use std::process::{Child, Command, ExitStatus, Stdio}; use std::sync::{mpsc, Arc}; use std::time::Duration; use std::{fmt, io, thread}; use target::Target; use thiserror::Error; pub mod linux; pub mod macos; #[cfg(windows)] pub mod windows; mod bsd; #[cfg(feature = "fake-ping")] mod fake; mod target; #[cfg(test)] mod test; #[derive(Debug, Clone)] pub struct PingOptions { pub target: Target, pub interval: Duration, pub interface: Option, pub raw_arguments: Option>, } impl PingOptions { pub fn with_raw_arguments(mut self, raw_arguments: Vec) -> Self { self.raw_arguments = Some( raw_arguments .into_iter() .map(|item| item.to_string()) .collect(), ); self } } impl PingOptions { pub fn from_target(target: Target, interval: Duration, interface: Option) -> Self { Self { target, interval, interface, raw_arguments: None, } } pub fn new(target: impl ToString, interval: Duration, interface: Option) -> Self { Self::from_target(Target::new_any(target), interval, interface) } pub fn new_ipv4(target: impl ToString, interval: Duration, interface: Option) -> Self { Self::from_target(Target::new_ipv4(target), interval, interface) } pub fn new_ipv6(target: impl ToString, interval: Duration, interface: Option) -> Self { Self::from_target(Target::new_ipv6(target), interval, interface) } } pub fn run_ping( cmd: impl AsRef + Debug, args: Vec + Debug>, ) -> Result { Ok(Command::new(cmd.as_ref()) .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()?) } pub(crate) fn extract_regex(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)) } pub trait Pinger: Send + Sync { fn from_options(options: PingOptions) -> std::result::Result where Self: Sized; fn parse_fn(&self) -> fn(String) -> Option; fn ping_args(&self) -> (&str, Vec); fn start(&self) -> Result, PingCreationError> { let (tx, rx) = mpsc::channel(); let (cmd, args) = self.ping_args(); let mut child = run_ping(cmd, args)?; let stdout = child.stdout.take().expect("child did not have a stdout"); let parse_fn = self.parse_fn(); thread::spawn(move || { let reader = BufReader::new(stdout).lines(); for line in reader { match line { Ok(msg) => { if let Some(result) = parse_fn(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) } } #[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 PingCreationError { #[error("Could not detect ping. Stderr: {stderr:?}\nStdout: {stdout:?}")] UnknownPing { stderr: Vec, stdout: Vec, }, #[error("Error spawning ping: {0}")] SpawnError(#[from] io::Error), #[error("Installed ping is not supported: {alternative}")] NotSupported { alternative: String }, #[error("Invalid or unresolvable hostname {0}")] HostnameError(String), } pub fn get_pinger(options: PingOptions) -> std::result::Result, PingCreationError> { #[cfg(feature = "fake-ping")] if std::env::var("PINGER_FAKE_PING") .map(|e| e == "1") .unwrap_or_default() { return Ok(Arc::new(fake::FakePinger::from_options(options)?)); } #[cfg(windows)] { return Ok(Arc::new(windows::WindowsPinger::from_options(options)?)); } #[cfg(unix)] { if cfg!(target_os = "freebsd") || cfg!(target_os = "dragonfly") || cfg!(target_os = "openbsd") || cfg!(target_os = "netbsd") { Ok(Arc::new(bsd::BSDPinger::from_options(options)?)) } else if cfg!(target_os = "macos") { Ok(Arc::new(macos::MacOSPinger::from_options(options)?)) } else { Ok(Arc::new(LinuxPinger::from_options(options)?)) } } } /// Start pinging a an address. The address can be either a hostname or an IP address. pub fn ping( options: PingOptions, ) -> std::result::Result, PingCreationError> { let pinger = get_pinger(options)?; pinger.start() } pinger-2.0.0/src/linux.rs000064400000000000000000000107601046102023000134070ustar 00000000000000use crate::{extract_regex, run_ping, PingCreationError, PingOptions, PingResult, Pinger}; use lazy_regex::*; pub static UBUNTU_RE: Lazy = lazy_regex!(r"(?i-u)time=(?P\d+)(?:\.(?P\d+))? *ms"); #[derive(Debug)] pub enum LinuxPinger { // Alpine BusyBox(PingOptions), // Debian, Ubuntu, etc IPTools(PingOptions), } impl LinuxPinger { pub fn detect_platform_ping(options: PingOptions) -> Result { let child = run_ping("ping", vec!["-V".to_string()])?; let output = child.wait_with_output()?; let stdout = String::from_utf8(output.stdout).expect("Error decoding ping stdout"); let stderr = String::from_utf8(output.stderr).expect("Error decoding ping stderr"); if stderr.contains("BusyBox") { Ok(LinuxPinger::BusyBox(options)) } else if stdout.contains("iputils") { Ok(LinuxPinger::IPTools(options)) } else if stdout.contains("inetutils") { Err(PingCreationError::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(PingCreationError::UnknownPing { stdout: first_two_lines_stout, stderr: first_two_lines_stderr, }) } } } impl Pinger for LinuxPinger { fn from_options(options: PingOptions) -> Result where Self: Sized, { Self::detect_platform_ping(options) } fn parse_fn(&self) -> fn(String) -> Option { |line| { #[cfg(test)] eprintln!("Got line {line}"); if line.starts_with("64 bytes from") { return extract_regex(&UBUNTU_RE, line); } else if line.starts_with("no answer yet") { return Some(PingResult::Timeout(line)); } None } } fn ping_args(&self) -> (&str, Vec) { match self { // Alpine doesn't support timeout notifications, so we don't add the -O flag here. LinuxPinger::BusyBox(options) => { let cmd = if options.target.is_ipv6() { "ping6" } else { "ping" }; let mut args = vec![ options.target.to_string(), format!("-i{:.1}", options.interval.as_millis() as f32 / 1_000_f32), ]; if let Some(raw_args) = &options.raw_arguments { args.extend(raw_args.iter().cloned()); } (cmd, args) } LinuxPinger::IPTools(options) => { let cmd = if options.target.is_ipv6() { "ping6" } else { "ping" }; // 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}", options.interval.as_millis() as f32 / 1_000_f32), ]; if let Some(interface) = &options.interface { args.push("-I".into()); args.push(interface.clone()); } if let Some(raw_args) = &options.raw_arguments { args.extend(raw_args.iter().cloned()); } args.push(options.target.to_string()); (cmd, args) } } } } #[cfg(test)] mod tests { #[test] #[cfg(target_os = "linux")] fn test_linux_detection() { use super::*; use os_info::Type; use std::time::Duration; let platform = LinuxPinger::detect_platform_ping(PingOptions::new( "foo.com".to_string(), Duration::from_secs(1), None, )) .unwrap(); match os_info::get().os_type() { Type::Alpine => { assert!(matches!(platform, LinuxPinger::BusyBox(_))) } Type::Ubuntu => { assert!(matches!(platform, LinuxPinger::IPTools(_))) } _ => {} } } } pinger-2.0.0/src/macos.rs000064400000000000000000000022711046102023000133500ustar 00000000000000use crate::bsd::parse_bsd; use crate::{PingCreationError, PingOptions, PingResult, Pinger}; use lazy_regex::*; pub static RE: Lazy = lazy_regex!(r"time=(?:(?P[0-9]+).(?P[0-9]+)\s+ms)"); pub struct MacOSPinger { options: PingOptions, } impl Pinger for MacOSPinger { fn from_options(options: PingOptions) -> Result where Self: Sized, { Ok(Self { options }) } fn parse_fn(&self) -> fn(String) -> Option { parse_bsd } fn ping_args(&self) -> (&str, Vec) { let cmd = if self.options.target.is_ipv6() { "ping6" } else { "ping" }; let mut args = vec![ format!( "-i{:.1}", self.options.interval.as_millis() as f32 / 1_000_f32 ), self.options.target.to_string(), ]; if let Some(interface) = &self.options.interface { args.push("-b".into()); args.push(interface.clone()); } if let Some(raw_args) = &self.options.raw_arguments { args.extend(raw_args.iter().cloned()); } (cmd, args) } } pinger-2.0.0/src/target.rs000064400000000000000000000032151046102023000135330ustar 00000000000000use std::fmt; use std::fmt::{Display, Formatter}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum IPVersion { V4, V6, Any, } #[derive(Debug, Clone)] pub enum Target { IP(IpAddr), Hostname { domain: String, version: IPVersion }, } impl Target { pub fn is_ipv6(&self) -> bool { match self { Target::IP(ip) => ip.is_ipv6(), Target::Hostname { version, .. } => *version == IPVersion::V6, } } pub fn new_any(value: impl ToString) -> Self { let value = value.to_string(); if let Ok(ip) = value.parse::() { return Self::IP(ip); } Self::Hostname { domain: value, version: IPVersion::Any, } } pub fn new_ipv4(value: impl ToString) -> Self { let value = value.to_string(); if let Ok(ip) = value.parse::() { return Self::IP(IpAddr::V4(ip)); } Self::Hostname { domain: value.to_string(), version: IPVersion::V4, } } pub fn new_ipv6(value: impl ToString) -> Self { let value = value.to_string(); if let Ok(ip) = value.parse::() { return Self::IP(IpAddr::V6(ip)); } Self::Hostname { domain: value.to_string(), version: IPVersion::V6, } } } impl Display for Target { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { Target::IP(v) => Display::fmt(&v, f), Target::Hostname { domain, .. } => Display::fmt(&domain, f), } } } pinger-2.0.0/src/test.rs000064400000000000000000000111771046102023000132320ustar 00000000000000#[cfg(test)] mod tests { use crate::bsd::BSDPinger; use crate::linux::LinuxPinger; use crate::macos::MacOSPinger; #[cfg(windows)] use crate::windows::WindowsPinger; use crate::{PingOptions, PingResult, Pinger}; use anyhow::bail; use ntest::timeout; use std::time::Duration; const IS_GHA: bool = option_env!("GITHUB_ACTIONS").is_some(); #[test] #[timeout(20_000)] fn test_integration_any() { run_integration_test(PingOptions::new( "tomforb.es", Duration::from_millis(500), None, )) .unwrap(); } #[test] #[timeout(20_000)] fn test_integration_ipv4() { run_integration_test(PingOptions::new_ipv4( "tomforb.es", Duration::from_millis(500), None, )) .unwrap(); } #[test] #[timeout(20_000)] fn test_integration_ip6() { let res = run_integration_test(PingOptions::new_ipv6( "tomforb.es", Duration::from_millis(500), None, )); // ipv6 tests are allowed to fail on Gitlab CI, as it doesn't support ipv6, apparently. if !IS_GHA { res.unwrap(); } } fn run_integration_test(options: PingOptions) -> anyhow::Result<()> { let stream = crate::ping(options.clone())?; let mut success = 0; let mut errors = 0; for message in stream.into_iter().take(3) { match message { PingResult::Pong(_, m) | PingResult::Timeout(m) => { eprintln!("Message: {}", m); success += 1; } PingResult::Unknown(line) => { eprintln!("Unknown line: {}", line); errors += 1; } PingResult::PingExited(code, stderr) => { bail!("Ping exited with code: {}, stderr: {}", code, stderr); } } } assert_eq!(success, 3, "Success != 3 with opts {options:?}"); assert_eq!(errors, 0, "Errors != 0 with opts {options:?}"); Ok(()) } fn opts() -> PingOptions { PingOptions::new("foo".to_string(), Duration::from_secs(1), None) } fn test_parser(contents: &str) { let pinger = T::from_options(opts()).unwrap(); run_parser_test(contents, &pinger); } fn run_parser_test(contents: &str, pinger: &impl Pinger) { let parser = pinger.parse_fn(); 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(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() { run_parser_test( include_str!("tests/ubuntu.txt"), &LinuxPinger::IPTools(opts()), ); } #[test] fn debian() { run_parser_test( include_str!("tests/debian.txt"), &LinuxPinger::IPTools(opts()), ); } #[cfg(windows)] #[test] fn windows() { test_parser::(include_str!("tests/windows.txt")); } #[test] fn android() { run_parser_test( include_str!("tests/android.txt"), &LinuxPinger::BusyBox(opts()), ); } #[test] fn alpine() { run_parser_test( include_str!("tests/alpine.txt"), &LinuxPinger::BusyBox(opts()), ); } } pinger-2.0.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-2.0.0/src/tests/android.txt000064400000000000000000000014221046102023000152200ustar 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 None pinger-2.0.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-2.0.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-2.0.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-2.0.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-2.0.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-2.0.0/src/windows.rs000064400000000000000000000061541046102023000137440ustar 00000000000000use crate::target::{IPVersion, Target}; use crate::PingCreationError; use crate::{extract_regex, PingOptions, PingResult, Pinger}; use lazy_regex::*; use std::net::{IpAddr, ToSocketAddrs}; 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 { options: PingOptions, } impl Pinger for WindowsPinger { fn from_options(options: PingOptions) -> Result { Ok(Self { options }) } fn parse_fn(&self) -> fn(String) -> Option { |line| { if line.contains("timed out") || line.contains("failure") { return Some(PingResult::Timeout(line)); } extract_regex(&RE, line) } } fn ping_args(&self) -> (&str, Vec) { unimplemented!("ping_args for WindowsPinger is not implemented") } fn start(&self) -> Result, PingCreationError> { let interval = self.options.interval; let parsed_ip = match &self.options.target { Target::IP(ip) => ip.clone(), Target::Hostname { domain, version } => { let ips = (domain.as_str(), 0).to_socket_addrs()?; let selected_ips: Vec<_> = if *version == IPVersion::Any { ips.collect() } else { ips.into_iter() .filter(|addr| { if *version == IPVersion::V6 { matches!(addr.ip(), IpAddr::V6(_)) } else { matches!(addr.ip(), IpAddr::V4(_)) } }) .collect() }; if selected_ips.is_empty() { return Err(PingCreationError::HostnameError(domain.clone()).into()); } selected_ips[0].ip() } }; 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) } }