1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
use anyhow::Result;
use os_info::Type;
use regex::Regex;
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use thiserror::Error;

#[macro_use]
extern crate lazy_static;

pub mod linux;
pub mod macos;
#[cfg(test)]
mod test;
pub mod windows;

pub trait Pinger: Default {
    fn start<P>(&self, target: String) -> Result<mpsc::Receiver<PingResult>>
    where
        P: Parser,
    {
        let (tx, rx) = mpsc::channel();
        let args = self.ping_args(target);

        thread::spawn(move || {
            let mut child = Command::new("ping")
                .args(args)
                .stdout(Stdio::piped())
                .stderr(Stdio::null())
                .spawn()
                .expect("Failed to run ping");
            let parser = P::default();
            let stdout = child.stdout.take().expect("child did not have a stdout");
            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,
                }
            }
        });

        Ok(rx)
    }

    fn ping_args(&self, target: String) -> Vec<String> {
        return vec![target];
    }
}

// Default empty implementation of a pinger.
#[derive(Default)]
pub struct SimplePinger {}

impl Pinger for SimplePinger {}

pub trait Parser: Default {
    fn parse(&self, line: String) -> Option<PingResult>;

    fn extract_regex(&self, regex: &Regex, line: String) -> Option<PingResult> {
        let cap = regex.captures(&line)?;
        let time = cap
            .name("time")
            .expect("No capture group named 'time'")
            .as_str()
            .parse::<f32>()
            .expect("time cannot be parsed as f32");
        Some(PingResult::Pong(Duration::from_micros(
            (time * 100f32) as u64,
        )))
    }
}

#[derive(Debug)]
pub enum PingResult {
    Pong(Duration),
    Timeout,
}

#[derive(Error, Debug)]
pub enum PingError {
    #[error("Unsupported OS {0}")]
    UnsupportedOS(String),
}

pub fn ping(addr: String) -> Result<mpsc::Receiver<PingResult>> {
    let os_type = os_info::get().os_type();
    match os_type {
        Type::Windows => {
            let p = SimplePinger::default();
            p.start::<windows::WindowsParser>(addr)
        }
        Type::Linux | Type::Debian | Type::Ubuntu | Type::Alpine => {
            let p = linux::LinuxPinger::default();
            p.start::<linux::LinuxParser>(addr)
        }
        Type::Macos => {
            let p = macos::MacOSPinger::default();
            p.start::<macos::MacOSParser>(addr)
        }
        _ => Err(PingError::UnsupportedOS(os_type.to_string()).into()),
    }
}