156 lines
4.3 KiB
Go
156 lines
4.3 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package feature tracks which features are linked into the binary.
|
|
package feature
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
|
|
"tailscale.com/util/testenv"
|
|
)
|
|
|
|
var ErrUnavailable = errors.New("feature not included in this build")
|
|
|
|
var in = map[string]bool{}
|
|
|
|
// Registered reports the set of registered features.
|
|
//
|
|
// The returned map should not be modified by the caller,
|
|
// not accessed concurrently with calls to Register.
|
|
func Registered() map[string]bool { return in }
|
|
|
|
// IsRegistered reports whether the named feature package's init
|
|
// function has run and registered itself via [Register] in this
|
|
// binary. It is distinct from the compile-time [buildfeatures]
|
|
// constants: a feature package can be present in the binary but not
|
|
// imported (e.g. tsnet deliberately does not import many features),
|
|
// in which case its init does not run.
|
|
func IsRegistered(name string) bool { return in[name] }
|
|
|
|
// Register notes that the named feature is linked into the binary and
|
|
// reports whether it should initialize itself. It reports false if the
|
|
// feature was disabled via the TS_DISABLE_FEATURE environment variable,
|
|
// in which case the feature is not recorded as registered and the
|
|
// caller should skip the rest of its registration, such as setting
|
|
// hooks and registering extensions and handlers. The typical use is at
|
|
// the top of a feature package's init:
|
|
//
|
|
// func init() {
|
|
// if !feature.Register("foo") {
|
|
// return
|
|
// }
|
|
// // ... set hooks, register extensions and handlers ...
|
|
// }
|
|
//
|
|
// A feature package with more than one init that registers things puts
|
|
// this gate in one of them and guards the others with [Disabled], or
|
|
// better, consolidates them into a single init.
|
|
//
|
|
// Register panics if the feature is already registered.
|
|
func Register(name string) bool {
|
|
if Disabled(name) {
|
|
return false
|
|
}
|
|
if _, ok := in[name]; ok {
|
|
panic("duplicate feature registration for " + name)
|
|
}
|
|
in[name] = true
|
|
return true
|
|
}
|
|
|
|
// Hook is a func that can only be set once.
|
|
//
|
|
// It is not safe for concurrent use.
|
|
type Hook[Func any] struct {
|
|
f Func
|
|
ok bool
|
|
}
|
|
|
|
// IsSet reports whether the hook has been set.
|
|
func (h *Hook[Func]) IsSet() bool {
|
|
return h.ok
|
|
}
|
|
|
|
// Set sets the hook function, panicking if it's already been set
|
|
// or f is the zero value.
|
|
//
|
|
// It's meant to be called in init.
|
|
//
|
|
// As a backstop for feature packages that forget to consult
|
|
// [Register], if the caller is a package under tailscale.com/feature/
|
|
// whose feature was disabled via TS_DISABLE_FEATURE, the call is
|
|
// silently ignored and the hook is left unset.
|
|
func (h *Hook[Func]) Set(f Func) {
|
|
if callerFeatureDisabled() {
|
|
return
|
|
}
|
|
if h.ok {
|
|
panic("Set on already-set feature hook")
|
|
}
|
|
if reflect.ValueOf(f).IsZero() {
|
|
panic("Set with zero value")
|
|
}
|
|
h.f = f
|
|
h.ok = true
|
|
}
|
|
|
|
// SetForTest sets the hook function for tests, blowing
|
|
// away any previous value. It will panic if called from
|
|
// non-test code.
|
|
//
|
|
// It returns a restore function that resets the hook
|
|
// to its previous value.
|
|
func (h *Hook[Func]) SetForTest(f Func) (restore func()) {
|
|
testenv.AssertInTest()
|
|
old := *h
|
|
h.f, h.ok = f, true
|
|
return func() { *h = old }
|
|
}
|
|
|
|
// Get returns the hook function, or panics if it hasn't been set.
|
|
// Use IsSet to check if it's been set, or use GetOrNil if you're
|
|
// okay with a nil return value.
|
|
func (h *Hook[Func]) Get() Func {
|
|
if !h.ok {
|
|
panic("Get on unset feature hook, without IsSet")
|
|
}
|
|
return h.f
|
|
}
|
|
|
|
// GetOk returns the hook function and true if it has been set,
|
|
// otherwise its zero value and false.
|
|
func (h *Hook[Func]) GetOk() (f Func, ok bool) {
|
|
return h.f, h.ok
|
|
}
|
|
|
|
// GetOrNil returns the hook function or nil if it hasn't been set.
|
|
func (h *Hook[Func]) GetOrNil() Func {
|
|
return h.f
|
|
}
|
|
|
|
// Hooks is a slice of funcs.
|
|
//
|
|
// As opposed to a single Hook, this is meant to be used when
|
|
// multiple parties are able to install the same hook.
|
|
type Hooks[Func any] []Func
|
|
|
|
// Add adds a hook to the list of hooks.
|
|
//
|
|
// Add should only be called during early program
|
|
// startup before Tailscale has started.
|
|
// It is not safe for concurrent use.
|
|
//
|
|
// Like [Hook.Set], it is silently ignored if the calling feature
|
|
// package was disabled via TS_DISABLE_FEATURE.
|
|
func (h *Hooks[Func]) Add(f Func) {
|
|
if callerFeatureDisabled() {
|
|
return
|
|
}
|
|
if reflect.ValueOf(f).IsZero() {
|
|
panic("Add with zero value")
|
|
}
|
|
*h = append(*h, f)
|
|
}
|