summaryrefslogtreecommitdiffhomepage
path: root/wgengine/monitor/monitor.go
diff options
context:
space:
mode:
Diffstat (limited to 'wgengine/monitor/monitor.go')
-rw-r--r--wgengine/monitor/monitor.go142
1 files changed, 108 insertions, 34 deletions
diff --git a/wgengine/monitor/monitor.go b/wgengine/monitor/monitor.go
index 254df7cb6..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 {
@@ -101,12 +112,7 @@ func (m *Mon) InterfaceState() *interfaces.State {
}
func (m *Mon) interfaceStateUncached() (*interfaces.State, error) {
- s, err := interfaces.GetState()
- if s != nil {
- s.RemoveTailscaleInterfaces()
- s.RemoveUninterestingInterfacesAndAddresses()
- }
- return s, err
+ return interfaces.GetState()
}
// GatewayAndSelfIP returns the current network's default gateway, and
@@ -145,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
@@ -232,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.Equal(oldState)
- if changed {
+ ifChanged := !curState.EqualFiltered(oldState, interfaces.FilterInteresting)
+ if ifChanged {
m.gwValid = false
m.ifState = curState
@@ -243,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)
}
@@ -264,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
+}