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