2024-08-22 11:38:12 -06:00
|
|
|
package config
|
|
|
|
|
|
|
|
import "os"
|
|
|
|
import "log"
|
|
|
|
import "sync"
|
|
|
|
import "slices"
|
|
|
|
import "path/filepath"
|
|
|
|
import "github.com/fsnotify/fsnotify"
|
|
|
|
import "git.tebibyte.media/tomo/tomo/event"
|
|
|
|
|
2024-08-22 14:03:05 -06:00
|
|
|
// Goroutine model:
|
|
|
|
// All private methods (except for lockAndProcessEvent) do not lock the config,
|
|
|
|
// but all public methods do. Private methods may not call public methods.
|
|
|
|
// Locking must always be method-level, with a call to Lock at the start,
|
|
|
|
// directly followed by a deferred call to Unlock.
|
|
|
|
|
2024-08-22 11:38:12 -06:00
|
|
|
type config struct {
|
|
|
|
open bool
|
|
|
|
watcher *fsnotify.Watcher
|
2024-08-22 14:03:05 -06:00
|
|
|
lock sync.RWMutex
|
2024-08-22 11:38:12 -06:00
|
|
|
|
|
|
|
paths struct {
|
|
|
|
user string
|
|
|
|
system []string
|
|
|
|
watching map[string] struct { }
|
|
|
|
}
|
|
|
|
|
|
|
|
data struct {
|
2024-08-22 14:03:05 -06:00
|
|
|
user *File
|
2024-08-22 11:38:12 -06:00
|
|
|
system []map[string] Value
|
|
|
|
}
|
|
|
|
|
|
|
|
on struct {
|
|
|
|
change event.Broadcaster[func (string)]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewConfig creates a new Config using paths to the user-level config file, and
|
|
|
|
// a set of system config files. These files need not exist: the user-level file
|
|
|
|
// will be created when Set is called for the first time if it does not exist
|
|
|
|
// already, and nonexistent system files are simply ignored (unless the Config
|
|
|
|
// finds them at any point to have been spontaneously created).
|
|
|
|
//
|
|
|
|
// The user file is written to when Set is called, and the system files are only
|
|
|
|
// read from. Values in the user file override those in the system files, and
|
|
|
|
// system files specified nearer to the start of the vararg list will override
|
|
|
|
// those farther down.
|
|
|
|
func NewConfig (user string, system ...string) (ConfigCloser, error) {
|
|
|
|
conf := new(config)
|
|
|
|
conf.paths.user = user
|
|
|
|
conf.paths.system = system
|
|
|
|
err := conf.init()
|
|
|
|
if err != nil { return nil, err }
|
2024-08-22 14:03:05 -06:00
|
|
|
go func () {
|
|
|
|
for event := range conf.watcher.Events {
|
|
|
|
conf.lockAndProcessEvent(event)
|
|
|
|
}
|
|
|
|
} ()
|
2024-08-22 11:38:12 -06:00
|
|
|
return conf, nil
|
|
|
|
}
|
|
|
|
|
2024-08-22 14:03:05 -06:00
|
|
|
// this method may only be run in the goroutine spawned by NewConfig.
|
|
|
|
func (this *config) lockAndProcessEvent (event fsnotify.Event) {
|
|
|
|
this.lock.Lock()
|
|
|
|
defer this.lock.Unlock()
|
|
|
|
if !(event.Has(fsnotify.Write)) { return }
|
|
|
|
if _, ok := this.paths.watching[event.Name]; !ok { return }
|
|
|
|
|
|
|
|
if event.Name == this.paths.user {
|
|
|
|
this.reloadUser()
|
|
|
|
} else {
|
|
|
|
index := slices.Index(this.paths.system, event.Name)
|
|
|
|
if index > 0 {
|
|
|
|
this.reloadSystem(index)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO diff and call event handler if changed
|
|
|
|
}
|
|
|
|
|
2024-08-22 11:38:12 -06:00
|
|
|
func (this *config) init () error {
|
|
|
|
watcher, err := fsnotify.NewWatcher()
|
|
|
|
if err != nil { return err }
|
|
|
|
this.watcher = watcher
|
|
|
|
|
|
|
|
this.paths.watching = make(map[string] struct { })
|
|
|
|
this.watcher.Add(filepath.Dir(this.paths.user))
|
|
|
|
this.paths.watching[this.paths.user] = struct { } { }
|
|
|
|
this.reloadUser()
|
|
|
|
for index, path := range this.paths.system {
|
|
|
|
this.watcher.Add(filepath.Dir(path))
|
|
|
|
this.paths.watching[path] = struct { } { }
|
|
|
|
this.reloadSystem(index)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *config) errIfClosed () error {
|
|
|
|
if this.open {
|
|
|
|
return nil
|
|
|
|
} else {
|
|
|
|
return ErrClosed
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *config) reloadUser () {
|
|
|
|
file, err := os.Open(this.paths.user)
|
|
|
|
if err != nil { return }
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
userFile, err := Parse(file)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("nasin: problems loading user config file %s: %v", this.paths.user, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
this.data.user = userFile
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *config) reloadSystem (index int) {
|
|
|
|
path := this.paths.system[index]
|
|
|
|
file, err := os.Open(path)
|
|
|
|
if err != nil { return }
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
systemFile, err := Parse(file)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("nasin: problems loading system config file %s: %v", path, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
this.data.system[index] = systemFile.Map()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *config) saveUser () error {
|
|
|
|
// TODO set some sort of flag to ignore the next inotify event for the
|
|
|
|
// user file so we dont reload it immediately after. also need to
|
|
|
|
// broadacast Changed event.
|
|
|
|
enclosingDir := filepath.Dir(this.paths.user)
|
|
|
|
err := os.MkdirAll(enclosingDir, 755)
|
|
|
|
if err != nil { return err }
|
|
|
|
file, err := os.Create(this.paths.user)
|
|
|
|
if err != nil { return err }
|
|
|
|
defer file.Close()
|
|
|
|
_, err = this.data.user.WriteTo(file)
|
|
|
|
if err != nil { return err }
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-08-22 14:03:05 -06:00
|
|
|
func (this *config) processUserDiff (changed []string) {
|
|
|
|
// TODO
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *config) processSystemDiff (index int, changed []string) {
|
|
|
|
// TODO
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *config) get (key string, fallback Value) (Value, error) {
|
2024-08-22 11:38:12 -06:00
|
|
|
// try user config
|
|
|
|
if !KeyValid(key) { return nil, ErrMalformedKey }
|
|
|
|
|
|
|
|
if this.data.user != nil {
|
|
|
|
value, err := this.data.user.Get(key)
|
|
|
|
if err == nil && err != ErrNonexistentEntry {
|
|
|
|
return value, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// try system configs
|
|
|
|
for _, config := range this.data.system {
|
|
|
|
if value, ok := config[key]; ok {
|
|
|
|
return value, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// use fallback
|
|
|
|
return fallback, nil
|
2024-08-22 14:03:05 -06:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *config) Get (key string, fallback Value) (Value, error) {
|
|
|
|
this.lock.Lock()
|
|
|
|
defer this.lock.Unlock()
|
|
|
|
return this.get(key, fallback)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *config) Close () error {
|
|
|
|
this.lock.Lock()
|
|
|
|
defer this.lock.Unlock()
|
|
|
|
this.open = false
|
|
|
|
return this.watcher.Close()
|
2024-08-22 11:38:12 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func (this *config) GetString (key string, fallback string) (string, error) {
|
2024-08-22 14:03:05 -06:00
|
|
|
this.lock.Lock()
|
|
|
|
defer this.lock.Unlock()
|
|
|
|
value, err := this.get(key, ValueString(fallback))
|
2024-08-22 11:38:12 -06:00
|
|
|
if err != nil { return "", err }
|
|
|
|
if value, ok := value.(ValueString); ok {
|
|
|
|
return string(value), nil
|
|
|
|
}
|
|
|
|
return fallback, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *config) GetNumber (key string, fallback float64) (float64, error) {
|
2024-08-22 14:03:05 -06:00
|
|
|
this.lock.Lock()
|
|
|
|
defer this.lock.Unlock()
|
|
|
|
value, err := this.get(key, ValueNumber(fallback))
|
2024-08-22 11:38:12 -06:00
|
|
|
if err != nil { return 0, err }
|
|
|
|
if value, ok := value.(ValueNumber); ok {
|
|
|
|
return float64(value), nil
|
|
|
|
}
|
|
|
|
return fallback, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *config) GetBool (key string, fallback bool) (bool, error) {
|
2024-08-22 14:03:05 -06:00
|
|
|
this.lock.Lock()
|
|
|
|
defer this.lock.Unlock()
|
|
|
|
value, err := this.get(key, ValueBool(fallback))
|
2024-08-22 11:38:12 -06:00
|
|
|
if err != nil { return false, err }
|
|
|
|
if value, ok := value.(ValueBool); ok {
|
|
|
|
return bool(value), nil
|
|
|
|
}
|
|
|
|
return fallback, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *config) Set (key string, value Value) error {
|
2024-08-22 14:03:05 -06:00
|
|
|
this.lock.Lock()
|
|
|
|
defer this.lock.Unlock()
|
2024-08-22 11:38:12 -06:00
|
|
|
if this.data.user == nil { this.data.user = NewFile() }
|
|
|
|
err := this.data.user.Set(key, value)
|
|
|
|
if err != nil { return err }
|
|
|
|
return this.saveUser()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *config) Reset (key string) error {
|
2024-08-22 14:03:05 -06:00
|
|
|
this.lock.Lock()
|
|
|
|
defer this.lock.Unlock()
|
2024-08-22 11:38:12 -06:00
|
|
|
if this.data.user == nil { this.data.user = NewFile() }
|
|
|
|
err := this.data.user.Reset(key)
|
|
|
|
if err != nil { return err }
|
|
|
|
return this.saveUser()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *config) OnChange (callback func (string)) event.Cookie {
|
2024-08-22 14:03:05 -06:00
|
|
|
this.lock.Lock()
|
|
|
|
defer this.lock.Unlock()
|
2024-08-22 11:38:12 -06:00
|
|
|
return this.on.change.Connect(callback)
|
|
|
|
}
|