Skip to content

Commit 3bc10ea

Browse files
committed
ipn/ipnext: remove some interface indirection to add hooks
Now that 25c4dc5 removed unregistering hooks and made them into slices, just expose the slices and remove the setter funcs. This removes boilerplate ceremony around adding new hooks. This does export the hooks and make them mutable at runtime in theory, but that'd be a data race. If we really wanted to lock it down in the future we could make the feature.Hooks slice type be an opaque struct with an All() iterator and a "frozen" bool and we could freeze all the hooks after init. But that doesn't seem worth it. This means that hook registration is also now all in one place, rather than being mixed into ProfilesService vs ipnext.Host vs FooService vs BarService. I view that as a feature. When we have a ton of hooks and the list is long, then we can rearrange the fields in the Hooks struct as needed, or make sub-structs, or big comments. Updates tailscale#12614 Change-Id: I05ce5baa45a61e79c04591c2043c05f3288d8587 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
1 parent 3d8533b commit 3bc10ea

File tree

7 files changed

+83
-112
lines changed

7 files changed

+83
-112
lines changed

feature/feature.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,21 @@ func (h *Hook[Func]) Get() Func {
5252
}
5353
return h.f
5454
}
55+
56+
// Hooks is a slice of funcs.
57+
//
58+
// As opposed to a single Hook, this is meant to be used when
59+
// multiple parties are able to install the same hook.
60+
type Hooks[Func any] []Func
61+
62+
// Add adds a hook to the list of hooks.
63+
//
64+
// Add should only be called during early program
65+
// startup before Tailscale has started.
66+
// It is not safe for concurrent use.
67+
func (h *Hooks[Func]) Add(f Func) {
68+
if reflect.ValueOf(f).IsZero() {
69+
panic("Add with zero value")
70+
}
71+
*h = append(*h, f)
72+
}

