latest
This commit is contained in:
@@ -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]
|
||||||
@@ -309,13 +331,14 @@ func drawInterface(s tcell.Screen, y, w int, st *InterfaceStatus) int {
|
|||||||
drawDivider(s, y, w)
|
drawDivider(s, y, w)
|
||||||
st.mu.RLock()
|
st.mu.RLock()
|
||||||
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())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user