1
0

ping.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. // Pingo defines a number of high level functions and data structures to
  2. // interact with and represent a Ping via Go. Primarily, it defines the
  3. // Ping() function and Address data structure.
  4. //
  5. // NOTE this project does not provide a Go implementation of the ping protocol.
  6. // Instead, this project merely provides a wrapper to your system's existing
  7. // ping.c binary.
  8. //
  9. // NOTE(os): this project may fail with non-unix compliant system ping binaries,
  10. // predominantly Windows platforms. This relates to the way in which the project
  11. // interacts with the ping binary. (read: the project uses `ping -c1 ADDRESS`
  12. // and will break on systems that implement a different CLI)
  13. //
  14. // The project defines a simple top level binding for the system's ping.c:
  15. // Ping(). It simply reports a delay time as reported by the system ping, and
  16. // any errors encountered.
  17. //
  18. // Additionally, the project defines the "Address" data structure to represent
  19. // a series of ping results relating to the given host. Its fields are simple:
  20. //
  21. // Address (string): the host or IP address
  22. //
  23. // Results ([]float64): a series of ping results
  24. //
  25. // MaxResults (int): The maximum number of results to track, as enforced by
  26. // Address.Truncate()
  27. //
  28. // The Address data structure provides a series of higher level access functions
  29. // to prevent code repetition:
  30. //
  31. // Example:
  32. // ...
  33. // a := Address{Address:"google.ca", MaxResults:20}
  34. // latency, err := a.Ping() // get a single result and return the value
  35. // ...
  36. // // append a ping to the Results slice, then return the updated slice
  37. // a.Results, err = a.Poll()
  38. // ...
  39. // // return a modified slice of only the most n*a.MaxResults long
  40. // a.Results = a.Truncate()
  41. //
  42. // For more, see each function's GoDoc.
  43. //
  44. // TODO(performance): implement ping message channel buffering
  45. package pingo
  46. import (
  47. "errors"
  48. "os/exec"
  49. "strconv"
  50. "strings"
  51. )
  52. // runPing returns the byte array as returned by [exec.Command]
  53. //
  54. // NOTE(os): this function may fail on non-unix compliant implementations of the Ping spec.
  55. func runPing(address string) (delay *[]byte, err error) {
  56. out, err := exec.Command("ping", address, "-c 1").Output()
  57. if err != nil {
  58. return nil, err
  59. }
  60. return &out, err
  61. }
  62. // splitBytesToLines splits bytes as returned by runPing into an
  63. // array of strings by newline
  64. func splitBytesToLines(bytes *[]byte) (lines []string) {
  65. return strings.Split(strings.ReplaceAll(string(*bytes), "\r\n", "\n"), "\n")
  66. }
  67. // Ping returns the delay of a single Ping as reported by the system Ping binary.
  68. //
  69. // If the function is unable to resolve the system binary output or fails to
  70. // successfully resolve a Ping, it will always return -1.
  71. //
  72. // NOTE(os): this function may fail on non-unix compliant implementations of the Ping spec.
  73. func Ping(address string) (delay float64, err error) {
  74. out, err := runPing(address)
  75. if err != nil {
  76. return -1, err
  77. }
  78. lines := splitBytesToLines(out)
  79. for i := range lines {
  80. if strings.Contains(lines[i], "bytes from") {
  81. position := strings.Index(lines[i], "time=")
  82. before, _, success := strings.Cut(lines[i][position:], " ")
  83. if !success {
  84. return -1, errors.New("line does not match pattern")
  85. }
  86. _, after, success := strings.Cut(before, "=")
  87. if !success {
  88. return -1, errors.New("line does not match pattern")
  89. }
  90. delay, _ := strconv.ParseFloat(after, 64)
  91. return delay, nil
  92. }
  93. }
  94. return -1, errors.New("could not resolve host: " + address)
  95. }
  96. type Address struct {
  97. Address string
  98. Results []float64
  99. MaxResults int
  100. }
  101. // Enforces an Address.Result maximum length by dropping the oldest results,
  102. // then returns the modified slice.
  103. func (a Address) Truncate() []float64 {
  104. if len(a.Results) > a.MaxResults {
  105. a.Results = a.Results[len(a.Results)-a.MaxResults : len(a.Results)]
  106. }
  107. return a.Results // return modified slice less oldest results
  108. }
  109. // Wraps Ping, passing Address.Address as its argument.
  110. func (a Address) Ping() (delay float64, err error) {
  111. return Ping(a.Address)
  112. }
  113. // Poll the affiliated Address, append it to a.Results, and report the modified
  114. // slice and any errors.
  115. func (a Address) Poll() (results []float64, err error) {
  116. delay, err := a.Ping()
  117. a.Results = append(a.Results, delay)
  118. a.Results = a.Truncate() // enforce max length
  119. return a.Results, err
  120. }
  121. // Last returns the last result in Address.Results. Returns -1 if no previous result
  122. func (a Address) Last() (delay float64) {
  123. if len(a.Results) > 0 {
  124. return a.Results[len(a.Results)-1]
  125. } else {
  126. return -1
  127. }
  128. }