Skip to content

Commit 66371f3

Browse files
committed
feature,ipn/ipnlocal: add profileManager.StateChangeHook
We update profileManager to allow registering a single state (profile+prefs) change hook. This is to invert the dependency between the profileManager and the LocalBackend, so that instead of LocalBackend asking profileManager for the state, we can have profileManager call LocalBackend when the state changes. We also update feature.Hook with a new (*feature.Hook).GetOk method to avoid calling both IsSet and Get. Updates tailscale/corp#28014 Updates tailscale#12614 Signed-off-by: Nick Khyl <[email protected]>
1 parent 0cfd643 commit 66371f3

File tree

4 files changed

+589
-19
lines changed

4 files changed

+589
-19
lines changed

feature/feature.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ func (h *Hook[Func]) Get() Func {
5353
return h.f
5454
}
5555

56+
// GetOk returns the hook function and true if it has been set,
57+
// otherwise its zero value and false.
58+
func (h *Hook[Func]) GetOk() (f Func, ok bool) {
59+
return h.f, h.ok
60+
}
61+
5662
// Hooks is a slice of funcs.
5763
//
5864
// As opposed to a single Hook, this is meant to be used when

ipn/ipnlocal/local.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1675,28 +1675,13 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
16751675

16761676
// Perform all mutations of prefs based on the netmap here.
16771677
if prefsChanged {
1678-
profile := b.pm.CurrentProfile()
16791678
// Prefs will be written out if stale; this is not safe unless locked or cloned.
16801679
if err := b.pm.SetPrefs(prefs.View(), ipn.NetworkProfile{
16811680
MagicDNSName: curNetMap.MagicDNSSuffix(),
16821681
DomainName: curNetMap.DomainName(),
16831682
}); err != nil {
16841683
b.logf("Failed to save new controlclient state: %v", err)
16851684
}
1686-
// Updating profile prefs may have resulted in a change to the current [ipn.LoginProfile],
1687-
// either because the user completed a login, which populated and persisted their profile
1688-
// for the first time, or because of an [ipn.NetworkProfile] or [tailcfg.UserProfile] change.
1689-
// Theoretically, a completed login could also result in a switch to a different existing
1690-
// profile representing a different node (see tailscale/tailscale#8816).
1691-
//
1692-
// Let's check if the current profile has changed, and invoke all registered
1693-
// [ipnext.ProfileStateChangeCallback] if necessary.
1694-
if cp := b.pm.CurrentProfile(); *cp.AsStruct() != *profile.AsStruct() {
1695-
// If the profile ID was empty before SetPrefs, it's a new profile
1696-
// and the user has just completed a login for the first time.
1697-
sameNode := profile.ID() == "" || profile.ID() == cp.ID()
1698-
b.extHost.NotifyProfileChange(profile, prefs.View(), sameNode)
1699-
}
17001685
}
17011686

17021687
// initTKALocked is dependent on CurrentProfile.ID, which is initialized

ipn/ipnlocal/profiles.go

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ type profileManager struct {
4343
currentProfile ipn.LoginProfileView // always Valid (once [newProfileManager] returns).
4444
prefs ipn.PrefsView // always Valid (once [newProfileManager] returns).
4545

46+
// StateChangeHook is an optional hook that is called when the current profile or prefs change,
47+
// such as due to a profile switch or a change in the profile's preferences.
48+
// It is typically set by the [LocalBackend] to invert the dependency between
49+
// the [profileManager] and the [LocalBackend], so that instead of [LocalBackend]
50+
// asking [profileManager] for the state, we can have [profileManager] call
51+
// [LocalBackend] when the state changes. See also:
52+
// https://github.com/tailscale/tailscale/pull/15791#discussion_r2060838160
53+
StateChangeHook ipnext.ProfileStateChangeCallback
54+
4655
// extHost is the bridge between [profileManager] and the registered [ipnext.Extension]s.
4756
// It may be nil in tests. A nil pointer is a valid, no-op host.
4857
extHost *ExtensionHost
@@ -166,6 +175,16 @@ func (pm *profileManager) SwitchToProfile(profile ipn.LoginProfileView) (cp ipn.
166175
// But if updating the default profile fails, we should log it.
167176
pm.logf("failed to set %s (%s) as the default profile: %v", profile.Name(), profile.ID(), err)
168177
}
178+
179+
if f := pm.StateChangeHook; f != nil {
180+
f(pm.currentProfile, pm.prefs, false)
181+
}
182+
// Do not call pm.extHost.NotifyProfileChange here; it is invoked in
183+
// [LocalBackend.resetForProfileChangeLockedOnEntry] after the netmap reset.
184+
// TODO(nickkhyl): Consider moving it here (or into the stateChangeCb handler
185+
// in [LocalBackend]) once the profile/node state, including the netmap,
186+
// is actually tied to the current profile.
187+
169188
return profile, true, nil
170189
}
171190

