latest
This commit is contained in:
@@ -21,7 +21,7 @@ import (
|
|||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*──────────────────────── CLI ────────────────────────*/
|
/*────────────────── CLI flags ──────────────────*/
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ifaceA = flag.String("ifaceA", "gu0", "primary network interface")
|
ifaceA = flag.String("ifaceA", "gu0", "primary network interface")
|
||||||
@@ -33,21 +33,21 @@ var (
|
|||||||
"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 reachability hosts")
|
||||||
)
|
)
|
||||||
|
|
||||||
/*──────────────────────── constants ─────────────────────*/
|
/*────────────────── constants ──────────────────*/
|
||||||
|
|
||||||
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 @1 Hz
|
statsHistory = 300
|
||||||
screenRefresh = 500 * time.Millisecond
|
screenRefresh = 500 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
/*──────────────────────── runtime data ──────────────────*/
|
/*────────────────── runtime struct ─────────────*/
|
||||||
|
|
||||||
type InterfaceStatus struct {
|
type InterfaceStatus struct {
|
||||||
Name, Label, IPInfo string
|
Name, Label, IPInfo string
|
||||||
@@ -61,7 +61,7 @@ type InterfaceStatus struct {
|
|||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
/*──────────────────────── colours ───────────────────────*/
|
/*────────────────── colour styles ──────────────*/
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cBrightGreen = tcell.StyleDefault.Foreground(tcell.ColorGreen).Bold(true)
|
cBrightGreen = tcell.StyleDefault.Foreground(tcell.ColorGreen).Bold(true)
|
||||||
@@ -78,25 +78,24 @@ func styleLatency(ms float64) tcell.Style {
|
|||||||
return cBrightGreen
|
return cBrightGreen
|
||||||
case ms < 100:
|
case ms < 100:
|
||||||
return cGreen
|
return cGreen
|
||||||
case ms < 200: // new threshold
|
case ms < 200:
|
||||||
return cYellow
|
return cYellow
|
||||||
default:
|
default:
|
||||||
return cRed
|
return cRed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func styleLoss(p float64) tcell.Style {
|
||||||
func styleLoss(pct float64) tcell.Style {
|
|
||||||
switch {
|
switch {
|
||||||
case pct == 0:
|
case p == 0:
|
||||||
return cBrightGreen
|
return cBrightGreen
|
||||||
case pct < 5:
|
case p < 5:
|
||||||
return cYellow
|
return cYellow
|
||||||
default:
|
default:
|
||||||
return cBrightRed
|
return cBrightRed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*──────────────────────── maths ─────────────────────────*/
|
/*────────────────── math helpers ───────────────*/
|
||||||
|
|
||||||
func minMaxAvgStd(xs []float64) (min, max, avg, std float64) {
|
func minMaxAvgStd(xs []float64) (min, max, avg, std float64) {
|
||||||
if len(xs) == 0 {
|
if len(xs) == 0 {
|
||||||
@@ -123,33 +122,29 @@ func minMaxAvgStd(xs []float64) (min, max, avg, std float64) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/*──────────────────────── external lookup ───────────────*/
|
/*────────────────── external lookup ────────────*/
|
||||||
|
|
||||||
type ipInfoResp 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, "curl", "-s", "--interface", iface, "--max-time", "2", "ipinfo.io").Output()
|
out, _ := exec.CommandContext(ctx, "curl", "-s", "--interface", iface, "--max-time", "2", "ipinfo.io").Output()
|
||||||
if err != nil {
|
|
||||||
return "(ipinfo error)"
|
|
||||||
}
|
|
||||||
var r ipInfoResp
|
var r ipInfoResp
|
||||||
_ = json.Unmarshal(out, &r)
|
_ = json.Unmarshal(out, &r)
|
||||||
if r.IP == "" {
|
if r.IP == "" {
|
||||||
return "(ipinfo parse error)"
|
return "(ipinfo error)"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s [%s] %s", r.IP, r.Hostname, r.Org)
|
return fmt.Sprintf("%s [%s] %s", r.IP, r.Hostname, r.Org)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*──────────────────────── ICMP 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()
|
||||||
return exec.CommandContext(ctx, "ping", "-I", iface, "-c1", "-W1", host).Run() == nil
|
return exec.CommandContext(ctx, "ping", "-I", iface, "-c1", "-W1", host).Run() == 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()
|
||||||
@@ -171,126 +166,125 @@ func lossPercent(iface, host string) float64 {
|
|||||||
return 1.0
|
return 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
/*──────────────────────── TCP helpers ───────────────────*/
|
/*────────────────── TCP helpers ───────────────*/
|
||||||
|
|
||||||
func localAddr(iface string) (net.Addr, error) {
|
func localAddr(iface string) (net.Addr, error) {
|
||||||
ni, err := net.InterfaceByName(iface)
|
ifi, err := net.InterfaceByName(iface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, a := range func() []net.Addr { a, _ := ni.Addrs(); return a }() {
|
add, _ := ifi.Addrs()
|
||||||
|
for _, a := range add {
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("no IPv4 on %s", iface)
|
return nil, fmt.Errorf("no IPv4 on %s", iface)
|
||||||
}
|
}
|
||||||
|
|
||||||
func tcpDuration(iface, hp 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()
|
||||||
c, err := d.Dial("tcp", hp)
|
c, err := d.Dial("tcp", hp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tcpTimeout
|
return tcpTimeout
|
||||||
}
|
}
|
||||||
c.Close()
|
c.Close()
|
||||||
return time.Since(start)
|
return time.Since(st)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*──────────────────────── spinner ───────────────────────*/
|
/*────────────────── spinner ────────────────────*/
|
||||||
|
|
||||||
var spins = []rune{'|', '/', '-', '\\'}
|
var spins = []rune{'|', '/', '-', '\\'}
|
||||||
|
|
||||||
func (st *InterfaceStatus) spin() { st.SpinFrame = (st.SpinFrame + 1) % len(spins) }
|
func (st *InterfaceStatus) spin() { st.SpinFrame = (st.SpinFrame + 1) % len(spins) }
|
||||||
|
|
||||||
/*──────────────────────── screen helpers ────────────────*/
|
/*────────────────── screen helpers ─────────────*/
|
||||||
|
|
||||||
func put(scr tcell.Screen, x, y int, txt string, st tcell.Style) {
|
func put(scr tcell.Screen, x, y int, txt string, st tcell.Style) {
|
||||||
for i, r := range txt {
|
for i, r := range txt {
|
||||||
scr.SetContent(x+i, y, r, nil, st)
|
scr.SetContent(x+i, y, r, nil, st)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func hline(w int) string { return strings.Repeat("=", w) }
|
func hline(w int) string { return strings.Repeat("=", w) }
|
||||||
|
|
||||||
/*──────────────────────── UI draw helpers ───────────────*/
|
/*────────────────── UI drawing ─────────────────*/
|
||||||
|
|
||||||
func ifaceHeaderStyle(sta *InterfaceStatus) tcell.Style {
|
func ifaceHealthy(st *InterfaceStatus) bool {
|
||||||
sta.mu.RLock()
|
st.mu.RLock()
|
||||||
defer sta.mu.RUnlock()
|
defer st.mu.RUnlock()
|
||||||
if sta.DroppedCount > 0 {
|
if st.DroppedCount > 0 {
|
||||||
return cBrightRed
|
return false
|
||||||
}
|
}
|
||||||
for _, ok := range sta.Reachable {
|
for _, ok := range st.Reachable {
|
||||||
if !ok {
|
if !ok {
|
||||||
return cBrightRed
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, lp := range sta.Loss {
|
for _, lp := range st.Loss {
|
||||||
if lp > 0 {
|
if lp > 0 {
|
||||||
return cBrightRed
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cBrightGreen
|
for _, hp := range tcpTestHosts {
|
||||||
|
h := st.TCP[hp]
|
||||||
|
if len(h) == 0 || h[len(h)-1] >= float64(tcpTimeout.Milliseconds()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
hostCol = 0
|
hostW = 23
|
||||||
minCol = 18
|
numW = 7
|
||||||
avgCol = 27
|
stdW = 8
|
||||||
maxCol = 36
|
nW = 6
|
||||||
stdCol = 45
|
|
||||||
nCol = 56
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func drawLatencyRow(scr tcell.Screen, y int, host string, mi, av, ma, sd float64, n int) {
|
func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int {
|
||||||
put(scr, hostCol, y, fmt.Sprintf("%-17s", host), cDefault)
|
|
||||||
put(scr, minCol, y, fmt.Sprintf("%6.0fms", mi), styleLatency(mi))
|
|
||||||
put(scr, avgCol, y, fmt.Sprintf("%6.0fms", av), styleLatency(av))
|
|
||||||
put(scr, maxCol, y, fmt.Sprintf("%6.0fms", ma), styleLatency(ma))
|
|
||||||
put(scr, stdCol, y, fmt.Sprintf("%7.0fms", sd), cDefault)
|
|
||||||
put(scr, nCol, y, fmt.Sprintf("%5d", n), cDefault)
|
|
||||||
}
|
|
||||||
|
|
||||||
func drawIface(scr tcell.Screen, y, w int, sta *InterfaceStatus) int {
|
|
||||||
/* header */
|
|
||||||
put(scr, 0, y, hline(w), cDefault)
|
put(scr, 0, y, hline(w), cDefault)
|
||||||
hs := ifaceHeaderStyle(sta)
|
|
||||||
sta.mu.RLock()
|
// header line
|
||||||
head := fmt.Sprintf("== %c %s — %s", spins[sta.SpinFrame], sta.Label, sta.IPInfo)
|
healthy := ifaceHealthy(st)
|
||||||
sta.mu.RUnlock()
|
style := cBrightGreen
|
||||||
if len(head) > w {
|
if !healthy {
|
||||||
head = head[:w]
|
style = cBrightRed
|
||||||
}
|
}
|
||||||
put(scr, 0, y+1, head, hs)
|
st.mu.RLock()
|
||||||
|
header := fmt.Sprintf("%s: %s — %s", st.Name, st.Label, st.IPInfo)
|
||||||
|
spin := spins[st.SpinFrame]
|
||||||
|
st.mu.RUnlock()
|
||||||
|
put(scr, 0, y+1, "== ", cDefault)
|
||||||
|
put(scr, 3, y+1, string(spin)+" "+header, style)
|
||||||
put(scr, 0, y+2, hline(w), cDefault)
|
put(scr, 0, y+2, hline(w), cDefault)
|
||||||
y += 4
|
y += 4
|
||||||
|
|
||||||
/* reachability */
|
/* reachability */
|
||||||
sta.mu.RLock()
|
st.mu.RLock()
|
||||||
total := len(sta.Reachable)
|
total := len(st.Reachable)
|
||||||
good := 0
|
good := 0
|
||||||
for _, ok := range sta.Reachable {
|
for _, ok := range st.Reachable {
|
||||||
if ok {
|
if ok {
|
||||||
good++
|
good++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rStyle := cBrightGreen
|
age := time.Since(st.LastPing).Round(time.Second)
|
||||||
|
reachStyle := cBrightGreen
|
||||||
if good != total {
|
if good != total {
|
||||||
rStyle = cBrightRed
|
reachStyle = cBrightRed
|
||||||
}
|
}
|
||||||
age := time.Since(sta.LastPing).Round(time.Second)
|
put(scr, 0, y, fmt.Sprintf("Reachable: %d/%d (at %s, age %s)",
|
||||||
put(scr, 0, y, fmt.Sprintf("Reachable: %d/%d (at %s, age %s)", good, total, sta.LastPing.Format("15:04:05"), age), rStyle)
|
good, total, st.LastPing.Format("15:04:05"), age), reachStyle)
|
||||||
y++
|
y++
|
||||||
if good == total {
|
if good == total {
|
||||||
put(scr, 0, y, "Unreachable: none", cDefault)
|
put(scr, 0, y, "Unreachable: none", cDefault)
|
||||||
} else {
|
} else {
|
||||||
var down []string
|
var down []string
|
||||||
for h, ok := range sta.Reachable {
|
for h, ok := range st.Reachable {
|
||||||
if !ok {
|
if !ok {
|
||||||
down = append(down, h)
|
down = append(down, h)
|
||||||
}
|
}
|
||||||
@@ -303,49 +297,65 @@ func drawIface(scr tcell.Screen, y, w int, sta *InterfaceStatus) int {
|
|||||||
put(scr, 0, y, "Packet Loss:", cDefault)
|
put(scr, 0, y, "Packet Loss:", cDefault)
|
||||||
y++
|
y++
|
||||||
for _, h := range packetLossHosts {
|
for _, h := range packetLossHosts {
|
||||||
pct := sta.Loss[h] * 100
|
p := st.Loss[h] * 100
|
||||||
put(scr, 0, y, fmt.Sprintf("%-16s %5.0f%%", h+":", pct), styleLoss(pct))
|
put(scr, 0, y, fmt.Sprintf("%-16s %5.0f%%", h+":", p), styleLoss(p))
|
||||||
y++
|
y++
|
||||||
}
|
}
|
||||||
dAge := "N/A"
|
dAge := "N/A"
|
||||||
if !sta.LastDrop.IsZero() {
|
if !st.LastDrop.IsZero() {
|
||||||
dAge = time.Since(sta.LastDrop).Round(time.Second).String()
|
dAge = time.Since(st.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)
|
put(scr, 0, y, fmt.Sprintf("Dropped: %d (last at %s, age %s)",
|
||||||
|
st.DroppedCount, st.LastDrop.Format("15:04:05"), dAge), cDefault)
|
||||||
y += 2
|
y += 2
|
||||||
|
|
||||||
/* TCP table */
|
/* TCP table */
|
||||||
put(scr, 0, y, "TCP Connect Stats:", cDefault)
|
put(scr, 0, y, "TCP Connect Stats:", cDefault)
|
||||||
y++
|
y++
|
||||||
put(scr, hostCol, y, "Host", cDefault)
|
// header row
|
||||||
put(scr, minCol, y, " min", cDefault)
|
headerRow := fmt.Sprintf("%-*s %*s %*s %*s %*s %*s %*s",
|
||||||
put(scr, avgCol, y, " avg", cDefault)
|
hostW, "Host", numW, "last", numW, "min", numW, "avg",
|
||||||
put(scr, maxCol, y, " max", cDefault)
|
numW, "max", stdW, "stddev", nW, "n")
|
||||||
put(scr, stdCol, y, " stddev", cDefault)
|
put(scr, 0, y, headerRow, cDefault)
|
||||||
put(scr, nCol, y, " n", cDefault)
|
|
||||||
y++
|
y++
|
||||||
|
|
||||||
for _, hp := range tcpTestHosts {
|
for _, hp := range tcpTestHosts {
|
||||||
hist := sta.TCP[hp]
|
hist := st.TCP[hp]
|
||||||
if len(hist) == 0 {
|
if len(hist) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
last := hist[len(hist)-1]
|
||||||
mi, ma, av, sd := minMaxAvgStd(hist)
|
mi, ma, av, sd := minMaxAvgStd(hist)
|
||||||
drawLatencyRow(scr, y, hp, mi, av, ma, sd, len(hist))
|
|
||||||
|
row := fmt.Sprintf("%-*s %*s %*s %*s %*s %*s %*d",
|
||||||
|
hostW, hp,
|
||||||
|
numW, fmt.Sprintf("%.0fms", last),
|
||||||
|
numW, fmt.Sprintf("%.0fms", mi),
|
||||||
|
numW, fmt.Sprintf("%.0fms", av),
|
||||||
|
numW, fmt.Sprintf("%.0fms", ma),
|
||||||
|
stdW, fmt.Sprintf("%.0fms", sd),
|
||||||
|
nW, len(hist),
|
||||||
|
)
|
||||||
|
put(scr, 0, y, row, cDefault)
|
||||||
|
// colourise individual numbers
|
||||||
|
put(scr, hostW+1, y, fmt.Sprintf("%*s", numW, fmt.Sprintf("%.0fms", last)), styleLatency(last))
|
||||||
|
put(scr, hostW+1+numW+1, y, fmt.Sprintf("%*s", numW, fmt.Sprintf("%.0fms", mi)), styleLatency(mi))
|
||||||
|
put(scr, hostW+1+numW*2+2, y, fmt.Sprintf("%*s", numW, fmt.Sprintf("%.0fms", av)), styleLatency(av))
|
||||||
|
put(scr, hostW+1+numW*3+3, y, fmt.Sprintf("%*s", numW, fmt.Sprintf("%.0fms", ma)), styleLatency(ma))
|
||||||
y++
|
y++
|
||||||
}
|
}
|
||||||
y++
|
y++
|
||||||
|
|
||||||
/* totals */
|
/* totals */
|
||||||
put(scr, 0, y, fmt.Sprintf("Total ICMP Requests: %d", sta.TotalICMPReq), cDefault)
|
put(scr, 0, y, fmt.Sprintf("Total ICMP Requests: %d", st.TotalICMPReq), cDefault)
|
||||||
y++
|
y++
|
||||||
put(scr, 0, y, fmt.Sprintf("Total ICMP Replies: %d", sta.TotalICMPRep), cDefault)
|
put(scr, 0, y, fmt.Sprintf("Total ICMP Replies: %d", st.TotalICMPRep), cDefault)
|
||||||
y += 2
|
y += 2
|
||||||
sta.mu.RUnlock()
|
st.mu.RUnlock()
|
||||||
return y
|
return y
|
||||||
}
|
}
|
||||||
|
|
||||||
/*──────────────────────── loops ────────────────────────*/
|
/*────────────────── goroutine loops ─────────────*/
|
||||||
|
|
||||||
func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
||||||
tk := time.NewTicker(time.Second)
|
tk := time.NewTicker(time.Second)
|
||||||
@@ -362,35 +372,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.mu.Lock(); st.TotalICMPReq++; st.mu.Unlock()
|
||||||
st.TotalICMPReq++
|
|
||||||
st.mu.Unlock()
|
|
||||||
|
|
||||||
ok := pingOnce(st.Name, host)
|
ok := pingOnce(st.Name, host)
|
||||||
mu.Lock()
|
mu.Lock(); res[host] = ok; mu.Unlock()
|
||||||
res[host] = ok
|
|
||||||
mu.Unlock()
|
|
||||||
|
|
||||||
st.mu.Lock()
|
st.mu.Lock()
|
||||||
if ok {
|
if ok {
|
||||||
st.TotalICMPRep++
|
st.TotalICMPRep++; st.spin()
|
||||||
st.spin()
|
|
||||||
} else {
|
} else {
|
||||||
st.DroppedCount++
|
st.DroppedCount++; st.LastDrop = time.Now()
|
||||||
st.LastDrop = time.Now()
|
|
||||||
}
|
}
|
||||||
st.mu.Unlock()
|
st.mu.Unlock()
|
||||||
}(h)
|
}(h)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
st.mu.Lock()
|
st.mu.Lock(); st.Reachable = res; st.LastPing = time.Now(); st.mu.Unlock()
|
||||||
st.Reachable = res
|
|
||||||
st.LastPing = time.Now()
|
|
||||||
st.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func lossLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
func lossLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
||||||
tk := time.NewTicker(packetLossPeriod)
|
tk := time.NewTicker(packetLossPeriod)
|
||||||
defer tk.Stop()
|
defer tk.Stop()
|
||||||
@@ -407,24 +407,14 @@ func lossLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
|||||||
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(); res[host] = lp; if lp == 0 { st.spin() }; mu.Unlock()
|
||||||
res[host] = lp
|
|
||||||
if lp == 0 {
|
|
||||||
st.spin()
|
|
||||||
}
|
|
||||||
mu.Unlock()
|
|
||||||
}(h)
|
}(h)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
st.mu.Lock()
|
st.mu.Lock(); for k, v := range res { st.Loss[k] = v }; st.mu.Unlock()
|
||||||
for k, v := range res {
|
|
||||||
st.Loss[k] = v
|
|
||||||
}
|
|
||||||
st.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tcpLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
func tcpLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
||||||
tk := time.NewTicker(time.Second)
|
tk := time.NewTicker(time.Second)
|
||||||
defer tk.Stop()
|
defer tk.Stop()
|
||||||
@@ -436,27 +426,20 @@ func tcpLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
|||||||
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.spin() }
|
||||||
st.spin()
|
hist := st.TCP[hp]; if len(hist) >= statsHistory { hist = hist[1:] }
|
||||||
}
|
st.TCP[hp] = append(hist, ms); st.mu.Unlock()
|
||||||
hist := st.TCP[hp]
|
|
||||||
if len(hist) >= statsHistory {
|
|
||||||
hist = hist[1:]
|
|
||||||
}
|
|
||||||
st.TCP[hp] = append(hist, ms)
|
|
||||||
st.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*──────────────────────── UI ───────────────────────────*/
|
/*────────────────── 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()
|
||||||
tk := time.NewTicker(screenRefresh)
|
tk := time.NewTicker(screenRefresh)
|
||||||
defer tk.Stop()
|
defer tk.Stop()
|
||||||
|
|
||||||
spin := 0
|
spin := 0
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -478,7 +461,7 @@ func uiLoop(ctx context.Context, scr tcell.Screen, a, b *InterfaceStatus, start
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*──────────────────────── main ─────────────────────────*/
|
/*────────────────── main ─────────────────────────*/
|
||||||
|
|
||||||
var (
|
var (
|
||||||
reachHosts []string
|
reachHosts []string
|
||||||
@@ -505,34 +488,23 @@ func main() {
|
|||||||
}
|
}
|
||||||
a, b := newStatus(*ifaceA, *labelA), newStatus(*ifaceB, *labelB)
|
a, b := newStatus(*ifaceA, *labelA), newStatus(*ifaceB, *labelB)
|
||||||
|
|
||||||
screen, err := tcell.NewScreen()
|
scr, err := tcell.NewScreen(); if err != nil { panic(err) }
|
||||||
if err != nil {
|
if err = scr.Init(); err != nil { panic(err) }
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err = screen.Init(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// terminal Ctrl-C / SIGTERM
|
|
||||||
sig := make(chan os.Signal, 1)
|
sig := make(chan os.Signal, 1)
|
||||||
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
|
||||||
go func() { <-sig; cancel() }()
|
go func() { <-sig; cancel() }()
|
||||||
|
|
||||||
// in-app Ctrl-C / q
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
ev := screen.PollEvent()
|
if ev := scr.PollEvent(); ev != nil {
|
||||||
if ev == nil {
|
if ke, ok := ev.(*tcell.EventKey); ok {
|
||||||
return
|
if ke.Key() == tcell.KeyCtrlC || ke.Rune() == 'q' {
|
||||||
|
cancel(); return
|
||||||
}
|
}
|
||||||
switch e := ev.(type) {
|
|
||||||
case *tcell.EventKey:
|
|
||||||
if e.Key() == tcell.KeyCtrlC || e.Rune() == 'q' {
|
|
||||||
cancel()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -545,5 +517,19 @@ func main() {
|
|||||||
go tcpLoop(ctx, a, tcpTestHosts)
|
go tcpLoop(ctx, a, tcpTestHosts)
|
||||||
go tcpLoop(ctx, b, tcpTestHosts)
|
go tcpLoop(ctx, b, tcpTestHosts)
|
||||||
|
|
||||||
uiLoop(ctx, screen, a, b, time.Now())
|
uiLoop(ctx, scr, a, b, time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*────────────────── Extra packet-loss hosts ─────
|
||||||
|
|
||||||
|
To widen geographic and CDN coverage you could add:
|
||||||
|
|
||||||
|
facebook.com
|
||||||
|
microsoft.com
|
||||||
|
apple.com
|
||||||
|
twitter.com
|
||||||
|
akamai.com
|
||||||
|
|
||||||
|
These are large anycast/CDN endpoints that tend to reveal regional
|
||||||
|
network quirks. Add them to `packetLossHosts` (and `reachHosts`
|
||||||
|
if you also want individual pings every second). */
|
||||||
|
|||||||
Reference in New Issue
Block a user