tailscale/util/syspolicy/rsop/change_callbacks.go
Brad Fitzpatrick 2434bc69fc util/syspolicy/{setting,ptype}: move PreferenceOption and Visibility to new leaf package
Step 3 in the series. See earlier cc532efc2000 and d05e6dc09e.

This step moves some types into a new leaf "ptype" package out of the
big "settings" package. The policyclient.Client will later get new
methods to return those things (as well as Duration and Uint64, which
weren't done at the time of the earlier prototype).

Updates #16998
Updates #12614

Change-Id: I4d72d8079de3b5351ed602eaa72863372bd474a2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-09-02 10:28:35 -07:00

116 lines
3.0 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package rsop
import (
"reflect"
"slices"
"sync"
"time"
"tailscale.com/util/set"
"tailscale.com/util/syspolicy/internal/loggerx"
"tailscale.com/util/syspolicy/pkey"
"tailscale.com/util/syspolicy/policyclient"
"tailscale.com/util/syspolicy/ptype"
"tailscale.com/util/syspolicy/setting"
)
// Change represents a change from the Old to the New value of type T.
type Change[T any] struct {
New, Old T
}
// PolicyChangeCallback is a function called whenever a policy changes.
type PolicyChangeCallback func(policyclient.PolicyChange)
// PolicyChange describes a policy change.
type PolicyChange struct {
snapshots Change[*setting.Snapshot]
}
// New returns the [setting.Snapshot] after the change.
func (c PolicyChange) New() *setting.Snapshot {
return c.snapshots.New
}
// Old returns the [setting.Snapshot] before the change.
func (c PolicyChange) Old() *setting.Snapshot {
return c.snapshots.Old
}
// HasChanged reports whether a policy setting with the specified [pkey.Key], has changed.
func (c PolicyChange) HasChanged(key pkey.Key) bool {
new, newErr := c.snapshots.New.GetErr(key)
old, oldErr := c.snapshots.Old.GetErr(key)
if newErr != nil && oldErr != nil {
return false
}
if newErr != nil || oldErr != nil {
return true
}
switch newVal := new.(type) {
case bool, uint64, string, ptype.Visibility, ptype.PreferenceOption, time.Duration:
return newVal != old
case []string:
oldVal, ok := old.([]string)
return !ok || !slices.Equal(newVal, oldVal)
default:
loggerx.Errorf("[unexpected] %q has an unsupported value type: %T", key, newVal)
return !reflect.DeepEqual(new, old)
}
}
// HasChangedAnyOf reports whether any of the specified policy settings has changed.
func (c PolicyChange) HasChangedAnyOf(keys ...pkey.Key) bool {
return slices.ContainsFunc(keys, c.HasChanged)
}
// policyChangeCallbacks are the callbacks to invoke when the effective policy changes.
// It is safe for concurrent use.
type policyChangeCallbacks struct {
mu sync.Mutex
cbs set.HandleSet[PolicyChangeCallback]
}
// Register adds the specified callback to be invoked whenever the policy changes.
func (c *policyChangeCallbacks) Register(callback PolicyChangeCallback) (unregister func()) {
c.mu.Lock()
handle := c.cbs.Add(callback)
c.mu.Unlock()
return func() {
c.mu.Lock()
delete(c.cbs, handle)
c.mu.Unlock()
}
}
// Invoke calls the registered callback functions with the specified policy change info.
func (c *policyChangeCallbacks) Invoke(snapshots Change[*setting.Snapshot]) {
var wg sync.WaitGroup
defer wg.Wait()
c.mu.Lock()
defer c.mu.Unlock()
wg.Add(len(c.cbs))
change := &PolicyChange{snapshots: snapshots}
for _, cb := range c.cbs {
go func() {
defer wg.Done()
cb(change)
}()
}
}
// Close awaits the completion of active callbacks and prevents any further invocations.
func (c *policyChangeCallbacks) Close() {
c.mu.Lock()
defer c.mu.Unlock()
if c.cbs != nil {
clear(c.cbs)
c.cbs = nil
}
}