@ -88,34 +88,35 @@ type Tracker struct {
// sysErr maps subsystems to their current error (or nil if the subsystem is healthy)
// Deprecated: using Warnables should be preferred
sysErr map [ Subsystem ] error
watchers set . HandleSet [ func ( * Warnable , * UnhealthyStat e) ] // opt func to run if error state changes
watchers set . HandleSet [ func ( Chang e) ] // opt func to run if error state changes
timer tstime . TimerController
latestVersion * tailcfg . ClientVersion // or nil
checkForUpdates bool
applyUpdates opt . Bool
inMapPoll bool
inMapPollSince time . Time
lastMapPollEndedAt time . Time
lastStreamedMapResponse time . Time
lastNoiseDial time . Time
derpHomeRegion int
derpHomeless bool
derpRegionConnected map [ int ] bool
derpRegionHealthProblem map [ int ] string
derpRegionLastFrame map [ int ] time . Time
derpMap * tailcfg . DERPMap // last DERP map from control, could be nil if never received one
lastMapRequestHeard time . Time // time we got a 200 from control for a MapRequest
ipnState string
ipnWantRunning bool
ipnWantRunningLastTrue time . Time // when ipnWantRunning last changed false -> true
anyInterfaceUp opt . Bool // empty means unknown (assume true)
controlHealth [ ] string
lastLoginErr error
localLogConfigErr error
tlsConnectionErrors map [ string ] error // map[ServerName]error
metricHealthMessage * metrics . MultiLabelMap [ metricHealthMessageLabel ]
inMapPoll bool
inMapPollSince time . Time
lastMapPollEndedAt time . Time
lastStreamedMapResponse time . Time
lastNoiseDial time . Time
derpHomeRegion int
derpHomeless bool
derpRegionConnected map [ int ] bool
derpRegionHealthProblem map [ int ] string
derpRegionLastFrame map [ int ] time . Time
derpMap * tailcfg . DERPMap // last DERP map from control, could be nil if never received one
lastMapRequestHeard time . Time // time we got a 200 from control for a MapRequest
ipnState string
ipnWantRunning bool
ipnWantRunningLastTrue time . Time // when ipnWantRunning last changed false -> true
anyInterfaceUp opt . Bool // empty means unknown (assume true)
lastNotifiedControlMessages map [ tailcfg . DisplayMessageID ] tailcfg . DisplayMessage // latest control messages processed, kept for change detection
controlMessages map [ tailcfg . DisplayMessageID ] tailcfg . DisplayMessage // latest control messages received
lastLoginErr error
localLogConfigErr error
tlsConnectionErrors map [ string ] error // map[ServerName]error
metricHealthMessage * metrics . MultiLabelMap [ metricHealthMessageLabel ]
}
func ( t * Tracker ) now ( ) time . Time {
@ -207,13 +208,15 @@ func unregister(w *Warnable) {
// the program.
type WarnableCode string
// A Warnable is something that we might want to warn the user about, or not. A Warnable is either
// in an healthy or unhealth state. A Warnable is unhealthy if the Tracker knows about a WarningState
// affecting the Warnable.
// In most cases, Warnables are components of the backend (for instance, "DNS" or "Magicsock").
// Warnables are similar to the Subsystem type previously used in this package, but they provide
// a unique identifying code for each Warnable, along with more metadata that makes it easier for
// a GUI to display the Warnable in a user-friendly way.
// A Warnable is something that we might want to warn the user about, or not. A
// Warnable is either in a healthy or unhealthy state. A Warnable is unhealthy if
// the Tracker knows about a WarningState affecting the Warnable.
//
// In most cases, Warnables are components of the backend (for instance, "DNS"
// or "Magicsock"). Warnables are similar to the Subsystem type previously used
// in this package, but they provide a unique identifying code for each
// Warnable, along with more metadata that makes it easier for a GUI to display
// the Warnable in a user-friendly way.
type Warnable struct {
// Code is a string that uniquely identifies this Warnable across the entire Tailscale backend,
// and can be mapped to a user-displayable localized string.
@ -409,12 +412,18 @@ func (t *Tracker) setUnhealthyLocked(w *Warnable, args Args) {
prevWs := t . warnableVal [ w ]
mak . Set ( & t . warnableVal , w , ws )
if ! ws . Equal ( prevWs ) {
change := Change {
WarnableChanged : true ,
Warnable : w ,
UnhealthyState : w . unhealthyState ( ws ) ,
}
for _ , cb := range t . watchers {
// If the Warnable has been unhealthy for more than its TimeToVisible, the callback should be
// executed immediately. Otherwise, the callback should be enqueued to run once the Warnable
// becomes visible.
if w . IsVisible ( ws , t . now ) {
cb ( w , w . unhealthyState ( ws ) )
cb ( change )
continue
}
@ -427,7 +436,7 @@ func (t *Tracker) setUnhealthyLocked(w *Warnable, args Args) {
// Check if the Warnable is still unhealthy, as it could have become healthy between the time
// the timer was set for and the time it was executed.
if t . warnableVal [ w ] != nil {
cb ( w, w . unhealthyState ( ws ) )
cb ( change )
delete ( t . pendingVisibleTimers , w )
}
} )
@ -460,8 +469,23 @@ func (t *Tracker) setHealthyLocked(w *Warnable) {
delete ( t . pendingVisibleTimers , w )
}
change := Change {
WarnableChanged : true ,
Warnable : w ,
}
for _ , cb := range t . watchers {
cb ( w , nil )
cb ( change )
}
}
// notifyWatchersControlChangedLocked calls each watcher to signal that control
// health messages have changed (and should be fetched via CurrentState).
func ( t * Tracker ) notifyWatchersControlChangedLocked ( ) {
change := Change {
ControlHealthChanged : true ,
}
for _ , cb := range t . watchers {
cb ( change )
}
}
@ -488,23 +512,57 @@ func (t *Tracker) AppendWarnableDebugFlags(base []string) []string {
return ret
}
// RegisterWatcher adds a function that will be called whenever the health state of any Warnable changes.
// If a Warnable becomes unhealthy or its unhealthy state is updated, the callback will be called with its
// current Representation.
// If a Warnable becomes healthy, the callback will be called with ws set to nil.
// The provided callback function will be executed in its own goroutine. The returned function can be used
// to unregister the callback.
func ( t * Tracker ) RegisterWatcher ( cb func ( w * Warnable , r * UnhealthyState ) ) ( unregister func ( ) ) {
return t . registerSyncWatcher ( func ( w * Warnable , r * UnhealthyState ) {
go cb ( w , r )
// Change is used to communicate a change to health. This could either be due to
// a Warnable changing from health to unhealthy (or vice-versa), or because the
// health messages received from the control-plane have changed.
//
// Exactly one *Changed field will be true.
type Change struct {
// ControlHealthChanged indicates it was health messages from the
// control-plane server that changed.
ControlHealthChanged bool
// WarnableChanged indicates it was a client Warnable which changed state.
WarnableChanged bool
// Warnable is whose health changed, as indicated in UnhealthyState.
Warnable * Warnable
// UnhealthyState is set if the changed Warnable is now unhealthy, or nil
// if Warnable is now healthy.
UnhealthyState * UnhealthyState
}
// RegisterWatcher adds a function that will be called its own goroutine
// whenever the health state of any client [Warnable] or control-plane health
// messages changes. The returned function can be used to unregister the
// callback.
//
// If a client [Warnable] becomes unhealthy or its unhealthy state is updated,
// the callback will be called with WarnableChanged set to true and the Warnable
// and its UnhealthyState:
//
// go cb(Change{WarnableChanged: true, Warnable: w, UnhealthyState: us})
//
// If a Warnable becomes healthy, the callback will be called with
// WarnableChanged set to true, the Warnable set, and UnhealthyState set to nil:
//
// go cb(Change{WarnableChanged: true, Warnable: w, UnhealthyState: nil})
//
// If the health messages from the control-plane change, the callback will be
// called with ControlHealthChanged set to true. Recipients can fetch the set of
// control-plane health messages by calling [Tracker.CurrentState]:
//
// go cb(Change{ControlHealthChanged: true})
func ( t * Tracker ) RegisterWatcher ( cb func ( Change ) ) ( unregister func ( ) ) {
return t . registerSyncWatcher ( func ( c Change ) {
go cb ( c )
} )
}
// registerSyncWatcher adds a function that will be called whenever the health
// state of any Warnable changes. The provided callback function will be
// executed synchronously. Call RegisterWatcher to register any callbacks that
// won't return from execution immediately.
func ( t * Tracker ) registerSyncWatcher ( cb func ( w * Warnable , r * UnhealthyState ) ) ( unregister func ( ) ) {
// state changes. The provided callback function will be executed synchronously.
// Call RegisterWatcher to register any callbacks that won't return from
// execution immediately.
func ( t * Tracker ) registerSyncWatcher ( cb func ( c Chang e) ) ( unregister func ( ) ) {
if t . nil ( ) {
return func ( ) { }
}
@ -512,7 +570,7 @@ func (t *Tracker) registerSyncWatcher(cb func(w *Warnable, r *UnhealthyState)) (
t . mu . Lock ( )
defer t . mu . Unlock ( )
if t . watchers == nil {
t . watchers = set . HandleSet [ func ( * Warnable , * UnhealthyStat e) ] { }
t . watchers = set . HandleSet [ func ( Chang e) ] { }
}
handle := t . watchers . Add ( cb )
if t . timer == nil {
@ -659,13 +717,15 @@ func (t *Tracker) updateLegacyErrorWarnableLocked(key Subsystem, err error) {
}
}
func ( t * Tracker ) SetControlHealth ( problems [ ] string ) {
func ( t * Tracker ) SetControlHealth ( problems map [ tailcfg . DisplayMessageID ] tailcfg . DisplayMessage ) {
if t . nil ( ) {
return
}
t . mu . Lock ( )
defer t . mu . Unlock ( )
t . controlHealth = problems
t . controlMessages = problems
t . selfCheckLocked ( )
}
@ -961,11 +1021,11 @@ func (t *Tracker) OverallError() error {
return t . multiErrLocked ( )
}
// Strings() returns a string array containing the Text of all Warnings
// currently known to the Tracker. These strings can be presented to th e
// user, although ideally you would use the Code property on each Warning
// to show a localized version of them instead.
// This function is here for legacy compatibility purposes and is deprecated.
// Strings() returns a string array containing the Text of all Warnings and
// ControlHealth messages currently known to the Tracker. These strings can be
// presented to the user, although ideally you would use the Code property on
// each Warning to show a localized version of them instead. This function is
// here for legacy compatibility purposes and is deprecated.
func ( t * Tracker ) Strings ( ) [ ] string {
if t . nil ( ) {
return nil
@ -991,6 +1051,19 @@ func (t *Tracker) stringsLocked() []string {
result = append ( result , w . Text ( ws . Args ) )
}
}
warnLen := len ( result )
for _ , c := range t . controlMessages {
if c . Title != "" && c . Text != "" {
result = append ( result , c . Title + ": " + c . Text )
} else if c . Title != "" {
result = append ( result , c . Title )
} else if c . Text != "" {
result = append ( result , c . Text )
}
}
sort . Strings ( result [ warnLen : ] )
return result
}
@ -1171,14 +1244,10 @@ func (t *Tracker) updateBuiltinWarnablesLocked() {
t . setHealthyLocked ( derpRegionErrorWarnable )
}
if len ( t . controlHealth ) > 0 {
for _ , s := range t . controlHealth {
t . setUnhealthyLocked ( controlHealthWarnable , Args {
ArgError : s ,
} )
}
} else {
t . setHealthyLocked ( controlHealthWarnable )
// Check if control health messages have changed
if ! maps . EqualFunc ( t . lastNotifiedControlMessages , t . controlMessages , tailcfg . DisplayMessage . Equal ) {
t . lastNotifiedControlMessages = t . controlMessages
t . notifyWatchersControlChangedLocked ( )
}
if err := envknob . ApplyDiskConfigError ( ) ; err != nil {