matterbridge/vendor/github.com/wiggin77/cfg/config.go

367 lines
9.0 KiB
Go

package cfg
import (
"errors"
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/wiggin77/cfg/timeconv"
)
// ErrNotFound returned when an operation is attempted on a
// resource that doesn't exist, such as fetching a non-existing
// property name.
var ErrNotFound = errors.New("not found")
type sourceEntry struct {
src Source
props map[string]string
}
// Config provides methods for retrieving property values from one or more
// configuration sources.
type Config struct {
mutexSrc sync.RWMutex
mutexListeners sync.RWMutex
srcs []*sourceEntry
chgListeners []ChangedListener
shutdown chan interface{}
wantPanicOnError bool
}
// PrependSource inserts one or more `Sources` at the beginning of
// the list of sources such that the first source will be the
// source checked first when resolving a property value.
func (config *Config) PrependSource(srcs ...Source) {
arr := config.wrapSources(srcs...)
config.mutexSrc.Lock()
if config.shutdown == nil {
config.shutdown = make(chan interface{})
}
config.srcs = append(arr, config.srcs...)
config.mutexSrc.Unlock()
for _, se := range arr {
if _, ok := se.src.(SourceMonitored); ok {
config.monitor(se)
}
}
}
// AppendSource appends one or more `Sources` at the end of
// the list of sources such that the last source will be the
// source checked last when resolving a property value.
func (config *Config) AppendSource(srcs ...Source) {
arr := config.wrapSources(srcs...)
config.mutexSrc.Lock()
if config.shutdown == nil {
config.shutdown = make(chan interface{})
}
config.srcs = append(config.srcs, arr...)
config.mutexSrc.Unlock()
for _, se := range arr {
if _, ok := se.src.(SourceMonitored); ok {
config.monitor(se)
}
}
}
// wrapSources wraps one or more Source's and returns
// them as an array of `sourceEntry`.
func (config *Config) wrapSources(srcs ...Source) []*sourceEntry {
arr := make([]*sourceEntry, 0, len(srcs))
for _, src := range srcs {
se := &sourceEntry{src: src}
config.reloadProps(se)
arr = append(arr, se)
}
return arr
}
// SetWantPanicOnError sets the flag determining if Config
// should panic when `GetProps` or `GetLastModified` errors
// for a `Source`.
func (config *Config) SetWantPanicOnError(b bool) {
config.mutexSrc.Lock()
config.wantPanicOnError = b
config.mutexSrc.Unlock()
}
// ShouldPanicOnError gets the flag determining if Config
// should panic when `GetProps` or `GetLastModified` errors
// for a `Source`.
func (config *Config) ShouldPanicOnError() (b bool) {
config.mutexSrc.RLock()
b = config.wantPanicOnError
config.mutexSrc.RUnlock()
return b
}
// getProp returns the value of a named property.
// Each `Source` is checked, in the order created by adding via
// `AppendSource` and `PrependSource`, until a value for the
// property is found.
func (config *Config) getProp(name string) (val string, ok bool) {
config.mutexSrc.RLock()
defer config.mutexSrc.RUnlock()
var s string
for _, se := range config.srcs {
if se.props != nil {
if s, ok = se.props[name]; ok {
val = strings.TrimSpace(s)
return
}
}
}
return
}
// String returns the value of the named prop as a string.
// If the property is not found then the supplied default `def`
// and `ErrNotFound` are returned.
func (config *Config) String(name string, def string) (val string, err error) {
if v, ok := config.getProp(name); ok {
val = v
err = nil
return
}
err = ErrNotFound
val = def
return
}
// Int returns the value of the named prop as an `int`.
// If the property is not found then the supplied default `def`
// and `ErrNotFound` are returned.
//
// See config.String
func (config *Config) Int(name string, def int) (val int, err error) {
var s string
if s, err = config.String(name, ""); err == nil {
var i int64
if i, err = strconv.ParseInt(s, 10, 32); err == nil {
val = int(i)
}
}
if err != nil {
val = def
}
return
}
// Int64 returns the value of the named prop as an `int64`.
// If the property is not found then the supplied default `def`
// and `ErrNotFound` are returned.
//
// See config.String
func (config *Config) Int64(name string, def int64) (val int64, err error) {
var s string
if s, err = config.String(name, ""); err == nil {
val, err = strconv.ParseInt(s, 10, 64)
}
if err != nil {
val = def
}
return
}
// Float64 returns the value of the named prop as a `float64`.
// If the property is not found then the supplied default `def`
// and `ErrNotFound` are returned.
//
// See config.String
func (config *Config) Float64(name string, def float64) (val float64, err error) {
var s string
if s, err = config.String(name, ""); err == nil {
val, err = strconv.ParseFloat(s, 64)
}
if err != nil {
val = def
}
return
}
// Bool returns the value of the named prop as a `bool`.
// If the property is not found then the supplied default `def`
// and `ErrNotFound` are returned.
//
// Supports (t, true, 1, y, yes) for true, and (f, false, 0, n, no) for false,
// all case-insensitive.
//
// See config.String
func (config *Config) Bool(name string, def bool) (val bool, err error) {
var s string
if s, err = config.String(name, ""); err == nil {
switch strings.ToLower(s) {
case "t", "true", "1", "y", "yes":
val = true
case "f", "false", "0", "n", "no":
val = false
default:
err = errors.New("invalid syntax")
}
}
if err != nil {
val = def
}
return
}
// Duration returns the value of the named prop as a `time.Duration`, representing
// a span of time.
//
// Units of measure are supported: ms, sec, min, hour, day, week, year.
// See config.UnitsToMillis for a complete list of units supported.
//
// If the property is not found then the supplied default `def`
// and `ErrNotFound` are returned.
//
// See config.String
func (config *Config) Duration(name string, def time.Duration) (val time.Duration, err error) {
var s string
if s, err = config.String(name, ""); err == nil {
var ms int64
ms, err = timeconv.ParseMilliseconds(s)
val = time.Duration(ms) * time.Millisecond
}
if err != nil {
val = def
}
return
}
// AddChangedListener adds a listener that will receive notifications
// whenever one or more property values change within the config.
func (config *Config) AddChangedListener(l ChangedListener) {
config.mutexListeners.Lock()
defer config.mutexListeners.Unlock()
config.chgListeners = append(config.chgListeners, l)
}
// RemoveChangedListener removes all instances of a ChangedListener.
// Returns `ErrNotFound` if the listener was not present.
func (config *Config) RemoveChangedListener(l ChangedListener) error {
config.mutexListeners.Lock()
defer config.mutexListeners.Unlock()
dest := make([]ChangedListener, 0, len(config.chgListeners))
err := ErrNotFound
// Remove all instances of the listener by
// copying list while filtering.
for _, s := range config.chgListeners {
if s != l {
dest = append(dest, s)
} else {
err = nil
}
}
config.chgListeners = dest
return err
}
// Shutdown can be called to stop monitoring of all config sources.
func (config *Config) Shutdown() {
config.mutexSrc.RLock()
defer config.mutexSrc.RUnlock()
if config.shutdown != nil {
close(config.shutdown)
}
}
// onSourceChanged is called whenever one or more properties of a
// config source has changed.
func (config *Config) onSourceChanged(src SourceMonitored) {
defer func() {
if p := recover(); p != nil {
fmt.Println(p)
}
}()
config.mutexListeners.RLock()
defer config.mutexListeners.RUnlock()
for _, l := range config.chgListeners {
l.ConfigChanged(config, src)
}
}
// monitor periodically checks a config source for changes.
func (config *Config) monitor(se *sourceEntry) {
go func(se *sourceEntry, shutdown <-chan interface{}) {
var src SourceMonitored
var ok bool
if src, ok = se.src.(SourceMonitored); !ok {
return
}
paused := false
last := time.Time{}
freq := src.GetMonitorFreq()
if freq <= 0 {
paused = true
freq = 10
last, _ = src.GetLastModified()
}
timer := time.NewTimer(freq)
for {
select {
case <-timer.C:
if !paused {
if latest, err := src.GetLastModified(); err != nil {
if config.ShouldPanicOnError() {
panic(fmt.Sprintf("error <%v> getting last modified for %v", err, src))
}
} else {
if last.Before(latest) {
last = latest
config.reloadProps(se)
// TODO: calc diff and provide detailed changes
config.onSourceChanged(src)
}
}
}
freq = src.GetMonitorFreq()
if freq <= 0 {
paused = true
freq = 10
} else {
paused = false
}
timer.Reset(freq)
case <-shutdown:
// stop the timer and exit
if !timer.Stop() {
<-timer.C
}
return
}
}
}(se, config.shutdown)
}
// reloadProps causes a Source to reload its properties.
func (config *Config) reloadProps(se *sourceEntry) {
config.mutexSrc.Lock()
defer config.mutexSrc.Unlock()
m, err := se.src.GetProps()
if err != nil {
if config.wantPanicOnError {
panic(fmt.Sprintf("GetProps error for %v", se.src))
}
return
}
se.props = make(map[string]string)
for k, v := range m {
se.props[k] = v
}
}