higher frame rate, PL meter
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
//go:build linux
|
||||||
|
// +build linux
|
||||||
|
|
||||||
// netmon – dual-interface network dashboard (curses)
|
// netmon – dual-interface network dashboard (curses)
|
||||||
// WTFPL – 2025-05-16 sneak@sneak.berlin
|
// WTFPL – 2025-05-16 sneak@sneak.berlin
|
||||||
package main
|
package main
|
||||||
@@ -34,6 +37,8 @@ var (
|
|||||||
"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",
|
||||||
"comma-separated reachability hosts")
|
"comma-separated reachability hosts")
|
||||||
|
|
||||||
|
logFile = flag.String("logfile", "/tmp/rtnetmon.log", "path to log file")
|
||||||
)
|
)
|
||||||
|
|
||||||
/*────────────────── constants ──────────────────*/
|
/*────────────────── constants ──────────────────*/
|
||||||
@@ -44,7 +49,14 @@ const (
|
|||||||
packetLossPings = 20
|
packetLossPings = 20
|
||||||
packetLossPeriod = 5 * time.Second
|
packetLossPeriod = 5 * time.Second
|
||||||
statsHistory = 300
|
statsHistory = 300
|
||||||
screenRefresh = 500 * time.Millisecond
|
screenRefresh = 500 * time.Millisecond // Still used as backup refresh rate
|
||||||
|
|
||||||
|
// Unicode characters for the meter
|
||||||
|
greenDot = '🟢' // Unicode green circle
|
||||||
|
redDot = '🔴' // Unicode red circle
|
||||||
|
emptyDot = '⚪' // Unicode white circle
|
||||||
|
meterWidth = 6 // Fixed width of the meter
|
||||||
|
maxMeterValue = 10 // Maximum value for the meter
|
||||||
)
|
)
|
||||||
|
|
||||||
/*────────────────── runtime struct ─────────────*/
|
/*────────────────── runtime struct ─────────────*/
|
||||||
@@ -58,6 +70,7 @@ type InterfaceStatus struct {
|
|||||||
DroppedCount int
|
DroppedCount int
|
||||||
LastDrop, LastPing time.Time
|
LastDrop, LastPing time.Time
|
||||||
SpinFrame int
|
SpinFrame int
|
||||||
|
MeterValue int // Current value for the packet loss meter
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +213,21 @@ func tcpDuration(iface, hp string) time.Duration {
|
|||||||
|
|
||||||
var spins = []rune{'|', '/', '-', '\\'}
|
var spins = []rune{'|', '/', '-', '\\'}
|
||||||
|
|
||||||
func (st *InterfaceStatus) spin() { st.SpinFrame = (st.SpinFrame + 1) % len(spins) }
|
// Update to add a channel for signaling UI updates
|
||||||
|
var uiUpdateChan = make(chan struct{}, 100) // Buffered channel to avoid blocking
|
||||||
|
|
||||||
|
// Advances the spinner frame and signals a UI update
|
||||||
|
// This is called only when packets are successfully received,
|
||||||
|
// so the spinner only moves when there's actual network activity
|
||||||
|
func (st *InterfaceStatus) spin() {
|
||||||
|
st.SpinFrame = (st.SpinFrame + 1) % len(spins)
|
||||||
|
// Signal UI update after spinner changes
|
||||||
|
select {
|
||||||
|
case uiUpdateChan <- struct{}{}:
|
||||||
|
default:
|
||||||
|
// Non-blocking send - if channel is full, just continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*────────────────── screen helpers ─────────────*/
|
/*────────────────── screen helpers ─────────────*/
|
||||||
|
|
||||||
@@ -257,9 +284,16 @@ func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int {
|
|||||||
st.mu.RLock()
|
st.mu.RLock()
|
||||||
header := fmt.Sprintf("%s: %s — %s", st.Name, st.Label, st.IPInfo)
|
header := fmt.Sprintf("%s: %s — %s", st.Name, st.Label, st.IPInfo)
|
||||||
spin := spins[st.SpinFrame]
|
spin := spins[st.SpinFrame]
|
||||||
|
meterValue := st.MeterValue
|
||||||
st.mu.RUnlock()
|
st.mu.RUnlock()
|
||||||
|
|
||||||
|
// Create the meter - fixed width of 6 characters
|
||||||
|
meter := createMeter(meterValue, meterWidth)
|
||||||
|
|
||||||
put(scr, 0, y+1, "== ", cDefault)
|
put(scr, 0, y+1, "== ", cDefault)
|
||||||
put(scr, 3, y+1, string(spin)+" "+header, style)
|
put(scr, 3, y+1, string(spin)+" ", cDefault)
|
||||||
|
put(scr, 5, y+1, meter+" ", cDefault) // Add the meter after the spinner
|
||||||
|
put(scr, 5+meterWidth+1, y+1, header, style) // Move the header after the meter
|
||||||
put(scr, 0, y+2, hline(w), cDefault)
|
put(scr, 0, y+2, hline(w), cDefault)
|
||||||
y += 4
|
y += 4
|
||||||
|
|
||||||
@@ -356,16 +390,51 @@ func drawIface(scr tcell.Screen, y, w int, st *InterfaceStatus) int {
|
|||||||
return y
|
return y
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a visual meter using green and red dots
|
||||||
|
func createMeter(value, width int) string {
|
||||||
|
if value > maxMeterValue {
|
||||||
|
value = maxMeterValue
|
||||||
|
}
|
||||||
|
if value < 0 {
|
||||||
|
value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate how many dots of each color to show
|
||||||
|
redDots := value
|
||||||
|
if redDots > width {
|
||||||
|
redDots = width
|
||||||
|
}
|
||||||
|
greenDots := width - redDots
|
||||||
|
|
||||||
|
// Build the meter string
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
// Add red dots for unreplied packets
|
||||||
|
for i := 0; i < redDots; i++ {
|
||||||
|
b.WriteRune(redDot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add green dots for available capacity
|
||||||
|
for i := 0; i < greenDots; i++ {
|
||||||
|
b.WriteRune(greenDot)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
/*────────────────── goroutine loops ─────────────*/
|
/*────────────────── goroutine loops ─────────────*/
|
||||||
|
|
||||||
func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
||||||
|
logf("Starting reachability monitoring for %s with %d hosts", st.Name, len(hosts))
|
||||||
tk := time.NewTicker(time.Second)
|
tk := time.NewTicker(time.Second)
|
||||||
defer tk.Stop()
|
defer tk.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
logf("Stopping reachability monitoring for %s", st.Name)
|
||||||
return
|
return
|
||||||
case <-tk.C:
|
case <-tk.C:
|
||||||
|
// logf("Checking reachability for %s", st.Name) // Too verbose
|
||||||
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{}
|
||||||
@@ -375,6 +444,11 @@ func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
|||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
st.mu.Lock()
|
st.mu.Lock()
|
||||||
st.TotalICMPReq++
|
st.TotalICMPReq++
|
||||||
|
// Increase meter value when a packet is sent
|
||||||
|
st.MeterValue++
|
||||||
|
if st.MeterValue > maxMeterValue {
|
||||||
|
st.MeterValue = maxMeterValue
|
||||||
|
}
|
||||||
st.mu.Unlock()
|
st.mu.Unlock()
|
||||||
|
|
||||||
ok := pingOnce(st.Name, host)
|
ok := pingOnce(st.Name, host)
|
||||||
@@ -385,18 +459,41 @@ func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
|||||||
st.mu.Lock()
|
st.mu.Lock()
|
||||||
if ok {
|
if ok {
|
||||||
st.TotalICMPRep++
|
st.TotalICMPRep++
|
||||||
|
// Decrease meter value when a packet is successfully received
|
||||||
|
st.MeterValue--
|
||||||
|
if st.MeterValue < 0 {
|
||||||
|
st.MeterValue = 0
|
||||||
|
}
|
||||||
|
// Only update spinner when packets are successfully received
|
||||||
st.spin()
|
st.spin()
|
||||||
} else {
|
} else {
|
||||||
st.DroppedCount++
|
st.DroppedCount++
|
||||||
st.LastDrop = time.Now()
|
st.LastDrop = time.Now()
|
||||||
|
// For failed pings, we don't decrease the meter value
|
||||||
|
// Trigger UI update on ping failure
|
||||||
|
select {
|
||||||
|
case uiUpdateChan <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
st.mu.Unlock()
|
st.mu.Unlock()
|
||||||
}(h)
|
}(h)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
|
// Check if reachability status changed
|
||||||
|
statusChanged := false
|
||||||
st.mu.Lock()
|
st.mu.Lock()
|
||||||
|
for host, newStatus := range res {
|
||||||
|
if oldStatus, ok := st.Reachable[host]; !ok || oldStatus != newStatus {
|
||||||
|
statusChanged = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
st.Reachable = res
|
st.Reachable = res
|
||||||
st.LastPing = time.Now()
|
st.LastPing = time.Now()
|
||||||
|
|
||||||
// Reset DroppedCount if all hosts are reachable
|
// Reset DroppedCount if all hosts are reachable
|
||||||
allReachable := true
|
allReachable := true
|
||||||
for _, ok := range res {
|
for _, ok := range res {
|
||||||
@@ -407,19 +504,34 @@ func reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
|||||||
}
|
}
|
||||||
if allReachable {
|
if allReachable {
|
||||||
st.DroppedCount = 0
|
st.DroppedCount = 0
|
||||||
|
// If all hosts are reachable, gradually decay the meter value
|
||||||
|
if st.MeterValue > 0 {
|
||||||
|
st.MeterValue--
|
||||||
|
}
|
||||||
}
|
}
|
||||||
st.mu.Unlock()
|
st.mu.Unlock()
|
||||||
|
|
||||||
|
// Always trigger UI update when reachability status changes
|
||||||
|
if statusChanged {
|
||||||
|
select {
|
||||||
|
case uiUpdateChan <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func lossLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
func lossLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
||||||
|
logf("Starting packet loss monitoring for %s with %d hosts", st.Name, len(hosts))
|
||||||
tk := time.NewTicker(packetLossPeriod)
|
tk := time.NewTicker(packetLossPeriod)
|
||||||
defer tk.Stop()
|
defer tk.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
logf("Stopping packet loss monitoring for %s", st.Name)
|
||||||
return
|
return
|
||||||
case <-tk.C:
|
case <-tk.C:
|
||||||
|
// logf("Checking packet loss for %s", st.Name) // Too verbose
|
||||||
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{}
|
||||||
@@ -430,42 +542,103 @@ 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 == 0 {
|
|
||||||
st.spin()
|
|
||||||
}
|
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|
||||||
|
st.mu.Lock()
|
||||||
|
if lp == 0 {
|
||||||
|
// Only update spinner when there's 0% packet loss (successful receipt)
|
||||||
|
st.spin()
|
||||||
|
} else {
|
||||||
|
// Trigger UI update on packet loss
|
||||||
|
select {
|
||||||
|
case uiUpdateChan <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
st.mu.Unlock()
|
||||||
}(h)
|
}(h)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
|
// Check if loss status changed
|
||||||
|
statusChanged := false
|
||||||
st.mu.Lock()
|
st.mu.Lock()
|
||||||
|
for host, newLoss := range res {
|
||||||
|
if oldLoss, ok := st.Loss[host]; !ok || math.Abs(oldLoss-newLoss) > 0.01 {
|
||||||
|
statusChanged = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for k, v := range res {
|
for k, v := range res {
|
||||||
st.Loss[k] = v
|
st.Loss[k] = v
|
||||||
}
|
}
|
||||||
st.mu.Unlock()
|
st.mu.Unlock()
|
||||||
|
|
||||||
|
// Always trigger UI update when loss status changes
|
||||||
|
if statusChanged {
|
||||||
|
select {
|
||||||
|
case uiUpdateChan <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func tcpLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
func tcpLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
||||||
|
logf("Starting TCP monitoring for %s with %d hosts", st.Name, len(hosts))
|
||||||
tk := time.NewTicker(time.Second)
|
tk := time.NewTicker(time.Second)
|
||||||
defer tk.Stop()
|
defer tk.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
logf("Stopping TCP monitoring for %s", st.Name)
|
||||||
return
|
return
|
||||||
case <-tk.C:
|
case <-tk.C:
|
||||||
|
// logf("Checking TCP for %s", st.Name) // Too verbose
|
||||||
|
statusChanged := false
|
||||||
|
|
||||||
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.spin()
|
// Check if TCP latency significantly changed
|
||||||
}
|
|
||||||
hist := st.TCP[hp]
|
hist := st.TCP[hp]
|
||||||
|
if len(hist) > 0 {
|
||||||
|
lastMs := hist[len(hist)-1]
|
||||||
|
if math.Abs(lastMs-ms) > 20 { // 20ms threshold for significant change
|
||||||
|
statusChanged = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// First measurement
|
||||||
|
statusChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ms < float64(tcpTimeout.Milliseconds()) {
|
||||||
|
// Only update spinner on successful TCP connections
|
||||||
|
st.spin()
|
||||||
|
} else {
|
||||||
|
// Trigger UI update on TCP timeout
|
||||||
|
select {
|
||||||
|
case uiUpdateChan <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(hist) >= statsHistory {
|
if len(hist) >= statsHistory {
|
||||||
hist = hist[1:]
|
hist = hist[1:]
|
||||||
}
|
}
|
||||||
st.TCP[hp] = append(hist, ms)
|
st.TCP[hp] = append(hist, ms)
|
||||||
st.mu.Unlock()
|
st.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always trigger UI update when TCP status changes significantly
|
||||||
|
if statusChanged {
|
||||||
|
select {
|
||||||
|
case uiUpdateChan <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -473,15 +646,22 @@ func tcpLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
|
|||||||
/*────────────────── UI loop ─────────────────────*/
|
/*────────────────── 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()
|
logf("UI loop started")
|
||||||
tk := time.NewTicker(screenRefresh)
|
defer func() {
|
||||||
defer tk.Stop()
|
logf("UI loop cleanup")
|
||||||
|
scr.Clear()
|
||||||
|
scr.ShowCursor(0, 0)
|
||||||
|
scr.Fini()
|
||||||
|
logf("Screen finalized")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Remove the ticker as we'll update on spinner ticks
|
||||||
|
// tk := time.NewTicker(screenRefresh)
|
||||||
|
// defer tk.Stop()
|
||||||
spin := 0
|
spin := 0
|
||||||
for {
|
|
||||||
select {
|
// Function to draw the screen
|
||||||
case <-ctx.Done():
|
drawScreen := func() {
|
||||||
return
|
|
||||||
case <-tk.C:
|
|
||||||
w, _ := scr.Size()
|
w, _ := scr.Size()
|
||||||
scr.Clear()
|
scr.Clear()
|
||||||
put(scr, 0, 0, hline(w), cDefault)
|
put(scr, 0, 0, hline(w), cDefault)
|
||||||
@@ -494,6 +674,27 @@ func uiLoop(ctx context.Context, scr tcell.Screen, a, b *InterfaceStatus, start
|
|||||||
_ = drawIface(scr, y, w, b)
|
_ = drawIface(scr, y, w, b)
|
||||||
scr.Show()
|
scr.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initial draw
|
||||||
|
drawScreen()
|
||||||
|
|
||||||
|
// Even without a ticker, ensure we update at least every second
|
||||||
|
// This is a backup in case there are no spinner updates
|
||||||
|
backupTicker := time.NewTicker(time.Second)
|
||||||
|
defer backupTicker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
logf("Context cancelled, exiting UI loop")
|
||||||
|
return
|
||||||
|
case <-uiUpdateChan:
|
||||||
|
// Update on spinner ticks (no rate limiting)
|
||||||
|
drawScreen()
|
||||||
|
case <-backupTicker.C:
|
||||||
|
// Fallback to ensure we update at least once per second
|
||||||
|
drawScreen()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,11 +709,31 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Simple logging function
|
||||||
|
func logf(format string, v ...interface{}) {
|
||||||
|
if logFile == nil || *logFile == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f, err := os.OpenFile(*logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return // silently fail if we can't log
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
fmt.Fprintf(f, time.Now().Format("2006-01-02 15:04:05.000 ")+format+"\n", v...)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
logf("Starting rtnetmon")
|
||||||
reachHosts = strings.Split(*hostCSV, ",")
|
reachHosts = strings.Split(*hostCSV, ",")
|
||||||
|
logf("Monitoring interfaces %s and %s", *ifaceA, *ifaceB)
|
||||||
|
|
||||||
|
// Initialize UI update channel
|
||||||
|
uiUpdateChan = make(chan struct{}, 100)
|
||||||
|
logf("UI update channel initialized with buffer size 100")
|
||||||
|
|
||||||
newStatus := func(name, label string) *InterfaceStatus {
|
newStatus := func(name, label string) *InterfaceStatus {
|
||||||
|
logf("Initializing status for interface %s", name)
|
||||||
return &InterfaceStatus{
|
return &InterfaceStatus{
|
||||||
Name: name,
|
Name: name,
|
||||||
Label: label,
|
Label: label,
|
||||||
@@ -520,38 +741,60 @@ func main() {
|
|||||||
Reachable: map[string]bool{},
|
Reachable: map[string]bool{},
|
||||||
Loss: map[string]float64{},
|
Loss: map[string]float64{},
|
||||||
TCP: map[string][]float64{},
|
TCP: map[string][]float64{},
|
||||||
|
MeterValue: 0, // Initialize meter value to 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a, b := newStatus(*ifaceA, *labelA), newStatus(*ifaceB, *labelB)
|
a, b := newStatus(*ifaceA, *labelA), newStatus(*ifaceB, *labelB)
|
||||||
|
|
||||||
|
logf("Initializing screen")
|
||||||
scr, err := tcell.NewScreen()
|
scr, err := tcell.NewScreen()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logf("Error creating screen: %v", err)
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if err = scr.Init(); err != nil {
|
if err = scr.Init(); err != nil {
|
||||||
|
logf("Error initializing screen: %v", err)
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
logf("Screen initialized")
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
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() {
|
go func() {
|
||||||
|
s := <-sig
|
||||||
|
logf("Signal received: %v", s)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Event polling loop with logging
|
||||||
|
go func() {
|
||||||
|
logf("Starting keyboard event loop")
|
||||||
for {
|
for {
|
||||||
if ev := scr.PollEvent(); ev != nil {
|
if ev := scr.PollEvent(); ev != nil {
|
||||||
|
logf("Event received: %T", ev)
|
||||||
if ke, ok := ev.(*tcell.EventKey); ok {
|
if ke, ok := ev.(*tcell.EventKey); ok {
|
||||||
|
logf("Key event: %v, rune: %c", ke.Key(), ke.Rune())
|
||||||
if ke.Key() == tcell.KeyCtrlC || ke.Rune() == 'q' {
|
if ke.Key() == tcell.KeyCtrlC || ke.Rune() == 'q' {
|
||||||
|
logf("Quit key detected")
|
||||||
cancel()
|
cancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trigger UI update on any event
|
||||||
|
select {
|
||||||
|
case uiUpdateChan <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Start monitoring goroutines
|
||||||
|
logf("Starting monitoring goroutines")
|
||||||
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)
|
||||||
@@ -559,7 +802,9 @@ func main() {
|
|||||||
go tcpLoop(ctx, a, tcpTestHosts)
|
go tcpLoop(ctx, a, tcpTestHosts)
|
||||||
go tcpLoop(ctx, b, tcpTestHosts)
|
go tcpLoop(ctx, b, tcpTestHosts)
|
||||||
|
|
||||||
|
logf("Starting UI loop")
|
||||||
uiLoop(ctx, scr, a, b, time.Now())
|
uiLoop(ctx, scr, a, b, time.Now())
|
||||||
|
logf("UI loop exited, program ending")
|
||||||
}
|
}
|
||||||
|
|
||||||
/*────────────────── Extra packet-loss hosts ─────
|
/*────────────────── Extra packet-loss hosts ─────
|
||||||
|
|||||||
Reference in New Issue
Block a user