diff --git a/main.go b/main.go index 290c7e8..35ce285 100644 --- a/main.go +++ b/main.go @@ -18,9 +18,9 @@ import ( "github.com/gdamore/tcell/v2" ) -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // Flags -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- var ( ifaceA = flag.String("ifaceA", "gu0", "primary network interface") @@ -38,9 +38,9 @@ var ( "hosts for reachability checks") ) -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // Constants -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- const ( icmpTimeout = 500 * time.Millisecond tcpTimeout = 500 * time.Millisecond @@ -50,24 +50,33 @@ const ( screenRefresh = 500 * time.Millisecond ) -//----------------------------------------------------------------------------- -// Data -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// Data structures +// ----------------------------------------------------------------------------- type InterfaceStatus struct { - Name string - Label string - IPInfo string + Name string + Label string + IPInfo string + Reachable map[string]bool Loss map[string]float64 TCP map[string][]float64 + + TotalICMPReq int + TotalICMPRep int + + DroppedCount int + LastDrop time.Time + LastPing time.Time SpinFrame int - mu sync.RWMutex + + mu sync.RWMutex } -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // Math helpers (no heavy deps) -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- func minMaxAvgStd(nums []float64) (min, max, avg, std float64) { if len(nums) == 0 { return @@ -84,18 +93,18 @@ func minMaxAvgStd(nums []float64) (min, max, avg, std float64) { sum += v } avg = sum / float64(len(nums)) - var vSum float64 + var vsum float64 for _, v := range nums { - diff := v - avg - vSum += diff * diff + d := v - avg + vsum += d * d } - std = math.Sqrt(vSum / float64(len(nums))) + std = math.Sqrt(vsum / float64(len(nums))) return } -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // External look-ups -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- type ipInfo struct{ IP, Hostname, Org string } func fetchIPInfo(iface string) string { @@ -114,9 +123,9 @@ func fetchIPInfo(iface string) string { return fmt.Sprintf("%s [%s] %s", resp.IP, resp.Hostname, resp.Org) } -//----------------------------------------------------------------------------- -// Ping + loss -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// Ping helpers +// ----------------------------------------------------------------------------- func pingOnce(iface, host string) bool { ctx, cancel := context.WithTimeout(context.Background(), icmpTimeout) defer cancel() @@ -146,9 +155,9 @@ func lossPercent(iface, host string) float64 { return 1.0 } -//----------------------------------------------------------------------------- -// TCP timing -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// TCP helpers +// ----------------------------------------------------------------------------- func localAddr(iface string) (net.Addr, error) { ni, err := net.InterfaceByName(iface) if err != nil { @@ -178,18 +187,18 @@ func tcpDuration(iface, hostPort string) time.Duration { return time.Since(start) } -//----------------------------------------------------------------------------- -// Spinners -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// Spinner helpers +// ----------------------------------------------------------------------------- var spinner = []rune{'|', '/', '-', '\\'} func (st *InterfaceStatus) tickSpinner() { st.SpinFrame = (st.SpinFrame + 1) % len(spinner) } -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // Goroutines -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) { tick := time.NewTicker(time.Second) defer tick.Stop() @@ -205,13 +214,25 @@ func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) { wg.Add(1) go func(host string) { defer wg.Done() + st.mu.Lock() + st.TotalICMPReq++ + st.mu.Unlock() + ok := pingOnce(st.Name, host) + mu.Lock() res[host] = ok - if ok { - st.tickSpinner() - } mu.Unlock() + + st.mu.Lock() + if ok { + st.TotalICMPRep++ + st.tickSpinner() + } else { + st.DroppedCount++ + st.LastDrop = time.Now() + } + st.mu.Unlock() }(h) } wg.Wait() @@ -282,21 +303,22 @@ func tcpLoop(ctx context.Context, st *InterfaceStatus, hosts []string) { } } -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // UI helpers -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- func write(s tcell.Screen, x, y int, str string) { for i, r := range str { s.SetContent(x+i, y, r, nil, tcell.StyleDefault) } } +func divider(w int) string { return strings.Repeat("=", w) } + func drawDivider(s tcell.Screen, y, w int) { - line := strings.Repeat("=", w) - write(s, 0, y, line) + write(s, 0, y, divider(w)) } -func drawHeaderLine(s tcell.Screen, y, w int, spin rune, txt string) { +func drawHeader(s tcell.Screen, y, w int, spin rune, txt string) { line := fmt.Sprintf("== %c %s", spin, txt) if len(line) > w { line = line[:w] @@ -309,13 +331,14 @@ func drawInterface(s tcell.Screen, y, w int, st *InterfaceStatus) int { drawDivider(s, y, w) st.mu.RLock() spin := spinner[st.SpinFrame] - head := fmt.Sprintf("%s — %s", st.Label, st.IPInfo) + head := fmt.Sprintf("%s — %s", st.Label, st.IPInfo) st.mu.RUnlock() - drawHeaderLine(s, y+1, w, spin, head) + drawHeader(s, y+1, w, spin, head) drawDivider(s, y+2, w) - y += 4 // skip blank line after - // contents + y += 4 + st.mu.RLock() + // Reachability total := len(st.Reachable) rc := 0 var down []string @@ -336,6 +359,8 @@ func drawInterface(s tcell.Screen, y, w int, st *InterfaceStatus) int { write(s, 0, y, "Unreachable: none") } y += 2 + + // Packet loss write(s, 0, y, "Packet Loss:") y++ for _, h := range packetLossHosts { @@ -343,7 +368,16 @@ func drawInterface(s tcell.Screen, y, w int, st *InterfaceStatus) int { write(s, 0, y, fmt.Sprintf("%-16s %.0f%%", h+":", lp)) y++ } - y++ + dropAge := "N/A" + if !st.LastDrop.IsZero() { + dropAge = fmt.Sprintf("%s", time.Since(st.LastDrop).Round(time.Second)) + } + write(s, 0, y, fmt.Sprintf("Dropped: %d (last at %s, age %s)", + st.DroppedCount, + st.LastDrop.Format("15:04:05"), dropAge)) + y += 2 + + // TCP stats write(s, 0, y, "TCP Connect Stats:") y++ for _, hp := range tcpTestHosts { @@ -356,48 +390,66 @@ func drawInterface(s tcell.Screen, y, w int, st *InterfaceStatus) int { hp, mi, av, ma, sd)) y++ } + y++ + + // ICMP totals + write(s, 0, y, fmt.Sprintf("Total ICMP Requests: %d", st.TotalICMPReq)) + y++ + write(s, 0, y, fmt.Sprintf("Total ICMP Replies: %d", st.TotalICMPRep)) + y += 2 + st.mu.RUnlock() - return y + 2 // leave a blank line after section + return y } -//----------------------------------------------------------------------------- -// UI main loop -//----------------------------------------------------------------------------- -func uiLoop(ctx context.Context, scr tcell.Screen, a, b *InterfaceStatus) { +// ----------------------------------------------------------------------------- +// UI loop +// ----------------------------------------------------------------------------- +func uiLoop(ctx context.Context, scr tcell.Screen, a, b *InterfaceStatus, start time.Time) { defer scr.Fini() - ticker := time.NewTicker(screenRefresh) - defer ticker.Stop() + tick := time.NewTicker(screenRefresh) + defer tick.Stop() topSpin := 0 + for { select { case <-ctx.Done(): return - case <-ticker.C: + case <-tick.C: w, _ := scr.Size() scr.Clear() + // top banner drawDivider(scr, 0, w) - drawHeaderLine(scr, 1, w, spinner[topSpin%len(spinner)], + drawHeader(scr, 1, w, spinner[topSpin%len(spinner)], time.Now().Format(time.RFC1123Z)) drawDivider(scr, 2, w) topSpin++ - y := 4 + + write(scr, 0, 3, fmt.Sprintf("Runtime: %s", time.Since(start).Round(time.Second))) + + y := 5 y = drawInterface(scr, y, w, a) - y = drawInterface(scr, y, w, b) + _ = drawInterface(scr, y, w, b) + scr.Show() } } } -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // Main -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- var ( - reachHosts []string - packetLossHosts = []string{"datavi.be", "google.com", "fast.com", - "cloudflare.com", "github.com"} - tcpTestHosts = []string{"google.com:443", "github.com:443", - "1.1.1.1:443", "8.8.8.8:53"} + reachHosts []string + packetLossHosts = []string{ + "datavi.be", "google.com", "fast.com", + "cloudflare.com", "github.com", + } + tcpTestHosts = []string{ + "google.com:443", "github.com:443", + "1.1.1.1:443", "8.8.8.8:53", + } ) func main() { @@ -439,5 +491,5 @@ func main() { go tcpLoop(ctx, a, tcpTestHosts) go tcpLoop(ctx, b, tcpTestHosts) - uiLoop(ctx, scr, a, b) + uiLoop(ctx, scr, a, b, time.Now()) }