From 0e1fc6a88dc4841881ccd51fac78386e85be8c81 Mon Sep 17 00:00:00 2001 From: sneak Date: Wed, 21 May 2025 21:34:06 -0700 Subject: [PATCH] latest --- main.go | 193 ++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 145 insertions(+), 48 deletions(-) diff --git a/main.go b/main.go index 351dac0..43cb14b 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "flag" "fmt" "math" + "math/rand" "net" "os" "os/exec" @@ -33,9 +34,9 @@ var ( labelB = flag.String("labelB", "Cox cable direct", "label for ifaceB") hostCSV = flag.String("hosts", - "1.1.1.1,8.8.8.8,8.8.4.4,google.com,github.com,"+ + "8.8.8.8,8.8.4.4,google.com,github.com,"+ "console.aws.amazon.com,console.cloud.google.com,"+ - "fast.com,cloudflare.com,datavi.be", + "fast.com,datavi.be,captive.apple.com", "comma-separated reachability hosts") logFile = flag.String("logfile", "/tmp/rtnetmon.log", "path to log file") @@ -51,12 +52,20 @@ const ( statsHistory = 300 screenRefresh = 500 * time.Millisecond // Still used as backup refresh rate - // Unicode characters for the meter - greenDot = '🟢' // Unicode green circle - redDot = '🔴' // Unicode red circle - emptyDot = '⚪' // Unicode white circle - meterWidth = 6 // Fixed width of the meter - maxMeterValue = 10 // Maximum value for the meter + // ASCII characters for the meter + meterStart = '[' + meterEnd = ']' + meterFill = '=' + meterEmpty = ' ' + meterWidth = 7 // Fixed width of the meter (including brackets) + meterFillWidth = 5 // Width of the fillable area (meterWidth - 2 for brackets) + maxMeterValue = 10 // Maximum value for the meter + + hostW = 30 // Increased width for host column + numW = 7 + stdW = 8 + nW = 6 + lostW = 7 // Width for the lost column ) /*────────────────── runtime struct ─────────────*/ @@ -70,7 +79,8 @@ type InterfaceStatus struct { DroppedCount int LastDrop, LastPing time.Time SpinFrame int - MeterValue int // Current value for the packet loss meter + MeterValue int // Current value for the packet loss meter + LostPackets map[string]int // Track lost packets per host mu sync.RWMutex } @@ -79,12 +89,42 @@ type InterfaceStatus struct { var ( cBrightGreen = tcell.StyleDefault.Foreground(tcell.ColorGreen).Bold(true) cGreen = tcell.StyleDefault.Foreground(tcell.ColorGreen) + cDimGreen = tcell.StyleDefault.Foreground(tcell.ColorGreen).Dim(true) cYellow = tcell.StyleDefault.Foreground(tcell.ColorYellow) + cOrange = tcell.StyleDefault.Foreground(tcell.ColorOrange) cRed = tcell.StyleDefault.Foreground(tcell.ColorRed) cBrightRed = tcell.StyleDefault.Foreground(tcell.ColorRed).Bold(true) cDefault = tcell.StyleDefault + + // Rainbow colors for the spinner + cRainbow = []tcell.Style{ + tcell.StyleDefault.Foreground(tcell.ColorRed), + tcell.StyleDefault.Foreground(tcell.ColorOrange), + tcell.StyleDefault.Foreground(tcell.ColorYellow), + tcell.StyleDefault.Foreground(tcell.ColorGreen), + tcell.StyleDefault.Foreground(tcell.ColorBlue), + tcell.StyleDefault.Foreground(tcell.ColorPurple), + } ) +// Get the appropriate meter style based on the value +func getMeterStyle(value int) tcell.Style { + switch { + case value <= 0: + return cBrightGreen + case value == 1: + return cGreen + case value == 2: + return cDimGreen + case value == 3: + return cYellow + case value == 4: + return cOrange + default: + return cRed + } +} + func styleLatency(ms float64) tcell.Style { switch { case ms < 50: @@ -265,13 +305,6 @@ func ifaceHealthy(st *InterfaceStatus) bool { return true } -const ( - hostW = 30 // Increased width for host column - numW = 7 - stdW = 8 - nW = 6 -) - func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int { put(scr, 0, y, hline(w), cDefault) @@ -283,17 +316,21 @@ func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int { } st.mu.RLock() header := fmt.Sprintf("%s: %s — %s", st.Name, st.Label, st.IPInfo) - spin := spins[st.SpinFrame] + spinChar := spins[st.SpinFrame] + spinnerFrame := st.SpinFrame meterValue := st.MeterValue st.mu.RUnlock() - // Create the meter - fixed width of 6 characters - meter := createMeter(meterValue, meterWidth) + // Create the meter with appropriate style + meter, meterStyle := createMeter(meterValue, meterWidth) + + // Get rainbow color for spinner (cycle through colors) + spinnerStyle := cRainbow[spinnerFrame%len(cRainbow)] put(scr, 0, y+1, "== ", cDefault) - put(scr, 3, y+1, string(spin)+" ", cDefault) - put(scr, 5, y+1, meter+" ", cDefault) // Add the meter after the spinner - put(scr, 5+meterWidth+1, y+1, header, style) // Move the header after the meter + put(scr, 3, y+1, string(spinChar)+" ", spinnerStyle) // Colorful spinner + put(scr, 5, y+1, meter+" ", meterStyle) // Colored meter + put(scr, 5+meterWidth+1, y+1, header, style) // Move the header after the meter put(scr, 0, y+2, hline(w), cDefault) y += 4 @@ -347,9 +384,9 @@ func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int { put(scr, 0, y, "TCP Connect Stats:", cDefault) y++ // header row - headerRow := fmt.Sprintf("%-*s %*s %*s %*s %*s %*s %*s", + headerRow := fmt.Sprintf("%-*s %*s %*s %*s %*s %*s %*s %*s", hostW, "Host", numW, "last", numW, "min", numW, "avg", - numW, "max", stdW, "stddev", nW, "n") + numW, "max", stdW, "stddev", nW, "n", lostW, "lost") put(scr, 0, y, headerRow, cDefault) y++ @@ -361,7 +398,12 @@ func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int { last := hist[len(hist)-1] mi, ma, av, sd := minMaxAvgStd(hist) - row := fmt.Sprintf("%-*s %*s %*s %*s %*s %*s %*d", + // Get host name without port for lost packets lookup + hostName := strings.Split(hp, ":")[0] + lost := st.LostPackets[hostName] + + // Add lost column + row := fmt.Sprintf("%-*s %*s %*s %*s %*s %*s %*d %*d", hostW, hp, numW, fmt.Sprintf("%.0fms", last), numW, fmt.Sprintf("%.0fms", mi), @@ -369,6 +411,7 @@ func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int { numW, fmt.Sprintf("%.0fms", ma), stdW, fmt.Sprintf("%.0fms", sd), nW, len(hist), + lostW, lost, ) put(scr, 0, y, row, cDefault) // colourise individual numbers @@ -377,6 +420,12 @@ func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int { put(scr, hostW+1+numW*2+2, y, fmt.Sprintf("%*s", numW, fmt.Sprintf("%.0fms", av)), styleLatency(av)) put(scr, hostW+1+numW*3+3, y, fmt.Sprintf("%*s", numW, fmt.Sprintf("%.0fms", ma)), styleLatency(ma)) put(scr, hostW+1+numW*4+4, y, fmt.Sprintf("%*s", stdW, fmt.Sprintf("%.0fms", sd)), cDefault) + // Colorize the lost packets column + lostStyle := cDefault + if lost > 0 { + lostStyle = cRed + } + put(scr, hostW+1+numW*4+4+stdW+1+nW+1, y, fmt.Sprintf("%*d", lostW, lost), lostStyle) y++ } y++ @@ -385,13 +434,22 @@ func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int { put(scr, 0, y, fmt.Sprintf("Total ICMP Requests: %d", st.TotalICMPReq), cDefault) y++ put(scr, 0, y, fmt.Sprintf("Total ICMP Replies: %d", st.TotalICMPRep), cDefault) + y++ + // Add a line showing lost packets + lost := st.TotalICMPReq - st.TotalICMPRep + lostStyle := cDefault + if lost > 0 { + lostStyle = cRed + } + put(scr, 0, y, fmt.Sprintf("Total ICMP Lost: %d", lost), lostStyle) y += 2 st.mu.RUnlock() return y } -// Create a visual meter using green and red dots -func createMeter(value, width int) string { +// Create a visual meter using ASCII characters +// Returns the meter string and the appropriate style +func createMeter(value, width int) (string, tcell.Style) { if value > maxMeterValue { value = maxMeterValue } @@ -399,33 +457,48 @@ func createMeter(value, width int) string { value = 0 } - // Calculate how many dots of each color to show - redDots := value - if redDots > width { - redDots = width + // Calculate the number of fill characters to show + // Map value (0-maxMeterValue) to fill width (0-meterFillWidth) + fillCount := value * meterFillWidth / maxMeterValue + if fillCount > meterFillWidth { + fillCount = meterFillWidth } - greenDots := width - redDots // Build the meter string var b strings.Builder - // Add red dots for unreplied packets - for i := 0; i < redDots; i++ { - b.WriteRune(redDot) + // Add opening bracket + b.WriteRune(meterStart) + + // Add fill characters + for i := 0; i < fillCount; i++ { + b.WriteRune(meterFill) } - // Add green dots for available capacity - for i := 0; i < greenDots; i++ { - b.WriteRune(greenDot) + // Add empty spaces + for i := 0; i < meterFillWidth-fillCount; i++ { + b.WriteRune(meterEmpty) } - return b.String() + // Add closing bracket + b.WriteRune(meterEnd) + + // Get the appropriate style for this meter value + style := getMeterStyle(value) + + return b.String(), style } /*────────────────── goroutine loops ─────────────*/ func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) { logf("Starting reachability monitoring for %s with %d hosts", st.Name, len(hosts)) + + // Add random offset to avoid clustering at 1-second intervals + randomOffset := time.Duration(100+rand.Intn(800)) * time.Millisecond + logf("Reachability monitoring for %s will start after %v offset", st.Name, randomOffset) + time.Sleep(randomOffset) + tk := time.NewTicker(time.Second) defer tk.Stop() for { @@ -469,6 +542,10 @@ func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) { } else { st.DroppedCount++ st.LastDrop = time.Now() + + // Track lost packets per host + st.LostPackets[host]++ + // For failed pings, we don't decrease the meter value // Trigger UI update on ping failure select { @@ -521,8 +598,15 @@ func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) { } } } + func lossLoop(ctx context.Context, st *InterfaceStatus, hosts []string) { logf("Starting packet loss monitoring for %s with %d hosts", st.Name, len(hosts)) + + // Add random offset to avoid clustering at periodic intervals + randomOffset := time.Duration(100+rand.Intn(800)) * time.Millisecond + logf("Packet loss monitoring for %s will start after %v offset", st.Name, randomOffset) + time.Sleep(randomOffset) + tk := time.NewTicker(packetLossPeriod) defer tk.Stop() for { @@ -585,8 +669,15 @@ func lossLoop(ctx context.Context, st *InterfaceStatus, hosts []string) { } } } + func tcpLoop(ctx context.Context, st *InterfaceStatus, hosts []string) { logf("Starting TCP monitoring for %s with %d hosts", st.Name, len(hosts)) + + // Add random offset to avoid clustering at 1-second intervals + randomOffset := time.Duration(100+rand.Intn(800)) * time.Millisecond + logf("TCP monitoring for %s will start after %v offset", st.Name, randomOffset) + time.Sleep(randomOffset) + tk := time.NewTicker(time.Second) defer tk.Stop() for { @@ -702,9 +793,9 @@ func uiLoop(ctx context.Context, scr tcell.Screen, a, b *InterfaceStatus, start var ( reachHosts []string - packetLossHosts = []string{"github.com", "google.com", "1.1.1.1", "8.8.8.8"} + packetLossHosts = []string{"github.com", "google.com", "8.8.8.8", "captive.apple.com"} tcpTestHosts = []string{ - "datavi.be:443", "fast.com:443", "cloudflare.com:443", + "datavi.be:443", "fast.com:443", "console.aws.amazon.com:443", "console.cloud.google.com:443", } ) @@ -725,6 +816,11 @@ func logf(format string, v ...interface{}) { func main() { flag.Parse() logf("Starting rtnetmon") + + // Seed the random number generator + rand.Seed(time.Now().UnixNano()) + logf("Random number generator seeded") + reachHosts = strings.Split(*hostCSV, ",") logf("Monitoring interfaces %s and %s", *ifaceA, *ifaceB) @@ -735,13 +831,14 @@ func main() { newStatus := func(name, label string) *InterfaceStatus { logf("Initializing status for interface %s", name) return &InterfaceStatus{ - Name: name, - Label: label, - IPInfo: fetchIPInfo(name), - Reachable: map[string]bool{}, - Loss: map[string]float64{}, - TCP: map[string][]float64{}, - MeterValue: 0, // Initialize meter value to 0 + Name: name, + Label: label, + IPInfo: fetchIPInfo(name), + Reachable: map[string]bool{}, + Loss: map[string]float64{}, + TCP: map[string][]float64{}, + MeterValue: 0, // Initialize meter value to 0 + LostPackets: map[string]int{}, // Initialize lost packets map } } a, b := newStatus(*ifaceA, *labelA), newStatus(*ifaceB, *labelB)