@@ -344,11 +363,19 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile)
344363
// [LocalBackend.resetForProfileChangeLockedOnEntry] is not called and certain
345364
// node/profile-specific state may not be reset as expected.
346365
//
347-
// However, LocalBackend notifies [ipnext.Extension]s about the profile change,
366+
// However, [profileManager] notifies [ipnext.Extension]s about the profile change,
348367
// so features migrated from LocalBackend to external packages should not be affected.
349368
//
350369
// See tailscale/corp#28014.
351-
pm.currentProfile = cp
370+
if !cp.Equals(pm.currentProfile) {
371+
const sameNode = false // implicit profile switch
372+
pm.currentProfile = cp
373+
pm.prefs = prefsIn.AsStruct().View()
374+
if f := pm.StateChangeHook; f != nil {
375+
f(cp, prefsIn, sameNode)
376+
}
377+
pm.extHost.NotifyProfileChange(cp, prefsIn, sameNode)
378+
}
352379
cp, err := pm.setProfilePrefs(nil, prefsIn, np)
353380
if err != nil {
354381
return err
@@ -410,7 +437,20 @@ func (pm *profileManager) setProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.Pref
410437
// Update the current profile view to reflect the changes
411438
// if the specified profile is the current profile.
412439
if isCurrentProfile {
413-
pm.currentProfile = lp.View()
440+
// Always set pm.currentProfile to the new profile view for pointer equality.
441+
// We check it further down the call stack.
442+
lp := lp.View()
443+
sameProfileInfo := lp.Equals(pm.currentProfile)
444+
pm.currentProfile = lp
445+
if !sameProfileInfo {
446+
// But only invoke the callbacks if the profile info has actually changed.
447+
const sameNode = true // just an info update; still the same node
448+
pm.prefs = prefsIn.AsStruct().View() // suppress further callbacks for this change
449+
if f := pm.StateChangeHook; f != nil {
450+
f(lp, prefsIn, sameNode)
451+
}
452+
pm.extHost.NotifyProfileChange(lp, prefsIn, sameNode)
453+
}
414454
}
415455

416456
// An empty profile.ID indicates that the node info is not available yet,
@@ -470,7 +510,13 @@ func (pm *profileManager) setProfilePrefsNoPermCheck(profile ipn.LoginProfileVie
470510
// That said, regardless of the cleanup, we might want
471511
// to keep the profileManager responsible for invoking
472512
// profile- and prefs-related callbacks.
473-
pm.extHost.NotifyProfilePrefsChanged(pm.currentProfile, oldPrefs, clonedPrefs)
513+
514+
if !clonedPrefs.Equals(oldPrefs) {
515+
if f := pm.StateChangeHook; f != nil {
516+
f(pm.currentProfile, clonedPrefs, true)
517+
}
518+
pm.extHost.NotifyProfilePrefsChanged(pm.currentProfile, oldPrefs, clonedPrefs)
519+
}
474520

475521
pm.updateHealth()
476522
}

0 commit comments

Comments
 (0)