mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
206 lines
5.7 KiB
Go
206 lines
5.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"maps"
|
|
"slices"
|
|
"sync"
|
|
"tailscale.com/net/netmon"
|
|
"time"
|
|
)
|
|
|
|
// noiseSummaryInterval bounds a storm of noise deltas to one journal line,
|
|
// because AirDrop and virtual adapters can churn for hours.
|
|
const noiseSummaryInterval = 10 * time.Minute
|
|
|
|
// wakeReportInterval merges the reports of the wake sources. netmon polls for
|
|
// a time jump every 15 s, so one wake can reach ctrld over half a minute.
|
|
const wakeReportInterval = 30 * time.Second
|
|
|
|
// repeatLogger holds the last value of each named line, so a line that
|
|
// repeats every few seconds reaches the log on change only.
|
|
type repeatLogger struct {
|
|
mu sync.Mutex
|
|
last map[string]string
|
|
repeats map[string]uint64
|
|
}
|
|
|
|
// changed reports whether the value of a key differs from the value before it.
|
|
// The repeat count tells how many lines the caller held back, so the next line
|
|
// it writes can carry them.
|
|
func (r *repeatLogger) changed(key, value string) (changed bool, repeats uint64) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.last == nil {
|
|
r.last = make(map[string]string)
|
|
r.repeats = make(map[string]uint64)
|
|
}
|
|
if previous, seen := r.last[key]; seen && previous == value {
|
|
r.repeats[key]++
|
|
return false, r.repeats[key]
|
|
}
|
|
repeats = r.repeats[key]
|
|
r.last[key] = value
|
|
r.repeats[key] = 0
|
|
return true, repeats
|
|
}
|
|
|
|
// wakeHoldInterval is the time that a wake report without a gap waits for the
|
|
// detector. The detector ticks every two seconds, so it reports well inside
|
|
// this hold when it runs.
|
|
const wakeHoldInterval = 8 * time.Second
|
|
|
|
// wakeReport is one wake as a source saw it.
|
|
type wakeReport struct {
|
|
source string
|
|
gap time.Duration
|
|
state *netmon.State
|
|
}
|
|
|
|
// wakeReporter keeps one report per wake. netmon measures no gap, so its
|
|
// report waits for the detector, which measures one. When no detector report
|
|
// comes, the held report goes out as it is.
|
|
type wakeReporter struct {
|
|
mu sync.Mutex
|
|
last time.Time
|
|
held *wakeReport
|
|
// after arms the hold timer. nil means time.AfterFunc, and a test fires
|
|
// the hold itself.
|
|
after func(time.Duration, func())
|
|
}
|
|
|
|
// note takes one report of a wake and hands the report that the journal gets
|
|
// to emit. The first source of a window reports. A report with a gap replaces
|
|
// a held report without one. Every other report of the window is the same
|
|
// wake and drops.
|
|
func (w *wakeReporter) note(now time.Time, report wakeReport, emit func(wakeReport)) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
gapKnown := report.gap > 0
|
|
if !w.last.IsZero() && now.Sub(w.last) < wakeReportInterval {
|
|
if w.held != nil && gapKnown {
|
|
w.held = nil
|
|
emit(report)
|
|
}
|
|
return
|
|
}
|
|
w.last = now
|
|
if gapKnown {
|
|
w.held = nil
|
|
emit(report)
|
|
return
|
|
}
|
|
held := &wakeReport{source: report.source, gap: report.gap, state: report.state}
|
|
w.held = held
|
|
w.afterFunc()(wakeHoldInterval, func() { w.release(held, emit) })
|
|
}
|
|
|
|
// release emits a held report when no source with a gap replaced it.
|
|
func (w *wakeReporter) release(held *wakeReport, emit func(wakeReport)) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if w.held != held {
|
|
return
|
|
}
|
|
w.held = nil
|
|
emit(*held)
|
|
}
|
|
|
|
func (w *wakeReporter) afterFunc() func(time.Duration, func()) {
|
|
if w.after != nil {
|
|
return w.after
|
|
}
|
|
return func(d time.Duration, fn func()) { time.AfterFunc(d, fn) }
|
|
}
|
|
|
|
// noiseSummary describes the noise deltas that one journal line covers.
|
|
type noiseSummary struct {
|
|
Count int
|
|
First time.Time
|
|
Last time.Time
|
|
Interfaces []string
|
|
}
|
|
|
|
// noiseCoalescer counts the noise deltas of a storm and reports when the
|
|
// journal needs a line for them. The count, the first time, and the names
|
|
// cover the open window; last covers the storm, so a quiet period ends it.
|
|
type noiseCoalescer struct {
|
|
mu sync.Mutex
|
|
count int
|
|
first time.Time
|
|
last time.Time
|
|
emitted time.Time
|
|
names map[string]struct{}
|
|
}
|
|
|
|
// add records one noise delta and reports whether to log a summary now. The
|
|
// summary covers the deltas since the last one, so a storm of any length
|
|
// keeps to one line per interval.
|
|
func (c *noiseCoalescer) add(interfaces []string, now time.Time) (summary noiseSummary, emit bool) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
newRun := c.stormEnded(now)
|
|
if newRun {
|
|
c.clearWindow()
|
|
}
|
|
c.record(interfaces, now)
|
|
if !newRun && now.Sub(c.emitted) < noiseSummaryInterval {
|
|
return noiseSummary{}, false
|
|
}
|
|
summary = c.summary()
|
|
c.emitted = now
|
|
c.clearWindow()
|
|
return summary, true
|
|
}
|
|
|
|
// flush reports the open window and closes it. A storm ends with a delta that
|
|
// the daemon acts on, so the last window of the storm reaches the journal
|
|
// there instead of waiting for the next storm.
|
|
func (c *noiseCoalescer) flush(now time.Time) (summary noiseSummary, emit bool) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.count == 0 {
|
|
return noiseSummary{}, false
|
|
}
|
|
summary = c.summary()
|
|
c.emitted = now
|
|
c.clearWindow()
|
|
return summary, true
|
|
}
|
|
|
|
// stormEnded is true when the deltas stopped for longer than the interval, so
|
|
// a new storm does not inherit the count of the storm before it.
|
|
func (c *noiseCoalescer) stormEnded(now time.Time) bool {
|
|
return c.last.IsZero() || now.Sub(c.last) > noiseSummaryInterval
|
|
}
|
|
|
|
func (c *noiseCoalescer) record(interfaces []string, now time.Time) {
|
|
if c.count == 0 {
|
|
c.first = now
|
|
}
|
|
c.count++
|
|
c.last = now
|
|
if c.names == nil {
|
|
c.names = make(map[string]struct{})
|
|
}
|
|
for _, name := range interfaces {
|
|
c.names[name] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// summary sorts the names, because a log line that keeps its order reads as
|
|
// one value across a capture.
|
|
func (c *noiseCoalescer) summary() noiseSummary {
|
|
return noiseSummary{
|
|
Count: c.count,
|
|
First: c.first,
|
|
Last: c.last,
|
|
Interfaces: slices.Sorted(maps.Keys(c.names)),
|
|
}
|
|
}
|
|
|
|
func (c *noiseCoalescer) clearWindow() {
|
|
c.count = 0
|
|
c.first = time.Time{}
|
|
c.names = nil
|
|
}
|