From b9edbf8cf79d870b7ee822f666a9f45ae6f72f4c Mon Sep 17 00:00:00 2001 From: Tom Forbes Date: Sun, 15 Dec 2024 16:52:00 +0000 Subject: [PATCH] Rewrite the pinger library --- Cargo.lock | 40 +++++++++ gping/src/main.rs | 5 +- pinger/Cargo.toml | 6 +- pinger/README.md | 6 +- pinger/examples/simple-ping.rs | 5 +- pinger/src/bsd.rs | 53 +++++------ pinger/src/fake.rs | 38 ++++---- pinger/src/lib.rs | 141 ++++++++++++++--------------- pinger/src/linux.rs | 157 ++++++++++++++++----------------- pinger/src/macos.rs | 49 ++++------ pinger/src/test.rs | 58 +++++++----- pinger/src/windows.rs | 43 ++++----- 12 files changed, 318 insertions(+), 283 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6cff39dd3..99d063996 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -310,6 +310,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive-new" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "diff" version = "0.1.13" @@ -406,6 +417,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -651,11 +671,13 @@ name = "pinger" version = "1.3.0" dependencies = [ "anyhow", + "derive-new", "dns-lookup", "lazy-regex", "os_info", "rand", "thiserror", + "which", "winping", ] @@ -1110,6 +1132,18 @@ version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +[[package]] +name = "which" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b" +dependencies = [ + "either", + "home", + "rustix", + "winsafe", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1310,6 +1344,12 @@ dependencies = [ "winapi_forked_icmpapi", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "yansi" version = "1.0.1" diff --git a/gping/src/main.rs b/gping/src/main.rs index 69c9df2d7..9f88927ac 100644 --- a/gping/src/main.rs +++ b/gping/src/main.rs @@ -11,7 +11,7 @@ use crossterm::{ }; use dns_lookup::lookup_host; use itertools::{Itertools, MinMaxResult}; -use pinger::{ping_with_interval, PingResult}; +use pinger::{ping, PingOptions, PingResult}; use std::io; use std::io::BufWriter; use std::iter; @@ -310,7 +310,8 @@ fn start_ping_thread( ) -> Result>> { let interval = Duration::from_millis((watch_interval.unwrap_or(0.2) * 1000.0) as u64); // Pump ping messages into the queue - let stream = ping_with_interval(host, interval, interface)?; + let ping_opts = PingOptions::new(host, interval, interface); + let stream = ping(ping_opts)?; Ok(thread::spawn(move || -> Result<()> { while !kill_event.load(Ordering::Acquire) { match stream.recv() { diff --git a/pinger/Cargo.toml b/pinger/Cargo.toml index 6383fd203..b4bdceb36 100644 --- a/pinger/Cargo.toml +++ b/pinger/Cargo.toml @@ -9,9 +9,11 @@ repository = "https://github.com/orf/pinger/" [dependencies] anyhow = "1.0.94" -thiserror = "2.0.6" +thiserror = "2.0.7" rand = "0.8.5" -lazy-regex = "3.1.0" +lazy-regex = "3.3.0" +derive-new = "0.7.0" +which = "7.0.0" [target.'cfg(windows)'.dependencies] winping = "0.10.1" diff --git a/pinger/README.md b/pinger/README.md index ae1c96d3d..3e4bc8601 100644 --- a/pinger/README.md +++ b/pinger/README.md @@ -14,10 +14,12 @@ A full example of using the library can be found in the `examples/` directory, b interface is quite simple: ```rust -use pinger::ping; +use std::time::Duration; +use pinger::{ping, PingOptions}; fn ping_google() { - let stream = ping("google.com", None).expect("Error pinging"); + 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, _) => { diff --git a/pinger/examples/simple-ping.rs b/pinger/examples/simple-ping.rs index 9afe06495..a266316ab 100644 --- a/pinger/examples/simple-ping.rs +++ b/pinger/examples/simple-ping.rs @@ -1,9 +1,10 @@ -use pinger::ping_with_interval; +use pinger::{ping, PingOptions}; 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"); + let options = PingOptions::new(target, interval, None); + let stream = ping(options).expect("Error pinging"); for message in stream { match message { pinger::PingResult::Pong(duration, line) => { diff --git a/pinger/src/bsd.rs b/pinger/src/bsd.rs index 8e6ecc907..c0c033e14 100644 --- a/pinger/src/bsd.rs +++ b/pinger/src/bsd.rs @@ -1,49 +1,44 @@ -use crate::{Parser, PingResult, Pinger}; +use crate::{extract_regex, PingDetectionError, PingOptions, 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, + 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 { - type Parser = BSDParser; + fn from_options(options: PingOptions) -> Result + where + Self: Sized, + { + Ok(Self { options }) + } - fn new(interval: Duration, interface: Option) -> Self { - Self { - interface, - interval, - } + fn parse_fn(&self) -> fn(String) -> Option { + parse_bsd } - fn ping_args(&self, target: String) -> (&str, Vec) { + fn ping_args(&self) -> (&str, Vec) { let mut args = vec![format!( "-i{:.1}", - self.interval.as_millis() as f32 / 1_000_f32 + self.options.interval.as_millis() as f32 / 1_000_f32 )]; - if let Some(interface) = &self.interface { + if let Some(interface) = &self.options.interface { args.push("-I".into()); args.push(interface.clone()); } - args.push(target); + args.push(self.options.target.clone()); ("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) - } -} diff --git a/pinger/src/fake.rs b/pinger/src/fake.rs index daa281644..48b9a7aac 100644 --- a/pinger/src/fake.rs +++ b/pinger/src/fake.rs @@ -1,4 +1,4 @@ -use crate::{Parser, PingResult, Pinger}; +use crate::{PingDetectionError, PingOptions, PingResult, Pinger}; use rand::prelude::*; use std::sync::mpsc; use std::sync::mpsc::Receiver; @@ -6,22 +6,31 @@ use std::thread; use std::time::Duration; pub struct FakePinger { - interval: Duration, + options: PingOptions, } impl Pinger for FakePinger { - type Parser = FakeParser; + fn from_options(options: PingOptions) -> Result + where + Self: Sized, + { + Ok(Self { options }) + } - fn new(interval: Duration, _interface: Option) -> Self { - Self { interval } + fn parse_fn(&self) -> fn(String) -> Option { + unimplemented!("parse for FakeParser not implemented") } - fn start(&self, _target: String) -> anyhow::Result> { + fn ping_args(&self) -> (&str, Vec) { + unimplemented!("ping_args not implemented for FakePinger") + } + + fn start(&self) -> anyhow::Result> { let (tx, rx) = mpsc::channel(); - let sleep_time = self.interval; + let sleep_time = self.options.interval; thread::spawn(move || { - let mut random = rand::thread_rng(); + let mut random = thread_rng(); loop { let fake_seconds = random.gen_range(50..150); let ping_result = PingResult::Pong( @@ -38,17 +47,4 @@ impl Pinger for FakePinger { 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") - } } diff --git a/pinger/src/lib.rs b/pinger/src/lib.rs index 9f863d94e..7576343fe 100644 --- a/pinger/src/lib.rs +++ b/pinger/src/lib.rs @@ -1,12 +1,13 @@ #[cfg(unix)] -use crate::linux::{detect_linux_ping, LinuxPingType}; +use crate::linux::LinuxPinger; /// 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"); +/// 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), @@ -18,10 +19,11 @@ use crate::linux::{detect_linux_ping, LinuxPingType}; /// ``` use anyhow::{Context, Result}; use lazy_regex::Regex; -use std::fmt::Formatter; +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; +use std::sync::{mpsc, Arc}; use std::time::Duration; use std::{fmt, thread}; use thiserror::Error; @@ -37,8 +39,18 @@ mod fake; #[cfg(test)] mod test; -pub fn run_ping(cmd: &str, args: Vec) -> Result { - Command::new(cmd) +#[derive(Debug, Clone, derive_new::new)] +pub struct PingOptions { + pub target: String, + pub interval: Duration, + pub interface: Option, +} + +pub fn run_ping( + cmd: impl AsRef + Debug, + args: Vec + Debug>, +) -> Result { + Command::new(cmd.as_ref()) .args(&args) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -50,24 +62,50 @@ pub fn run_ping(cmd: &str, args: Vec) -> Result { .with_context(|| format!("Failed to run ping with args {:?}", &args)) } -pub trait Pinger { - type Parser: Parser; +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 new(interval: Duration, interface: Option) -> Self; + fn ping_args(&self) -> (&str, Vec); - fn start(&self, target: String) -> Result> { + fn start(&self) -> Result> { let (tx, rx) = mpsc::channel(); - let (cmd, args) = self.ping_args(target); + let (cmd, args) = self.ping_args(); let mut child = run_ping(cmd, args)?; let stdout = child.stdout.take().context("child did not have a stdout")?; + let parse_fn = self.parse_fn(); + 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 let Some(result) = parse_fn(msg) { if tx.send(result).is_err() { break; } @@ -83,35 +121,6 @@ pub trait Pinger { 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)] @@ -155,29 +164,19 @@ pub enum PingError { 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> { +pub fn get_pinger( + options: PingOptions, +) -> std::result::Result, PingDetectionError> { 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); + return Ok(Arc::new(fake::FakePinger::from_options(options)?)); } #[cfg(windows)] { - let p = windows::WindowsPinger::new(interval, interface); - return p.start(addr); + return Ok(Arc::new(windows::WindowsPinger::from_options(options)?)); } #[cfg(unix)] { @@ -186,23 +185,17 @@ pub fn ping_with_interval( || cfg!(target_os = "openbsd") || cfg!(target_os = "netbsd") { - let p = bsd::BSDPinger::new(interval, interface); - p.start(addr) + Ok(Arc::new(bsd::BSDPinger::from_options(options)?)) } else if cfg!(target_os = "macos") { - let p = macos::MacOSPinger::new(interval, interface); - p.start(addr) + Ok(Arc::new(macos::MacOSPinger::from_options(options)?)) } 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))?, - } + 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) -> Result> { + let pinger = get_pinger(options)?; + pinger.start() +} diff --git a/pinger/src/linux.rs b/pinger/src/linux.rs index 952239f4d..63f727976 100644 --- a/pinger/src/linux.rs +++ b/pinger/src/linux.rs @@ -1,113 +1,110 @@ -use crate::{run_ping, Parser, PingDetectionError, PingResult, Pinger}; +use crate::{extract_regex, run_ping, PingDetectionError, PingOptions, PingResult, Pinger}; use anyhow::Context; use lazy_regex::*; -use std::time::Duration; -#[derive(Debug, Eq, PartialEq)] -pub enum LinuxPingType { - BusyBox, - IPTools, +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), } -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")?; +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() + .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, - }) + if stderr.contains("BusyBox") { + Ok(LinuxPinger::BusyBox(options)) + } else if stdout.contains("iputils") { + Ok(LinuxPinger::IPTools(options)) + } 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 from_options(options: PingOptions) -> Result + where + Self: Sized, + { + Self::detect_platform_ping(options) } - 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()); + fn parse_fn(&self) -> fn(String) -> Option { + |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 } - 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)); + 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) => ("ping", vec![options.target.to_string()]), + LinuxPinger::IPTools(options) => { + // 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()); + } + args.push(options.target.to_string()); + ("ping", args) + } } - None } } #[cfg(test)] mod tests { + #[test] #[cfg(target_os = "linux")] fn test_linux_detection() { use super::*; + use std::time::Duration; use os_info::Type; - let ping_type = detect_linux_ping().expect("Error getting ping"); + + 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_eq!(ping_type, LinuxPingType::BusyBox) + assert!(matches!(platform, LinuxPinger::BusyBox(_))) } Type::Ubuntu => { - assert_eq!(ping_type, LinuxPingType::IPTools) + assert!(matches!(platform, LinuxPinger::IPTools(_))) } _ => {} } diff --git a/pinger/src/macos.rs b/pinger/src/macos.rs index 9156b9009..ec9dbd734 100644 --- a/pinger/src/macos.rs +++ b/pinger/src/macos.rs @@ -1,35 +1,39 @@ -use crate::{Parser, PingResult, Pinger}; +use crate::bsd::parse_bsd; +use crate::{PingDetectionError, PingOptions, 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, + options: PingOptions, } impl Pinger for MacOSPinger { - type Parser = MacOSParser; + fn from_options(options: PingOptions) -> Result + where + Self: Sized, + { + Ok(Self { options }) + } - fn new(interval: Duration, interface: Option) -> Self { - Self { - interval, - interface, - } + fn parse_fn(&self) -> fn(String) -> Option { + parse_bsd } - fn ping_args(&self, target: String) -> (&str, Vec) { - let cmd = match target.parse::() { + fn ping_args(&self) -> (&str, Vec) { + let cmd = match self.options.target.parse::() { Ok(_) => "ping6", Err(_) => "ping", }; let mut args = vec![ - format!("-i{:.1}", self.interval.as_millis() as f32 / 1_000_f32), - target, + format!( + "-i{:.1}", + self.options.interval.as_millis() as f32 / 1_000_f32 + ), + self.options.target.clone(), ]; - if let Some(interface) = &self.interface { + if let Some(interface) = &self.options.interface { args.push("-b".into()); args.push(interface.clone()); } @@ -37,18 +41,3 @@ impl Pinger for MacOSPinger { (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) - } -} diff --git a/pinger/src/test.rs b/pinger/src/test.rs index c4be0acf6..52c8336c9 100644 --- a/pinger/src/test.rs +++ b/pinger/src/test.rs @@ -1,22 +1,28 @@ #[cfg(test)] mod tests { - use crate::bsd::BSDParser; - use crate::linux::LinuxParser; - use crate::macos::MacOSParser; - use crate::{Parser, PingResult}; - + use crate::bsd::BSDPinger; + use crate::linux::LinuxPinger; + use crate::macos::MacOSPinger; #[cfg(windows)] use crate::windows::WindowsParser; + use crate::{PingOptions, PingResult, Pinger}; + use std::time::Duration; + + 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 test_parser(contents: &str) - where - T: Parser, - { - let parser = T::default(); + 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.parse(l.to_string())).collect(); + let parsed: Vec> = input.map(|l| parser(l.to_string())).collect(); assert_eq!( parsed.len(), @@ -41,37 +47,43 @@ mod tests { #[test] fn macos() { - test_parser::(include_str!("tests/macos.txt")); + test_parser::(include_str!("tests/macos.txt")); } #[test] fn freebsd() { - test_parser::(include_str!("tests/bsd.txt")); + test_parser::(include_str!("tests/bsd.txt")); } #[test] fn dragonfly() { - test_parser::(include_str!("tests/bsd.txt")); + test_parser::(include_str!("tests/bsd.txt")); } #[test] fn openbsd() { - test_parser::(include_str!("tests/bsd.txt")); + test_parser::(include_str!("tests/bsd.txt")); } #[test] fn netbsd() { - test_parser::(include_str!("tests/bsd.txt")); + test_parser::(include_str!("tests/bsd.txt")); } #[test] fn ubuntu() { - test_parser::(include_str!("tests/ubuntu.txt")); + run_parser_test( + include_str!("tests/ubuntu.txt"), + &LinuxPinger::IPTools(opts()), + ); } #[test] fn debian() { - test_parser::(include_str!("tests/debian.txt")); + run_parser_test( + include_str!("tests/debian.txt"), + &LinuxPinger::IPTools(opts()), + ); } #[cfg(windows)] @@ -82,11 +94,17 @@ mod tests { #[test] fn android() { - test_parser::(include_str!("tests/android.txt")); + run_parser_test( + include_str!("tests/android.txt"), + &LinuxPinger::BusyBox(opts()), + ); } #[test] fn alpine() { - test_parser::(include_str!("tests/alpine.txt")); + run_parser_test( + include_str!("tests/alpine.txt"), + &LinuxPinger::BusyBox(opts()), + ); } } diff --git a/pinger/src/windows.rs b/pinger/src/windows.rs index 18cb48adb..7a442fbd5 100644 --- a/pinger/src/windows.rs +++ b/pinger/src/windows.rs @@ -1,4 +1,5 @@ -use crate::{Parser, PingError, PingResult, Pinger}; +use crate::PingDetectionError; +use crate::{extract_regex, PingError, PingOptions, PingResult, Pinger}; use anyhow::Result; use dns_lookup::lookup_host; use lazy_regex::*; @@ -11,23 +12,35 @@ 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, + options: PingOptions, } impl Pinger for WindowsPinger { - type Parser = WindowsParser; + 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 new(interval: Duration, _interface: Option) -> Self { - Self { interval } + fn ping_args(&self) -> (&str, Vec) { + unimplemented!("ping_args for WindowsPinger is not implemented") } - fn start(&self, target: String) -> Result> { - let interval = self.interval; - let parsed_ip: IpAddr = match target.parse() { + fn start(&self) -> Result> { + let interval = self.options.interval; + let target = &self.options.target; + let parsed_ip: IpAddr = match self.options.target.parse() { Err(_) => { let things = lookup_host(target.as_str())?; if things.is_empty() { - Err(PingError::HostnameError(target)) + Err(PingError::HostnameError(target.clone())) } else { Ok(things[0]) } @@ -67,15 +80,3 @@ impl Pinger for WindowsPinger { 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) - } -}