This commit is contained in:
2025-05-16 04:42:41 -07:00
parent 31936b21a2
commit 5d6ce56406
+276 -217
View File
@@ -9,73 +9,99 @@ import (
"fmt" "fmt"
"math" "math"
"net" "net"
"os"
"os/exec" "os/exec"
"os/signal"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
) )
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Flags // CLI flags
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
var ( var (
ifaceA = flag.String("ifaceA", "gu0", ifaceA = flag.String("ifaceA", "gu0", "primary network interface")
"primary network interface") labelA = flag.String("labelA", "gu LAN - VPN outbound", "label for ifaceA")
labelA = flag.String("labelA", "gu LAN - VPN outbound", ifaceB = flag.String("ifaceB", "backhaul0", "secondary network interface")
"label for ifaceA") labelB = flag.String("labelB", "Cox cable direct", "label for ifaceB")
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,"+ "1.1.1.1,8.8.8.8,8.8.4.4,google.com,github.com,"+
"console.aws.amazon.com,console.cloud.google.com,"+ "console.aws.amazon.com,console.cloud.google.com,"+
"fast.com,cloudflare.com,datavi.be", "fast.com,cloudflare.com,datavi.be",
"hosts for reachability checks") "comma-separated hosts for reachability checks")
) )
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Constants // Consts
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
const ( const (
icmpTimeout = 500 * time.Millisecond icmpTimeout = 500 * time.Millisecond
tcpTimeout = 500 * time.Millisecond tcpTimeout = 500 * time.Millisecond
packetLossPings = 20 packetLossPings = 20
packetLossPeriod = 5 * time.Second packetLossPeriod = 5 * time.Second
statsHistory = 300 // 5 min of 1 Hz samples statsHistory = 300 // 5 min @1 Hz
screenRefresh = 500 * time.Millisecond screenRefresh = 500 * time.Millisecond
) )
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Data structures // Runtime struct
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
type InterfaceStatus struct { type InterfaceStatus struct {
Name string Name, Label, IPInfo string
Label string Reachable map[string]bool
IPInfo string Loss map[string]float64
TCP map[string][]float64
Reachable map[string]bool TotalICMPReq int
Loss map[string]float64 TotalICMPRep int
TCP map[string][]float64 DroppedCount int
LastDrop, LastPing time.Time
TotalICMPReq int SpinFrame int
TotalICMPRep int mu sync.RWMutex
DroppedCount int
LastDrop time.Time
LastPing time.Time
SpinFrame int
mu sync.RWMutex
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Math helpers (no heavy deps) // Styles / colours
// -----------------------------------------------------------------------------
var (
cBrightGreen = tcell.StyleDefault.Foreground(tcell.ColorGreen).Bold(true)
cGreen = tcell.StyleDefault.Foreground(tcell.ColorGreen)
cYellow = tcell.StyleDefault.Foreground(tcell.ColorYellow)
cRed = tcell.StyleDefault.Foreground(tcell.ColorRed)
cDefault = tcell.StyleDefault
)
func styleLatency(ms float64) tcell.Style {
switch {
case ms < 50:
return cBrightGreen
case ms < 100:
return cGreen
case ms < 150:
return cYellow
default:
return cRed
}
}
func styleLoss(pct float64) tcell.Style {
switch {
case pct == 0:
return cBrightGreen
case pct < 5:
return cYellow
default:
return cRed
}
}
// -----------------------------------------------------------------------------
// Small maths
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
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 {
@@ -93,58 +119,56 @@ 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 vs float64
for _, v := range nums { for _, v := range nums {
d := v - avg d := v - avg
vsum += d * d vs += d * d
} }
std = math.Sqrt(vsum / float64(len(nums))) std = math.Sqrt(vs / float64(len(nums)))
return return
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// External look-ups // External lookup
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
type ipInfo struct{ IP, Hostname, Org string } type ipInfoResp 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)
defer cancel() defer cancel()
out, err := exec.CommandContext(ctx, out, err := exec.CommandContext(ctx, "curl", "-s", "--interface", iface,
"curl", "-s", "--interface", iface, "--max-time", "2", "ipinfo.io").Output() "--max-time", "2", "ipinfo.io").Output()
if err != nil { if err != nil {
return "(ipinfo error)" return "(ipinfo error)"
} }
var resp ipInfo var r ipInfoResp
_ = json.Unmarshal(out, &resp) _ = json.Unmarshal(out, &r)
if resp.IP == "" { if r.IP == "" {
return "(ipinfo parse error)" return "(ipinfo parse error)"
} }
return fmt.Sprintf("%s [%s] %s", resp.IP, resp.Hostname, resp.Org) return fmt.Sprintf("%s [%s] %s", r.IP, r.Hostname, r.Org)
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Ping helpers // ICMP 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()
err := exec.CommandContext(ctx, "ping", "-I", iface, "-c1", "-W1", host).Run() return exec.CommandContext(ctx, "ping", "-I", iface, "-c1", "-W1", host).Run() == 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",
"ping", "-q", "-i", "0.05", "-c", fmt.Sprint(packetLossPings), "-c", fmt.Sprint(packetLossPings), "-W1", "-I", iface, host).CombinedOutput()
"-W1", "-I", iface, host).CombinedOutput()
if err != nil { if err != nil {
return 1.0 return 1.0
} }
for _, line := range strings.Split(string(out), "\n") { for _, ln := range strings.Split(string(out), "\n") {
if strings.Contains(line, "packet loss") { if strings.Contains(ln, "packet loss") {
for _, f := range strings.Fields(line) { for _, f := range strings.Fields(ln) {
if strings.HasSuffix(f, "%") { if strings.HasSuffix(f, "%") {
p, _ := strconv.ParseFloat(strings.TrimSuffix(f, "%"), 64) p, _ := strconv.ParseFloat(strings.TrimSuffix(f, "%"), 64)
return p / 100.0 return p / 100.0
@@ -163,8 +187,7 @@ func localAddr(iface string) (net.Addr, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
addrs, _ := ni.Addrs() for _, a := range func() []net.Addr { a, _ := ni.Addrs(); return a }() {
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
} }
@@ -172,41 +195,167 @@ func localAddr(iface string) (net.Addr, error) {
return nil, fmt.Errorf("no IPv4 on %s", iface) return nil, fmt.Errorf("no IPv4 on %s", iface)
} }
func tcpDuration(iface, hostPort string) time.Duration { func tcpDuration(iface, hp string) time.Duration {
la, err := localAddr(iface) la, err := localAddr(iface)
if err != nil { if err != nil {
return tcpTimeout return tcpTimeout
} }
d := net.Dialer{Timeout: tcpTimeout, LocalAddr: la} d := net.Dialer{Timeout: tcpTimeout, LocalAddr: la}
start := time.Now() st := time.Now()
conn, err := d.Dial("tcp", hostPort) c, err := d.Dial("tcp", hp)
if err != nil { if err != nil {
return tcpTimeout return tcpTimeout
} }
conn.Close() c.Close()
return time.Since(start) return time.Since(st)
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Spinner helpers // Spinner
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
var spinner = []rune{'|', '/', '-', '\\'} var spins = []rune{'|', '/', '-', '\\'}
func (st *InterfaceStatus) tickSpinner() { func (st *InterfaceStatus) spin() { st.SpinFrame = (st.SpinFrame + 1) % len(spins) }
st.SpinFrame = (st.SpinFrame + 1) % len(spinner)
// -----------------------------------------------------------------------------
// Screen helpers
// -----------------------------------------------------------------------------
func put(s tcell.Screen, x, y int, str string, st tcell.Style) {
for i, r := range str {
s.SetContent(x+i, y, r, nil, st)
}
}
func hline(w int) string { return strings.Repeat("=", w) }
func headerLine(s tcell.Screen, y, w int, spin rune, txt string) {
line := fmt.Sprintf("== %c %s", spin, txt)
if len(line) > w {
line = line[:w]
}
put(s, 0, y, line, cDefault)
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Goroutines // UI drawing
// -----------------------------------------------------------------------------
func ifaceHeaderStyle(sta *InterfaceStatus) tcell.Style {
sta.mu.RLock()
defer sta.mu.RUnlock()
if sta.DroppedCount > 0 {
return cRed
}
for _, ok := range sta.Reachable {
if !ok {
return cRed
}
}
for _, lp := range sta.Loss {
if lp > 0 {
return cRed
}
}
return cBrightGreen
}
func drawIface(scr tcell.Screen, y, w int, sta *InterfaceStatus) int {
put(scr, 0, y, hline(w), cDefault)
hs := ifaceHeaderStyle(sta)
sta.mu.RLock()
spin := spins[sta.SpinFrame]
head := fmt.Sprintf("%s — %s", sta.Label, sta.IPInfo)
sta.mu.RUnlock()
put(scr, 0, y+1, fmt.Sprintf("== %c %s", spin, head), hs)
put(scr, 0, y+2, hline(w), cDefault)
y += 4
sta.mu.RLock()
total := len(sta.Reachable)
okCnt := 0
for _, ok := range sta.Reachable {
if ok {
okCnt++
}
}
rStyle := cBrightGreen
if okCnt != total {
rStyle = cRed
}
age := time.Since(sta.LastPing).Round(time.Second)
put(scr, 0, y, fmt.Sprintf("Reachable: %d/%d (at %s, age %s)",
okCnt, total, sta.LastPing.Format("15:04:05"), age), rStyle)
y++
if okCnt == total {
put(scr, 0, y, "Unreachable: none", cDefault)
} else {
var down []string
for h, ok := range sta.Reachable {
if !ok {
down = append(down, h)
}
}
put(scr, 0, y, "Unreachable: "+strings.Join(down, ", "), cRed)
}
y += 2
// loss table
put(scr, 0, y, "Packet Loss:", cDefault)
y++
for _, h := range packetLossHosts {
lp := sta.Loss[h] * 100
put(scr, 0, y, fmt.Sprintf("%-16s %5.0f%%", h+":", lp),
styleLoss(lp))
y++
}
dAge := "N/A"
if !sta.LastDrop.IsZero() {
dAge = time.Since(sta.LastDrop).Round(time.Second).String()
}
put(scr, 0, y, fmt.Sprintf("Dropped: %d (last at %s, age %s)",
sta.DroppedCount, sta.LastDrop.Format("15:04:05"), dAge), cDefault)
y += 2
// TCP
put(scr, 0, y, "TCP Connect Stats:", cDefault)
y++
put(scr, 0, y, "Host min avg max stddev n", cDefault)
y++
for _, hp := range tcpTestHosts {
hist := sta.TCP[hp]
if len(hist) == 0 {
continue
}
mi, ma, av, sd := minMaxAvgStd(hist)
put(scr, 0, y, fmt.Sprintf("%-16s", hp), cDefault)
put(scr, 17, y, fmt.Sprintf("%4.0fms", mi), styleLatency(mi))
put(scr, 24, y, fmt.Sprintf("%4.0fms", av), styleLatency(av))
put(scr, 31, y, fmt.Sprintf("%4.0fms", ma), styleLatency(ma))
put(scr, 38, y, fmt.Sprintf("%6.0fms", sd), cDefault)
put(scr, 46, y, fmt.Sprintf("%5d", len(hist)), cDefault)
y++
}
y++
// totals
put(scr, 0, y, fmt.Sprintf("Total ICMP Requests: %d", sta.TotalICMPReq), cDefault)
y++
put(scr, 0, y, fmt.Sprintf("Total ICMP Replies: %d", sta.TotalICMPRep), cDefault)
y += 2
sta.mu.RUnlock()
return y
}
// -----------------------------------------------------------------------------
// Loops
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) { func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
tick := time.NewTicker(time.Second) tk := time.NewTicker(time.Second)
defer tick.Stop() defer tk.Stop()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-tick.C: case <-tk.C:
var wg sync.WaitGroup var wg sync.WaitGroup
res := make(map[string]bool, len(hosts)) res := make(map[string]bool, len(hosts))
mu := sync.Mutex{} mu := sync.Mutex{}
@@ -227,7 +376,7 @@ func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
st.mu.Lock() st.mu.Lock()
if ok { if ok {
st.TotalICMPRep++ st.TotalICMPRep++
st.tickSpinner() st.spin()
} else { } else {
st.DroppedCount++ st.DroppedCount++
st.LastDrop = time.Now() st.LastDrop = time.Now()
@@ -245,13 +394,13 @@ func reachLoop(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) tk := time.NewTicker(packetLossPeriod)
defer tick.Stop() defer tk.Stop()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-tick.C: case <-tk.C:
var wg sync.WaitGroup var wg sync.WaitGroup
res := make(map[string]float64, len(hosts)) res := make(map[string]float64, len(hosts))
mu := sync.Mutex{} mu := sync.Mutex{}
@@ -262,8 +411,8 @@ func lossLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
lp := lossPercent(st.Name, host) lp := lossPercent(st.Name, host)
mu.Lock() mu.Lock()
res[host] = lp res[host] = lp
if lp < 1.0 { if lp == 0 {
st.tickSpinner() st.spin()
} }
mu.Unlock() mu.Unlock()
}(h) }(h)
@@ -279,18 +428,18 @@ 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) tk := time.NewTicker(time.Second)
defer tick.Stop() defer tk.Stop()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-tick.C: case <-tk.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()) { if ms < float64(tcpTimeout.Milliseconds()) {
st.tickSpinner() st.spin()
} }
hist := st.TCP[hp] hist := st.TCP[hp]
if len(hist) >= statsHistory { if len(hist) >= statsHistory {
@@ -304,134 +453,47 @@ func tcpLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// UI helpers // UI loop + input watcher
// -----------------------------------------------------------------------------
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) {
write(s, 0, y, divider(w))
}
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]
}
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()
drawHeader(s, y+1, w, spin, head)
drawDivider(s, y+2, w)
y += 4
st.mu.RLock()
// Reachability
total := len(st.Reachable)
rc := 0
var down []string
for h, ok := range st.Reachable {
if ok {
rc++
} else {
down = append(down, h)
}
}
age := time.Since(st.LastPing).Round(time.Second)
write(s, 0, y, fmt.Sprintf("Reachable: %d/%d (at %s, age %s)",
rc, total, st.LastPing.Format("15:04:05"), age))
y++
if len(down) > 0 {
write(s, 0, y, "Unreachable: "+strings.Join(down, ", "))
} else {
write(s, 0, y, "Unreachable: none")
}
y += 2
// Packet loss
write(s, 0, y, "Packet Loss:")
y++
for _, h := range packetLossHosts {
lp := st.Loss[h] * 100
write(s, 0, y, fmt.Sprintf("%-16s %.0f%%", h+":", lp))
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 {
samples := st.TCP[hp]
if len(samples) == 0 {
continue
}
mi, ma, av, sd := minMaxAvgStd(samples)
write(s, 0, y, fmt.Sprintf("%-16s %.0f/%.0f/%.0f/%.0fms",
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
}
// -----------------------------------------------------------------------------
// UI loop
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
func uiLoop(ctx context.Context, scr tcell.Screen, a, b *InterfaceStatus, start time.Time) { func uiLoop(ctx context.Context, scr tcell.Screen, a, b *InterfaceStatus, start time.Time) {
defer scr.Fini() defer scr.Fini()
tick := time.NewTicker(screenRefresh) tk := time.NewTicker(screenRefresh)
defer tick.Stop() defer tk.Stop()
topSpin := 0
// keyboard watcher (Ctrl-C or q)
go func() {
for {
ev := scr.PollEvent()
switch v := ev.(type) {
case *tcell.EventKey:
if v.Key() == tcell.KeyCtrlC || (v.Rune() == 'q') {
// cancel context -> exit
if cancel := ctx.Done(); cancel != nil {
// nothing
}
}
case nil:
return
}
}
}()
spin := 0
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case <-tick.C: case <-tk.C:
w, _ := scr.Size() w, _ := scr.Size()
scr.Clear() scr.Clear()
put(scr, 0, 0, hline(w), cDefault)
// top banner headerLine(scr, 1, w, spins[spin%len(spins)], time.Now().Format(time.RFC1123Z))
drawDivider(scr, 0, w) put(scr, 0, 2, hline(w), cDefault)
drawHeader(scr, 1, w, spinner[topSpin%len(spinner)], spin++
time.Now().Format(time.RFC1123Z)) put(scr, 0, 3, fmt.Sprintf("Runtime: %s", time.Since(start).Round(time.Second)), cDefault)
drawDivider(scr, 2, w)
topSpin++
write(scr, 0, 3, fmt.Sprintf("Runtime: %s", time.Since(start).Round(time.Second)))
y := 5 y := 5
y = drawInterface(scr, y, w, a) y = drawIface(scr, y, w, a)
_ = drawInterface(scr, y, w, b) _ = drawIface(scr, y, w, b)
scr.Show() scr.Show()
} }
} }
@@ -442,13 +504,10 @@ func uiLoop(ctx context.Context, scr tcell.Screen, a, b *InterfaceStatus, start
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
var ( var (
reachHosts []string reachHosts []string
packetLossHosts = []string{ packetLossHosts = []string{"github.com", "google.com", "1.1.1.1", "8.8.8.8"}
"datavi.be", "google.com", "fast.com", tcpTestHosts = []string{
"cloudflare.com", "github.com", "datavi.be:443", "fast.com:443", "cloudflare.com:443",
} "console.aws.amazon.com:443", "console.cloud.google.com:443",
tcpTestHosts = []string{
"google.com:443", "github.com:443",
"1.1.1.1:443", "8.8.8.8:53",
} }
) )
@@ -456,34 +515,34 @@ func main() {
flag.Parse() flag.Parse()
reachHosts = strings.Split(*hostCSV, ",") reachHosts = strings.Split(*hostCSV, ",")
a := &InterfaceStatus{ mkStatus := func(name, label string) *InterfaceStatus {
Name: *ifaceA, return &InterfaceStatus{
Label: *labelA, Name: name,
Reachable: map[string]bool{}, Label: label,
Loss: map[string]float64{}, IPInfo: fetchIPInfo(name),
TCP: map[string][]float64{}, Reachable: map[string]bool{},
IPInfo: fetchIPInfo(*ifaceA), Loss: map[string]float64{},
} TCP: map[string][]float64{},
b := &InterfaceStatus{ }
Name: *ifaceB,
Label: *labelB,
Reachable: map[string]bool{},
Loss: map[string]float64{},
TCP: map[string][]float64{},
IPInfo: fetchIPInfo(*ifaceB),
} }
a, b := mkStatus(*ifaceA, *labelA), mkStatus(*ifaceB, *labelB)
scr, err := tcell.NewScreen() scr, err := tcell.NewScreen()
if err != nil { if err != nil {
panic(err) panic(err)
} }
if err := scr.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()
// Ctrl-C from terminal (outside raw mode)
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
go func() { <-sig; cancel() }()
go reachLoop(ctx, a, reachHosts) go reachLoop(ctx, a, reachHosts)
go reachLoop(ctx, b, reachHosts) go reachLoop(ctx, b, reachHosts)
go lossLoop(ctx, a, packetLossHosts) go lossLoop(ctx, a, packetLossHosts)