Adopt repo standards: scaffold, policies, lint-clean (closes #1)
check / check (push) Failing after 0s

Add the standard scaffold and bring the tree to a clean lint under the
vendored `default: all` config: `script/` Scripts-to-Rule-Them-All
entrypoints with the `Makefile` as thin shims; a `Dockerfile` whose
`lint` and `test` phases gate the build; `.gitea/workflows/` CI running
`script/cibuild`; `REPO_POLICIES.md`, `.editorconfig`, `.dockerignore`,
`LICENSE` (WTFPL), `TODO.md`, `.gitignore`. The `.golangci.yml` is
byte-identical to the canonical copy in the `prompts` repo
(`https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml`).

The 211 lint findings were fixed, not suppressed: package globals became
functions/fields/a command constructor, magic numbers became named
constants, `ctx` threads into the probes, loop functions were split to
cut complexity. Behavior is unchanged; the log file mode stays `0644`.
Four `//nolint:gosec` remain — G204 on the fixed-argv subprocess calls,
G304 on the operator-chosen log file — matching the reference repos.

`make check` is green (lint and tests run in Docker).

Model: opus-4-8
This commit is contained in:
2026-09-21 07:13:37 +00:00
parent cf9dc39053
commit 18b27dc48c
34 changed files with 2172 additions and 884 deletions
+264 -223
View File
@@ -1,278 +1,319 @@
//go:build linux
// +build linux
package monitor
import (
"context"
crand "crypto/rand"
"math"
"math/rand"
"math/big"
"strings"
"sync"
"time"
)
// UIUpdateChan is used to signal UI updates
var UIUpdateChan = make(chan struct{}, 100)
// Probe scheduling and change-detection thresholds.
const (
jitterBaseMillis = 100
jitterRangeMillis = 800
lossChangeThreshold = 0.01
tcpChangeThreshold = 20 // milliseconds
)
// reachLoop monitors reachability for hosts
// randomOffset returns a startup jitter to avoid clustering probes at the
// same instant across loops. crypto/rand is used so no weak PRNG is linked.
func randomOffset() time.Duration {
n, err := crand.Int(crand.Reader, big.NewInt(jitterRangeMillis))
if err != nil {
return jitterBaseMillis * time.Millisecond
}
return time.Duration(jitterBaseMillis+n.Int64()) * time.Millisecond
}
// probeAll runs probe for each host concurrently and returns the results
// keyed by host.
func probeAll[T any](hosts []string, probe func(host string) T) map[string]T {
var (
wg sync.WaitGroup
mu sync.Mutex
res = make(map[string]T, len(hosts))
)
for _, h := range hosts {
wg.Add(1)
go func(host string) {
defer wg.Done()
v := probe(host)
mu.Lock()
res[host] = v
mu.Unlock()
}(h)
}
wg.Wait()
return res
}
// reachLoop monitors reachability for hosts on one interface.
func (m *Monitor) reachLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
m.logf("Starting reachability monitoring for %s with %d hosts", st.Name, len(hosts))
// Add random offset to avoid clustering at 1-second intervals
randomOffset := time.Duration(100+rand.Intn(800)) * time.Millisecond
m.logf("Reachability monitoring for %s will start after %v offset", st.Name, randomOffset)
time.Sleep(randomOffset)
m.logf("Starting reachability monitoring for %s with %d hosts",
st.Name, len(hosts))
time.Sleep(randomOffset())
tk := time.NewTicker(time.Second)
defer tk.Stop()
for {
select {
case <-ctx.Done():
m.logf("Stopping reachability monitoring for %s", st.Name)
return
case <-tk.C:
var wg sync.WaitGroup
res := make(map[string]bool, len(hosts))
mu := sync.Mutex{}
for _, h := range hosts {
wg.Add(1)
go func(host string) {
defer wg.Done()
st.mu.Lock()
st.TotalICMPReq++
// Increase meter value when a packet is sent
st.MeterValue++
if st.MeterValue > m.MaxMeterValue {
st.MeterValue = m.MaxMeterValue
}
st.mu.Unlock()
ok := m.pingOnce(st.Name, host)
mu.Lock()
res[host] = ok
mu.Unlock()
st.mu.Lock()
if ok {
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()
} else {
st.DroppedCount++
st.LastDrop = time.Now()
// Track lost packets per host
st.LostPackets[host]++
// Trigger UI update on ping failure
select {
case UIUpdateChan <- struct{}{}:
default:
}
}
st.mu.Unlock()
}(h)
}
wg.Wait()
// Check if reachability status changed
statusChanged := false
st.mu.Lock()
for host, newStatus := range res {
if oldStatus, ok := st.Reachable[host]; !ok || oldStatus != newStatus {
statusChanged = true
break
}
}
st.Reachable = res
st.LastPing = time.Now()
// Check if all hosts are reachable
allReachable := true
for _, ok := range res {
if !ok {
allReachable = false
break
}
}
// If all hosts are reachable, gradually decay the meter value
if allReachable && st.MeterValue > 0 {
st.MeterValue--
}
st.mu.Unlock()
// Always trigger UI update when reachability status changes
if statusChanged {
select {
case UIUpdateChan <- struct{}{}:
default:
}
}
m.reachTick(ctx, st, hosts)
}
}
}
// lossLoop monitors packet loss for hosts
func (m *Monitor) lossLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
m.logf("Starting packet loss monitoring for %s with %d hosts", st.Name, len(hosts))
// reachTick pings every host concurrently and applies the results.
func (m *Monitor) reachTick(ctx context.Context, st *InterfaceStatus, hosts []string) {
res := probeAll(hosts, func(host string) bool {
return m.reachProbe(ctx, st, host)
})
m.reachApply(st, res)
}
// Add random offset to avoid clustering at periodic intervals
randomOffset := time.Duration(100+rand.Intn(800)) * time.Millisecond
m.logf("Packet loss monitoring for %s will start after %v offset", st.Name, randomOffset)
time.Sleep(randomOffset)
// reachProbe pings a single host and updates per-host counters.
func (m *Monitor) reachProbe(
ctx context.Context, st *InterfaceStatus, host string,
) bool {
st.mu.Lock()
st.TotalICMPReq++
st.MeterValue = min(st.MeterValue+1, m.MaxMeterValue)
st.mu.Unlock()
ok := m.pingOnce(ctx, st.Name, host)
st.mu.Lock()
defer st.mu.Unlock()
if ok {
st.TotalICMPRep++
st.MeterValue = max(st.MeterValue-1, 0)
st.Spin()
return true
}
st.DroppedCount++
st.LastDrop = time.Now()
st.LostPackets[host]++
m.notifyUI()
return false
}
// reachApply stores the round's results and decays the meter when clean.
func (m *Monitor) reachApply(st *InterfaceStatus, res map[string]bool) {
st.mu.Lock()
changed := reachChanged(st.Reachable, res)
st.Reachable = res
st.LastPing = time.Now()
if allReachable(res) && st.MeterValue > 0 {
st.MeterValue--
}
st.mu.Unlock()
if changed {
m.notifyUI()
}
}
// reachChanged reports whether any host's reachability differs from before.
func reachChanged(old, cur map[string]bool) bool {
for host, newStatus := range cur {
if oldStatus, ok := old[host]; !ok || oldStatus != newStatus {
return true
}
}
return false
}
// allReachable reports whether every host in the map is reachable.
func allReachable(res map[string]bool) bool {
for _, ok := range res {
if !ok {
return false
}
}
return true
}
// lossLoop monitors packet loss for hosts on one interface.
func (m *Monitor) lossLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
m.logf("Starting packet loss monitoring for %s with %d hosts",
st.Name, len(hosts))
time.Sleep(randomOffset())
tk := time.NewTicker(m.PacketLossPeriod)
defer tk.Stop()
for {
select {
case <-ctx.Done():
m.logf("Stopping packet loss monitoring for %s", st.Name)
return
case <-tk.C:
var wg sync.WaitGroup
res := make(map[string]float64, len(hosts))
mu := sync.Mutex{}
for _, h := range hosts {
wg.Add(1)
go func(host string) {
defer wg.Done()
lp := m.lossPercent(st.Name, host)
mu.Lock()
res[host] = lp
mu.Unlock()
st.mu.Lock()
if lp == 0 {
// Only update spinner when there's 0% packet loss
st.Spin()
} else {
// Calculate approximate number of lost packets based on loss percentage
lostPackets := int(math.Ceil(float64(m.PacketLossPings) * lp))
// Update dropped count with the number of lost packets
st.DroppedCount += lostPackets
// Update last drop time if packets were lost
if lostPackets > 0 {
st.LastDrop = time.Now()
}
// Track lost packets per host
st.LostPackets[host] += lostPackets
// Trigger UI update on packet loss
select {
case UIUpdateChan <- struct{}{}:
default:
}
}
st.mu.Unlock()
}(h)
}
wg.Wait()
// Check if loss status changed
statusChanged := false
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 {
st.Loss[k] = v
}
st.mu.Unlock()
// Always trigger UI update when loss status changes
if statusChanged {
select {
case UIUpdateChan <- struct{}{}:
default:
}
}
m.lossTick(ctx, st, hosts)
}
}
}
// tcpLoop monitors TCP connectivity for hosts
// lossTick measures loss for every host concurrently and applies results.
func (m *Monitor) lossTick(ctx context.Context, st *InterfaceStatus, hosts []string) {
res := probeAll(hosts, func(host string) float64 {
return m.lossProbe(ctx, st, host)
})
m.lossApply(st, res)
}
// lossProbe measures loss for a host and updates per-host counters.
func (m *Monitor) lossProbe(
ctx context.Context, st *InterfaceStatus, host string,
) float64 {
lp := m.lossPercent(ctx, st.Name, host)
st.mu.Lock()
defer st.mu.Unlock()
if lp == 0 {
st.Spin()
return lp
}
lostPackets := int(math.Ceil(float64(m.PacketLossPings) * lp))
st.DroppedCount += lostPackets
if lostPackets > 0 {
st.LastDrop = time.Now()
}
st.LostPackets[host] += lostPackets
m.notifyUI()
return lp
}
// lossApply stores the round's loss results.
func (m *Monitor) lossApply(st *InterfaceStatus, res map[string]float64) {
st.mu.Lock()
changed := lossChanged(st.Loss, res)
for k, v := range res {
st.Loss[k] = v
}
st.mu.Unlock()
if changed {
m.notifyUI()
}
}
// lossChanged reports whether any host's loss moved beyond the threshold.
func lossChanged(old, cur map[string]float64) bool {
for host, newLoss := range cur {
if oldLoss, ok := old[host]; !ok || math.Abs(oldLoss-newLoss) > lossChangeThreshold {
return true
}
}
return false
}
// tcpLoop monitors TCP connectivity for hosts on one interface.
func (m *Monitor) tcpLoop(ctx context.Context, st *InterfaceStatus, hosts []string) {
m.logf("Starting TCP monitoring for %s with %d hosts", st.Name, len(hosts))
// Add random offset to avoid clustering at 1-second intervals
randomOffset := time.Duration(100+rand.Intn(800)) * time.Millisecond
m.logf("TCP monitoring for %s will start after %v offset", st.Name, randomOffset)
time.Sleep(randomOffset)
time.Sleep(randomOffset())
tk := time.NewTicker(time.Second)
defer tk.Stop()
for {
select {
case <-ctx.Done():
m.logf("Stopping TCP monitoring for %s", st.Name)
return
case <-tk.C:
statusChanged := false
for _, hp := range hosts {
ms := float64(m.tcpDuration(st.Name, hp).Milliseconds())
st.mu.Lock()
// Check if TCP latency significantly changed
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(m.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) >= m.StatsHistory {
hist = hist[1:]
}
st.TCP[hp] = append(hist, ms)
// Update lost packets for the host (without port)
hostName := strings.Split(hp, ":")[0]
if ms >= float64(m.TCPTimeout.Milliseconds()) {
st.LostPackets[hostName]++
}
st.mu.Unlock()
}
// Always trigger UI update when TCP status changes significantly
if statusChanged {
select {
case UIUpdateChan <- struct{}{}:
default:
}
}
m.tcpTick(st, hosts)
}
}
}
// tcpTick measures TCP latency for each host and redraws on change.
func (m *Monitor) tcpTick(st *InterfaceStatus, hosts []string) {
changed := false
for _, hp := range hosts {
if m.tcpProbe(st, hp) {
changed = true
}
}
if changed {
m.notifyUI()
}
}
// tcpProbe measures one host's latency, records it, and reports whether the
// latency changed significantly.
func (m *Monitor) tcpProbe(st *InterfaceStatus, hp string) bool {
ms := float64(m.tcpDuration(st.Name, hp).Milliseconds())
st.mu.Lock()
defer st.mu.Unlock()
hist := st.TCP[hp]
changed := tcpSignificant(hist, ms)
if ms < float64(m.TCPTimeout.Milliseconds()) {
st.Spin()
} else {
m.notifyUI()
}
if len(hist) >= m.StatsHistory {
hist = hist[1:]
}
st.TCP[hp] = append(hist, ms)
host := strings.Split(hp, ":")[0]
if ms >= float64(m.TCPTimeout.Milliseconds()) {
st.LostPackets[host]++
}
return changed
}
// tcpSignificant reports whether ms differs meaningfully from the last
// sample (or there is no prior sample).
func tcpSignificant(hist []float64, ms float64) bool {
if len(hist) == 0 {
return true
}
return math.Abs(hist[len(hist)-1]-ms) > tcpChangeThreshold
}