// TODO(performance): implement ping message channel buffering // // Pingo defines a number of high level functions and data structures to // interact with and represent a Ping via Go. Primarily, it defines the // Ping() function and Address data structure. // // NOTE this project does not provide a Go implementation of the ping protocol. // Instead, this project merely provides a wrapper to your system's existing // ping.c binary. // // NOTE(os): this project may fail with non-unix compliant system ping binaries, // predominantly Windows platforms. This relates to the way in which the project // interacts with the ping binary. (read: the project uses `ping -c1 ADDRESS` // and will break on systems that implement a different CLI) // // The project defines a simple top level binding for the system's ping.c: // Ping(). It simply reports a delay time as reported by the system ping, and // any errors encountered. // // Additionally, the project defines the "Address" data structure to represent // a series of ping results relating to the given host. Its fields are simple: // // Address (string): the host or IP address // // Results ([]float64): a series of ping results // // MaxResults (int): The maximum number of results to track, as enforced by // Address.Truncate() // // The Address data structure provides a series of higher level access functions // to prevent code repetition: // // Example: // ... // a := Address{Address:"google.ca", MaxResults:20} // latency, err := a.Ping() // get a single result and return the value // ... // handle error // a.Results, err = a.Poll() // append a ping to the Results slice, then return the updated slice // ... // handle error // a.Results = a.Truncate() // return a modified slice of only the most n*a.MaxResults long // // For more, see each function's GoDoc. package pingo import ( "errors" "os/exec" "strconv" "strings" ) // runPing returns the byte array as returned by [exec.Command] // // NOTE(os): this function may fail on non-unix compliant implementations of the Ping spec. func runPing(address string) (delay *[]byte, err error) { out, err := exec.Command("ping", address, "-c 1").Output() if err != nil { return nil, err } return &out, err } // splitBytesToLines splits bytes as returned by runPing into an // array of strings by newline func splitBytesToLines(bytes *[]byte) (lines []string) { return strings.Split(strings.ReplaceAll(string(*bytes), "\r\n", "\n"), "\n") } // Ping returns the delay of a single Ping as reported by the system Ping binary. // // If the function is unable to resolve the system binary output or fails to // successfully resolve a Ping, it will always return -1. // // NOTE(os): this function may fail on non-unix compliant implementations of the Ping spec. func Ping(address string) (delay float64, err error) { out, err := runPing(address) if err != nil { return -1, err } lines := splitBytesToLines(out) for i := range lines { if strings.Contains(lines[i], "bytes from") { position := strings.Index(lines[i], "time=") before, _, success := strings.Cut(lines[i][position:], " ") if !success { return -1, errors.New("line does not match pattern") } _, after, success := strings.Cut(before, "=") if !success { return -1, errors.New("line does not match pattern") } delay, _ := strconv.ParseFloat(after, 64) return delay, nil } } return -1, errors.New("could not resolve host: " + address) } type Address struct { Address string Results []float64 MaxResults int } // Enforces a Address.Result maximum length by dropping the oldest result, // then returns the modified slice. func (a Address) Truncate() []float64 { if len(a.Results) > a.MaxResults { a.Results = a.Results[len(a.Results)-a.MaxResults : len(a.Results)] // return modified slice missing first index } return a.Results } // Wraps Ping, passing Address.Address as its argument. func (a Address) Ping() (delay float64, err error) { return Ping(a.Address) } // Poll the affiliated Address, append it to a.Results, and report the modified // slice and any errors. func (a Address) Poll() (results []float64, err error) { delay, err := a.Ping() a.Results = append(a.Results, delay) a.Results = a.Truncate() // enforce max length return a.Results, err } // Last returns the last result in Address.Results. Returns -1 if no previous result func (a Address) Last() (delay float64) { if len(a.Results) > 0 { return a.Results[len(a.Results)-1] } else { return -1 } }