feature/relayserver/relayserver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func (e *extension) Name() string {
7171
func (e *extension) Init(host ipnext.Host) error {
7272
profile, prefs := host.Profiles().CurrentProfileState()
7373
e.profileStateChanged(profile, prefs, false)
74-
host.Profiles().RegisterProfileStateChangeCallback(e.profileStateChanged)
74+
host.Hooks().ProfileStateChange.Add(e.profileStateChanged)
7575
// TODO(jwhited): callback for netmap/nodeattr changes (e.hasNodeAttrRelayServer)
7676
return nil
7777
}

ipn/auditlog/extension.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ func (e *extension) Name() string {
6363
// Init implements [ipnext.Extension] by registering callbacks and providers
6464
// for the duration of the extension's lifetime.
6565
func (e *extension) Init(h ipnext.Host) error {
66-
h.RegisterControlClientCallback(e.controlClientChanged)
67-
h.Profiles().RegisterProfileStateChangeCallback(e.profileChanged)
68-
h.RegisterAuditLogProvider(e.getCurrentLogger)
66+
h.Hooks().NewControlClient.Add(e.controlClientChanged)
67+
h.Hooks().ProfileStateChange.Add(e.profileChanged)
68+
h.Hooks().AuditLoggers.Add(e.getCurrentLogger)
6969
return nil
7070
}
7171

ipn/desktop/extension.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func (e *desktopSessionsExt) Init(host ipnext.Host) (err error) {
7777
if err != nil {
7878
return fmt.Errorf("session callback registration failed: %w", err)
7979
}
80-
host.Profiles().RegisterBackgroundProfileResolver(e.getBackgroundProfile)
80+
host.Hooks().BackgroundProfileResolvers.Add(e.getBackgroundProfile)
8181
e.cleanup = []func(){unregisterSessionCb}
8282
return nil
8383
}

ipn/ipnext/ipnext.go

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111

1212
"tailscale.com/control/controlclient"
13+
"tailscale.com/feature"
1314
"tailscale.com/ipn"
1415
"tailscale.com/ipn/ipnauth"
1516
"tailscale.com/tsd"
@@ -182,14 +183,6 @@ type Host interface {
182183
// Profiles returns the host's [ProfileServices].
183184
Profiles() ProfileServices
184185

185-
// RegisterAuditLogProvider registers an audit log provider,
186-
// which returns a function to be called when an auditable action
187-
// is about to be performed.
188-
//
189-
// It is a runtime error to register a nil provider or call after the host
190-
// has been initialized.
191-
RegisterAuditLogProvider(AuditLogProvider)
192-
193186
// AuditLogger returns a function that calls all currently registered audit loggers.
194187
// The function fails if any logger returns an error, indicating that the action
195188
// cannot be logged and must not be performed.
@@ -198,12 +191,9 @@ type Host interface {
198191
// the time of the call and must not be persisted.
199192
AuditLogger() ipnauth.AuditLogFunc
200193

201-
// RegisterControlClientCallback registers a function to be called every time a new
202-
// control client is created.
203-
//
204-
// It is a runtime error to register a nil provider or call after the host
205-
// has been initialized.
206-
RegisterControlClientCallback(NewControlClientCallback)
194+
// Hooks returns a non-nil pointer to a [Hooks] struct.
195+
// Hooks must not be modified concurrently or after Tailscale has started.
196+
Hooks() *Hooks
207197

208198
// SendNotifyAsync sends a notification to the IPN bus,
209199
// typically to the GUI client.
@@ -269,28 +259,6 @@ type ProfileServices interface {
269259
// to a client connecting or disconnecting or a change in the desktop
270260
// session state. It is used for logging.
271261
SwitchToBestProfileAsync(reason string)
272-
273-
// RegisterBackgroundProfileResolver registers a function to be used when
274-
// resolving the background profile.
275-
//
276-
// It is a runtime error to register a nil provider or call after the host
277-
// has been initialized.
278-
//
279-
// TODO(nickkhyl): allow specifying some kind of priority/altitude for the resolver.
280-
// TODO(nickkhyl): make it a "profile resolver" instead of a "background profile resolver".
281-
// The concepts of the "current user", "foreground profile" and "background profile"
282-
// only exist on Windows, and we're moving away from them anyway.
283-
RegisterBackgroundProfileResolver(ProfileResolver)
284-
285-
// RegisterProfileStateChangeCallback registers a function to be called when the current
286-
// [ipn.LoginProfile] or its [ipn.Prefs] change.
287-
//
288-
// To get the initial profile or prefs, use [ProfileServices.CurrentProfileState]
289-
// or [ProfileServices.CurrentPrefs] from the extension's [Extension.Init].
290-
//
291-
// It is a runtime error to register a nil provider or call after the host
292-
// has been initialized.
293-
RegisterProfileStateChangeCallback(ProfileStateChangeCallback)
294262
}
295263

296264
// ProfileStore provides read-only access to available login profiles and their preferences.
@@ -354,3 +322,36 @@ type ProfileStateChangeCallback func(_ ipn.LoginProfileView, _ ipn.PrefsView, sa
354322
// It returns a function to be called when the cc is being shut down,
355323
// or nil if no cleanup is needed.
356324
type NewControlClientCallback func(controlclient.Client, ipn.LoginProfileView) (cleanup func())
325+
326+
// Hooks is a collection of hooks that extensions can add to (non-concurrently)
327+
// during program initialization and can be called by LocalBackend and others at
328+
// runtime.
329+
//
330+
// Each hook has its own rules about when it's called and what environment it
331+
// has access to and what it's allowed to do.
332+
type Hooks struct {
333+
// ProfileStateChange are callbacks that are invoked when the current login profile
334+
// or its [ipn.Prefs] change, after those changes have been made. The current login profile
335+
// may be changed either because of a profile switch, or because the profile information
336+
// was updated by [LocalBackend.SetControlClientStatus], including when the profile
337+
// is first populated and persisted.
338+
ProfileStateChange feature.Hooks[ProfileStateChangeCallback]
339+
340+
// BackgroundProfileResolvers are registered background profile resolvers.
341+
// They're used to determine the profile to use when no GUI/CLI client is connected.
342+
//
343+
// TODO(nickkhyl): allow specifying some kind of priority/altitude for the resolver.
344+
// TODO(nickkhyl): make it a "profile resolver" instead of a "background profile resolver".
345+
// The concepts of the "current user", "foreground profile" and "background profile"
346+
// only exist on Windows, and we're moving away from them anyway.
347+
BackgroundProfileResolvers feature.Hooks[ProfileResolver]
348+
349+
// AuditLoggers are registered [AuditLogProvider]s.
350+
// Each provider is called to get an [ipnauth.AuditLogFunc] when an auditable action
351+
// is about to be performed. If an audit logger returns an error, the action is denied.
352+
AuditLoggers feature.Hooks[AuditLogProvider]
353+
354+
// NewControlClient are the functions to be called when a new control client
355+
// is created. It is called with the LocalBackend locked.
356+
NewControlClient feature.Hooks[NewControlClientCallback]
357+
}

ipn/ipnlocal/extension_host.go

Lines changed: 20 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ import (
6464
// and to further reduce the risk of accessing unexported methods or fields of [LocalBackend], the host interacts
6565
// with it via the [Backend] interface.
6666
type ExtensionHost struct {
67-
b Backend
68-
logf logger.Logf // prefixed with "ipnext:"
67+
b Backend
68+
hooks ipnext.Hooks
69+
logf logger.Logf // prefixed with "ipnext:"
6970

7071
// allExtensions holds the extensions in the order they were registered,
7172
// including those that have not yet attempted initialization or have failed to initialize.
@@ -84,22 +85,6 @@ type ExtensionHost struct {
8485
// doEnqueueBackendOperation adds an asynchronous [LocalBackend] operation to the workQueue.
8586
doEnqueueBackendOperation func(func(Backend))
8687

87-
// profileStateChangeCbs are callbacks that are invoked when the current login profile
88-
// or its [ipn.Prefs] change, after those changes have been made. The current login profile
89-
// may be changed either because of a profile switch, or because the profile information
90-
// was updated by [LocalBackend.SetControlClientStatus], including when the profile
91-
// is first populated and persisted.
92-
profileStateChangeCbs []ipnext.ProfileStateChangeCallback
93-
// backgroundProfileResolvers are registered background profile resolvers.
94-
// They're used to determine the profile to use when no GUI/CLI client is connected.
95-
backgroundProfileResolvers []ipnext.ProfileResolver
96-
// auditLoggers are registered [AuditLogProvider]s.
97-
// Each provider is called to get an [ipnauth.AuditLogFunc] when an auditable action
98-
// is about to be performed. If an audit logger returns an error, the action is denied.
99-
auditLoggers []ipnext.AuditLogProvider
100-
// newControlClientCbs are the functions to be called when a new control client is created.
101-
newControlClientCbs []ipnext.NewControlClientCallback
102-
10388
shuttingDown atomic.Bool
10489

10590
// mu protects the following fields.
@@ -208,6 +193,15 @@ func (h *ExtensionHost) Init() {
208193
}
209194
}
210195

196+
var zeroHooks ipnext.Hooks
197+
198+
func (h *ExtensionHost) Hooks() *ipnext.Hooks {
199+
if h == nil {
200+
return &zeroHooks
201+
}
202+
return &h.hooks
203+
}
204+
211205
func (h *ExtensionHost) init() {
212206
defer h.initDone.Store(true)
213207

@@ -360,24 +354,6 @@ func (h *ExtensionHost) SendNotifyAsync(n ipn.Notify) {
360354
})
361355
}
362356

363-
// addFuncHook appends non-nil fn to hooks.
364-
func addFuncHook[F any](h *ExtensionHost, hooks *[]F, fn F) {
365-
if h.initDone.Load() {
366-
panic("invalid callback register after init")
367-
}
368-
if reflect.ValueOf(fn).IsZero() {
369-
panic("nil function hook")
370-
}
371-
*hooks = append(*hooks, fn)
372-
}
373-
374-
// RegisterProfileStateChangeCallback implements [ipnext.ProfileServices].
375-
func (h *ExtensionHost) RegisterProfileStateChangeCallback(cb ipnext.ProfileStateChangeCallback) {
376-
if h != nil {
377-
addFuncHook(h, &h.profileStateChangeCbs, cb)
378-
}
379-
}
380-
381357
// NotifyProfileChange invokes registered profile state change callbacks
382358
// and updates the current profile and prefs in the host.
383359
// It strips private keys from the [ipn.Prefs] before preserving
@@ -397,7 +373,7 @@ func (h *ExtensionHost) NotifyProfileChange(profile ipn.LoginProfileView, prefs
397373
h.currentProfile = profile
398374
h.mu.Unlock()
399375

400-
for _, cb := range h.profileStateChangeCbs {
376+
for _, cb := range h.hooks.ProfileStateChange {
401377
cb(profile, prefs, sameNode)
402378
}
403379
}
@@ -421,18 +397,11 @@ func (h *ExtensionHost) NotifyProfilePrefsChanged(profile ipn.LoginProfileView,
421397
// Get the callbacks to be invoked.
422398
h.mu.Unlock()
423399

424-
for _, cb := range h.profileStateChangeCbs {
400+
for _, cb := range h.hooks.ProfileStateChange {
425401
cb(profile, newPrefs, true)
426402
}
427403
}
428404

429-
// RegisterBackgroundProfileResolver implements [ipnext.ProfileServices].
430-
func (h *ExtensionHost) RegisterBackgroundProfileResolver(resolver ipnext.ProfileResolver) {
431-
if h != nil {
432-
addFuncHook(h, &h.backgroundProfileResolvers, resolver)
433-
}
434-
}
435-
436405
func (h *ExtensionHost) active() bool {
437406
return h != nil && !h.shuttingDown.Load()
438407
}
@@ -455,7 +424,7 @@ func (h *ExtensionHost) DetermineBackgroundProfile(profiles ipnext.ProfileStore)
455424

456425
// Attempt to resolve the background profile using the registered
457426
// background profile resolvers (e.g., [ipn/desktop.desktopSessionsExt] on Windows).
458-
for _, resolver := range h.backgroundProfileResolvers {
427+
for _, resolver := range h.hooks.BackgroundProfileResolvers {
459428
if profile := resolver(profiles); profile.Valid() {
460429
return profile
461430
}
@@ -466,37 +435,20 @@ func (h *ExtensionHost) DetermineBackgroundProfile(profiles ipnext.ProfileStore)
466435
return ipn.LoginProfileView{}
467436
}
468437

469-
// RegisterControlClientCallback implements [ipnext.Host].
470-
func (h *ExtensionHost) RegisterControlClientCallback(cb ipnext.NewControlClientCallback) {
471-
if h != nil {
472-
addFuncHook(h, &h.newControlClientCbs, cb)
473-
}
474-
}
475-
476438
// NotifyNewControlClient invokes all registered control client callbacks.
477439
// It returns callbacks to be executed when the control client shuts down.
478440
func (h *ExtensionHost) NotifyNewControlClient(cc controlclient.Client, profile ipn.LoginProfileView) (ccShutdownCbs []func()) {
479441
if !h.active() {
480442
return nil
481443
}
482-
if len(h.newControlClientCbs) > 0 {
483-
ccShutdownCbs = make([]func(), 0, len(h.newControlClientCbs))
484-
for _, cb := range h.newControlClientCbs {
485-
if shutdown := cb(cc, profile); shutdown != nil {
486-
ccShutdownCbs = append(ccShutdownCbs, shutdown)
487-
}
444+
for _, cb := range h.hooks.NewControlClient {
445+
if shutdown := cb(cc, profile); shutdown != nil {
446+
ccShutdownCbs = append(ccShutdownCbs, shutdown)
488447
}
489448
}
490449
return ccShutdownCbs
491450
}
492451

493-
// RegisterAuditLogProvider implements [ipnext.Host].
494-
func (h *ExtensionHost) RegisterAuditLogProvider(provider ipnext.AuditLogProvider) {
495-
if h != nil {
496-
addFuncHook(h, &h.auditLoggers, provider)
497-
}
498-
}
499-
500452
// AuditLogger returns a function that reports an auditable action
501453
// to all registered audit loggers. It fails if any of them returns an error,
502454
// indicating that the action cannot be logged and must not be performed.
@@ -510,8 +462,8 @@ func (h *ExtensionHost) AuditLogger() ipnauth.AuditLogFunc {
510462
if !h.active() {
511463
return func(tailcfg.ClientAuditAction, string) error { return nil }
512464
}
513-
loggers := make([]ipnauth.AuditLogFunc, 0, len(h.auditLoggers))
514-
for _, provider := range h.auditLoggers {
465+
loggers := make([]ipnauth.AuditLogFunc, 0, len(h.hooks.AuditLoggers))
466+
for _, provider := range h.hooks.AuditLoggers {
515467
loggers = append(loggers, provider())
516468
}
517469
return func(action tailcfg.ClientAuditAction, details string) error {

ipn/ipnlocal/extension_host_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,7 @@ func TestExtensionHostProfileStateChangeCallback(t *testing.T) {
748748
tt.ext.InitHook = func(e *testExtension) error {
749749
// Create and register the callback on init.
750750
handler := makeStateChangeAppender(e)
751-
e.host.Profiles().RegisterProfileStateChangeCallback(handler)
751+
e.host.Hooks().ProfileStateChange.Add(handler)
752752
return nil
753753
}
754754
}
@@ -875,7 +875,7 @@ func TestBackgroundProfileResolver(t *testing.T) {
875875
// This is typically done by the extensions themselves,
876876
// but we do it here for testing purposes.
877877
for _, r := range tt.resolvers {
878-
h.Profiles().RegisterBackgroundProfileResolver(r)
878+
h.Hooks().BackgroundProfileResolvers.Add(r)
879879
}
880880
h.Init()
881881

@@ -968,7 +968,7 @@ func TestAuditLogProviders(t *testing.T) {
968968
}
969969
}
970970
ext.InitHook = func(e *testExtension) error {
971-
e.host.RegisterAuditLogProvider(provider)
971+
e.host.Hooks().AuditLoggers.Add(provider)
972972
return nil
973973
}
974974
exts = append(exts, ext)

0 commit comments

Comments
 (0)