latest
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// netmon – real-time dual-interface network dashboard
|
// netmon – dual-interface network dashboard (curses)
|
||||||
// WTFPL – 2025-05-16 sneak@sneak.berlin
|
// WTFPL – 2025-05-16 sneak@sneak.berlin
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -19,56 +19,55 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// Configuration flags
|
// Flags
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ifaceA = flag.String("ifaceA", "gu0", "primary network interface")
|
ifaceA = flag.String("ifaceA", "gu0",
|
||||||
labelA = flag.String("labelA", "gu LAN - VPN outbound", "label for ifaceA")
|
"primary network interface")
|
||||||
ifaceB = flag.String("ifaceB", "backhaul0", "secondary network interface")
|
labelA = flag.String("labelA", "gu LAN - VPN outbound",
|
||||||
labelB = flag.String("labelB", "Cox cable direct", "label for ifaceB")
|
"label for ifaceA")
|
||||||
|
ifaceB = flag.String("ifaceB", "backhaul0",
|
||||||
|
"secondary network interface")
|
||||||
|
labelB = flag.String("labelB", "Cox cable direct",
|
||||||
|
"label for ifaceB")
|
||||||
|
|
||||||
hostCSV = flag.String("hosts",
|
hostCSV = flag.String("hosts",
|
||||||
"1.1.1.1,8.8.8.8,8.8.4.4,google.com,github.com,console.aws.amazon.com,"+
|
"1.1.1.1,8.8.8.8,8.8.4.4,google.com,github.com,"+
|
||||||
"console.cloud.google.com,fast.com,cloudflare.com,datavi.be",
|
"console.aws.amazon.com,console.cloud.google.com,"+
|
||||||
"comma-separated hosts for reachability checks")
|
"fast.com,cloudflare.com,datavi.be",
|
||||||
|
"hosts for reachability checks")
|
||||||
)
|
)
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// Constants
|
// Constants
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
const (
|
const (
|
||||||
icmpTimeout = 500 * time.Millisecond // per-ping timeout
|
icmpTimeout = 500 * time.Millisecond
|
||||||
tcpTimeout = 500 * time.Millisecond // TCP handshake timeout
|
tcpTimeout = 500 * time.Millisecond
|
||||||
packetLossPings = 20 // pings per host for loss calc
|
packetLossPings = 20
|
||||||
packetLossPeriod = 5 * time.Second // how often to recalc loss
|
packetLossPeriod = 5 * time.Second
|
||||||
statsHistory = 300 // 5 minutes of 1 Hz samples
|
statsHistory = 300 // 5 min of 1 Hz samples
|
||||||
screenRefresh = 500 * time.Millisecond // spinner / UI cadence
|
screenRefresh = 500 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// Runtime data structures
|
// Data
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
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 // last reachability results
|
Loss map[string]float64
|
||||||
Loss map[string]float64// packet-loss %
|
TCP map[string][]float64
|
||||||
TCP map[string][]float64 // rolling TCP ms samples
|
LastPing time.Time
|
||||||
|
SpinFrame int
|
||||||
LastPing time.Time
|
mu sync.RWMutex
|
||||||
|
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// Helpers – maths without external 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
|
||||||
@@ -85,22 +84,19 @@ func minMaxAvgStd(nums []float64) (min, max, avg, std float64) {
|
|||||||
sum += v
|
sum += v
|
||||||
}
|
}
|
||||||
avg = sum / float64(len(nums))
|
avg = sum / float64(len(nums))
|
||||||
var varSum float64
|
var vSum float64
|
||||||
for _, v := range nums {
|
for _, v := range nums {
|
||||||
diff := v - avg
|
diff := v - avg
|
||||||
varSum += diff * diff
|
vSum += diff * diff
|
||||||
}
|
}
|
||||||
std = math.Sqrt(varSum / 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 {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
@@ -119,21 +115,18 @@ func fetchIPInfo(iface string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// Ping helpers
|
// Ping + loss
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
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()
|
||||||
err := exec.CommandContext(ctx,
|
err := exec.CommandContext(ctx, "ping", "-I", iface, "-c1", "-W1", host).Run()
|
||||||
"ping", "-I", iface, "-c1", "-W1", host).Run()
|
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func lossPercent(iface, host string) float64 {
|
func lossPercent(iface, host string) float64 {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
out, err := exec.CommandContext(ctx,
|
out, err := exec.CommandContext(ctx,
|
||||||
"ping", "-q", "-i", "0.05", "-c", fmt.Sprint(packetLossPings),
|
"ping", "-q", "-i", "0.05", "-c", fmt.Sprint(packetLossPings),
|
||||||
"-W1", "-I", iface, host).CombinedOutput()
|
"-W1", "-I", iface, host).CombinedOutput()
|
||||||
@@ -141,13 +134,12 @@ func lossPercent(iface, host string) float64 {
|
|||||||
return 1.0
|
return 1.0
|
||||||
}
|
}
|
||||||
for _, line := range strings.Split(string(out), "\n") {
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
if !strings.Contains(line, "packet loss") {
|
if strings.Contains(line, "packet loss") {
|
||||||
continue
|
for _, f := range strings.Fields(line) {
|
||||||
}
|
if strings.HasSuffix(f, "%") {
|
||||||
for _, f := range strings.Fields(line) {
|
p, _ := strconv.ParseFloat(strings.TrimSuffix(f, "%"), 64)
|
||||||
if strings.HasSuffix(f, "%") {
|
return p / 100.0
|
||||||
pct, _ := strconv.ParseFloat(strings.TrimSuffix(f, "%"), 64)
|
}
|
||||||
return pct / 100.0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,18 +147,14 @@ func lossPercent(iface, host string) float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// TCP helpers
|
// TCP timing
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
func localAddr(iface string) (net.Addr, error) {
|
||||||
func localAddrFor(iface string) (net.Addr, error) {
|
|
||||||
ni, err := net.InterfaceByName(iface)
|
ni, err := net.InterfaceByName(iface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
addrs, err := ni.Addrs()
|
addrs, _ := ni.Addrs()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, a := range addrs {
|
for _, a := range addrs {
|
||||||
if ipnet, ok := a.(*net.IPNet); ok && ipnet.IP.To4() != nil {
|
if ipnet, ok := a.(*net.IPNet); ok && ipnet.IP.To4() != nil {
|
||||||
return &net.TCPAddr{IP: ipnet.IP}, nil
|
return &net.TCPAddr{IP: ipnet.IP}, nil
|
||||||
@@ -176,7 +164,7 @@ func localAddrFor(iface string) (net.Addr, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func tcpDuration(iface, hostPort string) time.Duration {
|
func tcpDuration(iface, hostPort string) time.Duration {
|
||||||
la, err := localAddrFor(iface)
|
la, err := localAddr(iface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tcpTimeout
|
return tcpTimeout
|
||||||
}
|
}
|
||||||
@@ -191,36 +179,44 @@ func tcpDuration(iface, hostPort string) time.Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// Goroutines – reachability, loss, TCP stats
|
// Spinners
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
var spinner = []rune{'|', '/', '-', '\\'}
|
||||||
|
|
||||||
func reachabilityLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
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)
|
tick := time.NewTicker(time.Second)
|
||||||
defer tick.Stop()
|
defer tick.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-tick.C:
|
case <-tick.C:
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
results := make(map[string]bool, len(hosts))
|
res := make(map[string]bool, len(hosts))
|
||||||
mu := sync.Mutex{}
|
mu := sync.Mutex{}
|
||||||
|
|
||||||
for _, h := range hosts {
|
for _, h := range hosts {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(host string) {
|
go func(host string) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
ok := pingOnce(st.Name, host)
|
ok := pingOnce(st.Name, host)
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
results[host] = ok
|
res[host] = ok
|
||||||
|
if ok {
|
||||||
|
st.tickSpinner()
|
||||||
|
}
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
}(h)
|
}(h)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
st.mu.Lock()
|
st.mu.Lock()
|
||||||
st.Reachable = results
|
st.Reachable = res
|
||||||
st.LastPing = time.Now()
|
st.LastPing = time.Now()
|
||||||
st.mu.Unlock()
|
st.mu.Unlock()
|
||||||
}
|
}
|
||||||
@@ -230,30 +226,30 @@ func reachabilityLoop(ctx context.Context, st *InterfaceStatus, hosts []string)
|
|||||||
func lossLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
func lossLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
||||||
tick := time.NewTicker(packetLossPeriod)
|
tick := time.NewTicker(packetLossPeriod)
|
||||||
defer tick.Stop()
|
defer tick.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-tick.C:
|
case <-tick.C:
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
results := make(map[string]float64, len(hosts))
|
res := make(map[string]float64, len(hosts))
|
||||||
mu := sync.Mutex{}
|
mu := sync.Mutex{}
|
||||||
|
|
||||||
for _, h := range hosts {
|
for _, h := range hosts {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(host string) {
|
go func(host string) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
lp := lossPercent(st.Name, host)
|
lp := lossPercent(st.Name, host)
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
results[host] = lp
|
res[host] = lp
|
||||||
|
if lp < 1.0 {
|
||||||
|
st.tickSpinner()
|
||||||
|
}
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
}(h)
|
}(h)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
st.mu.Lock()
|
st.mu.Lock()
|
||||||
for k, v := range results {
|
for k, v := range res {
|
||||||
st.Loss[k] = v
|
st.Loss[k] = v
|
||||||
}
|
}
|
||||||
st.mu.Unlock()
|
st.mu.Unlock()
|
||||||
@@ -264,7 +260,6 @@ func lossLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
|||||||
func tcpLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
func tcpLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
||||||
tick := time.NewTicker(time.Second)
|
tick := time.NewTicker(time.Second)
|
||||||
defer tick.Stop()
|
defer tick.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -272,11 +267,13 @@ func tcpLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
|||||||
case <-tick.C:
|
case <-tick.C:
|
||||||
for _, hp := range hosts {
|
for _, hp := range hosts {
|
||||||
ms := float64(tcpDuration(st.Name, hp).Milliseconds())
|
ms := float64(tcpDuration(st.Name, hp).Milliseconds())
|
||||||
|
|
||||||
st.mu.Lock()
|
st.mu.Lock()
|
||||||
|
if ms < float64(tcpTimeout.Milliseconds()) {
|
||||||
|
st.tickSpinner()
|
||||||
|
}
|
||||||
hist := st.TCP[hp]
|
hist := st.TCP[hp]
|
||||||
if len(hist) >= statsHistory {
|
if len(hist) >= statsHistory {
|
||||||
hist = hist[1:] // drop oldest
|
hist = hist[1:]
|
||||||
}
|
}
|
||||||
st.TCP[hp] = append(hist, ms)
|
st.TCP[hp] = append(hist, ms)
|
||||||
st.mu.Unlock()
|
st.mu.Unlock()
|
||||||
@@ -286,25 +283,39 @@ func tcpLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// UI
|
// UI helpers
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
func write(s tcell.Screen, x, y int, str string) {
|
||||||
var spin = []rune{'|', '/', '-', '\\'}
|
|
||||||
|
|
||||||
func writeLine(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 drawSection(s tcell.Screen, x, y, w int, st *InterfaceStatus) {
|
func drawDivider(s tcell.Screen, y, w int) {
|
||||||
|
line := strings.Repeat("=", w)
|
||||||
|
write(s, 0, y, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawHeaderLine(s tcell.Screen, y, w int, spin rune, txt string) {
|
||||||
|
line := fmt.Sprintf("== %c %s", spin, txt)
|
||||||
|
if len(line) > w {
|
||||||
|
line = line[:w]
|
||||||
|
}
|
||||||
|
write(s, 0, y, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawInterface(s tcell.Screen, y, w int, st *InterfaceStatus) int {
|
||||||
|
// divider, header, divider
|
||||||
|
drawDivider(s, y, w)
|
||||||
|
st.mu.RLock()
|
||||||
|
spin := spinner[st.SpinFrame]
|
||||||
|
head := fmt.Sprintf("%s — %s", st.Label, st.IPInfo)
|
||||||
|
st.mu.RUnlock()
|
||||||
|
drawHeaderLine(s, y+1, w, spin, head)
|
||||||
|
drawDivider(s, y+2, w)
|
||||||
|
y += 4 // skip blank line after
|
||||||
|
// contents
|
||||||
st.mu.RLock()
|
st.mu.RLock()
|
||||||
defer st.mu.RUnlock()
|
|
||||||
|
|
||||||
// header
|
|
||||||
writeLine(s, x, y, fmt.Sprintf("%s — %s", st.Label, st.IPInfo))
|
|
||||||
|
|
||||||
// reachability
|
|
||||||
total := len(st.Reachable)
|
total := len(st.Reachable)
|
||||||
rc := 0
|
rc := 0
|
||||||
var down []string
|
var down []string
|
||||||
@@ -316,121 +327,117 @@ func drawSection(s tcell.Screen, x, y, w int, st *InterfaceStatus) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
age := time.Since(st.LastPing).Round(time.Second)
|
age := time.Since(st.LastPing).Round(time.Second)
|
||||||
writeLine(s, x, y+1,
|
write(s, 0, y, fmt.Sprintf("Reachable: %d/%d (at %s, age %s)",
|
||||||
fmt.Sprintf("Reachable: %d/%d (at %s, age %s)",
|
rc, total, st.LastPing.Format("15:04:05"), age))
|
||||||
rc, total, st.LastPing.Format("15:04:05"), age))
|
y++
|
||||||
if len(down) > 0 {
|
if len(down) > 0 {
|
||||||
writeLine(s, x, y+2, "Unreachable: "+strings.Join(down, ", "))
|
write(s, 0, y, "Unreachable: "+strings.Join(down, ", "))
|
||||||
} else {
|
} else {
|
||||||
writeLine(s, x, y+2, "Unreachable: none")
|
write(s, 0, y, "Unreachable: none")
|
||||||
}
|
}
|
||||||
|
y += 2
|
||||||
// packet loss
|
write(s, 0, y, "Packet Loss:")
|
||||||
row := y + 4
|
y++
|
||||||
writeLine(s, x, row, "Packet Loss:")
|
|
||||||
row++
|
|
||||||
for _, h := range packetLossHosts {
|
for _, h := range packetLossHosts {
|
||||||
lp := st.Loss[h] * 100
|
lp := st.Loss[h] * 100
|
||||||
writeLine(s, x, row, fmt.Sprintf("%-16s %.0f%%", h+":", lp))
|
write(s, 0, y, fmt.Sprintf("%-16s %.0f%%", h+":", lp))
|
||||||
row++
|
y++
|
||||||
}
|
}
|
||||||
|
y++
|
||||||
// TCP stats
|
write(s, 0, y, "TCP Connect Stats:")
|
||||||
writeLine(s, x, row, "TCP Connect Stats:")
|
y++
|
||||||
row++
|
|
||||||
for _, hp := range tcpTestHosts {
|
for _, hp := range tcpTestHosts {
|
||||||
samples := st.TCP[hp]
|
samples := st.TCP[hp]
|
||||||
if len(samples) == 0 {
|
if len(samples) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mi, ma, av, sd := minMaxAvgStd(samples)
|
mi, ma, av, sd := minMaxAvgStd(samples)
|
||||||
writeLine(s, x, row,
|
write(s, 0, y, fmt.Sprintf("%-16s %.0f/%.0f/%.0f/%.0fms",
|
||||||
fmt.Sprintf("%-16s %.0f/%.0f/%.0f/%.0fms",
|
hp, mi, av, ma, sd))
|
||||||
hp, mi, av, ma, sd))
|
y++
|
||||||
row++
|
|
||||||
}
|
}
|
||||||
|
st.mu.RUnlock()
|
||||||
|
return y + 2 // leave a blank line after section
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// UI main loop
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
func uiLoop(ctx context.Context, scr tcell.Screen, a, b *InterfaceStatus) {
|
func uiLoop(ctx context.Context, scr tcell.Screen, a, b *InterfaceStatus) {
|
||||||
defer scr.Fini()
|
defer scr.Fini()
|
||||||
tick := time.NewTicker(screenRefresh)
|
ticker := time.NewTicker(screenRefresh)
|
||||||
defer tick.Stop()
|
defer ticker.Stop()
|
||||||
frame := 0
|
topSpin := 0
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-tick.C:
|
case <-ticker.C:
|
||||||
|
w, _ := scr.Size()
|
||||||
scr.Clear()
|
scr.Clear()
|
||||||
w, h := scr.Size()
|
// top banner
|
||||||
mid := h / 2
|
drawDivider(scr, 0, w)
|
||||||
|
drawHeaderLine(scr, 1, w, spinner[topSpin%len(spinner)],
|
||||||
ts := time.Now().Format(time.RFC1123Z)
|
time.Now().Format(time.RFC1123Z))
|
||||||
writeLine(scr, 0, 0, fmt.Sprintf("%c %s", spin[frame%len(spin)], ts))
|
drawDivider(scr, 2, w)
|
||||||
frame++
|
topSpin++
|
||||||
|
y := 4
|
||||||
drawSection(scr, 0, 1, w, a)
|
y = drawInterface(scr, y, w, a)
|
||||||
drawSection(scr, 0, mid, w, b)
|
y = drawInterface(scr, y, w, b)
|
||||||
|
|
||||||
scr.Show()
|
scr.Show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// Entry
|
// Main
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
var (
|
var (
|
||||||
reachabilityHosts []string
|
reachHosts []string
|
||||||
packetLossHosts = []string{"datavi.be", "google.com", "fast.com", "cloudflare.com", "github.com"}
|
packetLossHosts = []string{"datavi.be", "google.com", "fast.com",
|
||||||
tcpTestHosts = []string{"google.com:443", "github.com:443", "1.1.1.1:443", "8.8.8.8:53"}
|
"cloudflare.com", "github.com"}
|
||||||
|
tcpTestHosts = []string{"google.com:443", "github.com:443",
|
||||||
|
"1.1.1.1:443", "8.8.8.8:53"}
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
reachabilityHosts = strings.Split(*hostCSV, ",")
|
reachHosts = strings.Split(*hostCSV, ",")
|
||||||
|
|
||||||
// prepare interface status objects
|
a := &InterfaceStatus{
|
||||||
stA := &InterfaceStatus{
|
|
||||||
Name: *ifaceA,
|
Name: *ifaceA,
|
||||||
Label: *labelA,
|
Label: *labelA,
|
||||||
Reachable: make(map[string]bool),
|
Reachable: map[string]bool{},
|
||||||
Loss: make(map[string]float64),
|
Loss: map[string]float64{},
|
||||||
TCP: make(map[string][]float64),
|
TCP: map[string][]float64{},
|
||||||
IPInfo: fetchIPInfo(*ifaceA),
|
IPInfo: fetchIPInfo(*ifaceA),
|
||||||
}
|
}
|
||||||
stB := &InterfaceStatus{
|
b := &InterfaceStatus{
|
||||||
Name: *ifaceB,
|
Name: *ifaceB,
|
||||||
Label: *labelB,
|
Label: *labelB,
|
||||||
Reachable: make(map[string]bool),
|
Reachable: map[string]bool{},
|
||||||
Loss: make(map[string]float64),
|
Loss: map[string]float64{},
|
||||||
TCP: make(map[string][]float64),
|
TCP: map[string][]float64{},
|
||||||
IPInfo: fetchIPInfo(*ifaceB),
|
IPInfo: fetchIPInfo(*ifaceB),
|
||||||
}
|
}
|
||||||
|
|
||||||
screen, err := tcell.NewScreen()
|
scr, err := tcell.NewScreen()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if err := screen.Init(); err != nil {
|
if err := scr.Init(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// background jobs
|
go reachLoop(ctx, a, reachHosts)
|
||||||
go reachabilityLoop(ctx, stA, reachabilityHosts)
|
go reachLoop(ctx, b, reachHosts)
|
||||||
go reachabilityLoop(ctx, stB, reachabilityHosts)
|
go lossLoop(ctx, a, packetLossHosts)
|
||||||
|
go lossLoop(ctx, b, packetLossHosts)
|
||||||
|
go tcpLoop(ctx, a, tcpTestHosts)
|
||||||
|
go tcpLoop(ctx, b, tcpTestHosts)
|
||||||
|
|
||||||
go lossLoop(ctx, stA, packetLossHosts)
|
uiLoop(ctx, scr, a, b)
|
||||||
go lossLoop(ctx, stB, packetLossHosts)
|
|
||||||
|
|
||||||
go tcpLoop(ctx, stA, tcpTestHosts)
|
|
||||||
go tcpLoop(ctx, stB, tcpTestHosts)
|
|
||||||
|
|
||||||
// UI (blocks)
|
|
||||||
uiLoop(ctx, screen, stA, stB)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user