This commit is contained in:
2025-05-16 04:29:05 -07:00
parent 26e7fd997f
commit 3d543f9c5b
+114 -62
View File
@@ -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())
}