diff --git a/wgengine/monitor/monitor.go b/wgengine/monitor/monitor.go index 3d88a9a8b..8ee7087ce 100644 --- a/wgengine/monitor/monitor.go +++ b/wgengine/monitor/monitor.go @@ -10,6 +10,7 @@ package monitor import ( "encoding/json" "errors" + "runtime" "sync" "time" @@ -18,6 +19,13 @@ import ( "tailscale.com/types/logger" ) +// pollWallTimeInterval is how often we check the time to check +// for big jumps in wall (non-monotonic) time as a backup mechanism +// to get notified of a sleeping device waking back up. +// Usually there are also minor network change events on wake that let +// us check the wall time sooner than this. +const pollWallTimeInterval = 15 * time.Second + // message represents a message returned from an osMon. type message interface { // Ignore is whether we should ignore this message. @@ -50,18 +58,20 @@ type Mon struct { logf logger.Logf om osMon // nil means not supported on this platform change chan struct{} - stop chan struct{} - - mu sync.Mutex // guards cbs - cbs map[*callbackHandle]ChangeFunc - ifState *interfaces.State - gwValid bool // whether gw and gwSelfIP are valid (cached)x - gw netaddr.IP - gwSelfIP netaddr.IP + stop chan struct{} // closed on Stop - onceStart sync.Once + mu sync.Mutex // guards all following fields + cbs map[*callbackHandle]ChangeFunc + ifState *interfaces.State + gwValid bool // whether gw and gwSelfIP are valid + gw netaddr.IP // our gateway's IP + gwSelfIP netaddr.IP // our own IP address (that corresponds to gw) started bool + closed bool goroutines sync.WaitGroup + wallTimer *time.Timer // nil until Started; re-armed AfterFunc per tick + lastWall time.Time + timeJumped bool // whether we need to send a changed=true after a big time jump } // New instantiates and starts a monitoring instance. @@ -70,10 +80,11 @@ type Mon struct { func New(logf logger.Logf) (*Mon, error) { logf = logger.WithPrefix(logf, "monitor: ") m := &Mon{ - logf: logf, - cbs: map[*callbackHandle]ChangeFunc{}, - change: make(chan struct{}, 1), - stop: make(chan struct{}), + logf: logf, + cbs: map[*callbackHandle]ChangeFunc{}, + change: make(chan struct{}, 1), + stop: make(chan struct{}), + lastWall: wallTime(), } st, err := m.interfaceStateUncached() if err != nil { @@ -140,28 +151,54 @@ func (m *Mon) RegisterChangeCallback(callback ChangeFunc) (unregister func()) { // Start starts the monitor. // A monitor can only be started & closed once. func (m *Mon) Start() { - m.onceStart.Do(func() { - if m.om == nil { - return - } - m.started = true - m.goroutines.Add(2) - go m.pump() - go m.debounce() - }) + m.mu.Lock() + defer m.mu.Unlock() + if m.started || m.closed { + return + } + m.started = true + + switch runtime.GOOS { + case "ios", "android": + // For battery reasons, and because these platforms + // don't really sleep in the same way, don't poll + // for the wall time to detect for wake-for-sleep + // walltime jumps. + default: + m.wallTimer = time.AfterFunc(pollWallTimeInterval, m.pollWallTime) + } + + if m.om == nil { + return + } + m.goroutines.Add(2) + go m.pump() + go m.debounce() } // Close closes the monitor. -// It may only be called once. func (m *Mon) Close() error { + m.mu.Lock() + if m.closed { + m.mu.Unlock() + return nil + } + m.closed = true close(m.stop) + + if m.wallTimer != nil { + m.wallTimer.Stop() + } + var err error if m.om != nil { err = m.om.Close() } - // If it was previously started, wait for those goroutines to finish. - m.onceStart.Do(func() {}) - if m.started { + + started := m.started + m.mu.Unlock() + + if started { m.goroutines.Wait() } return err @@ -227,9 +264,17 @@ func (m *Mon) debounce() { m.logf("interfaces.State: %v", err) } else { m.mu.Lock() + + // See if we have a queued or new time jump signal. + m.checkWallTimeAdvanceLocked() + timeJumped := m.timeJumped + if timeJumped { + m.logf("time jumped (probably wake from sleep); synthesizing major change event") + } + oldState := m.ifState - changed := !curState.EqualFiltered(oldState, interfaces.FilterInteresting) - if changed { + ifChanged := !curState.EqualFiltered(oldState, interfaces.FilterInteresting) + if ifChanged { m.gwValid = false m.ifState = curState @@ -238,6 +283,10 @@ func (m *Mon) debounce() { jsonSummary(oldState), jsonSummary(curState)) } } + changed := ifChanged || timeJumped + if changed { + m.timeJumped = false + } for _, cb := range m.cbs { go cb(changed, m.ifState) } @@ -259,3 +308,33 @@ func jsonSummary(x interface{}) interface{} { } return j } + +func wallTime() time.Time { + // From time package's docs: "The canonical way to strip a + // monotonic clock reading is to use t = t.Round(0)." + return time.Now().Round(0) +} + +func (m *Mon) pollWallTime() { + m.mu.Lock() + defer m.mu.Unlock() + if m.closed { + return + } + m.checkWallTimeAdvanceLocked() + if m.timeJumped { + m.InjectEvent() + } + m.wallTimer.Reset(pollWallTimeInterval) +} + +// checkWallTimeAdvanceLocked updates m.timeJumped, if wall time jumped +// more than 150% of pollWallTimeInterval, indicating we probably just +// came out of sleep. +func (m *Mon) checkWallTimeAdvanceLocked() { + now := wallTime() + if now.Sub(m.lastWall) > pollWallTimeInterval*3/2 { + m.timeJumped = true + } + m.lastWall = now +}