package config import "os" import "log" import "sync" import "slices" import "path/filepath" import "github.com/fsnotify/fsnotify" import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo/event" // 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. type config struct { open bool watcher *fsnotify.Watcher lock sync.RWMutex ignoreNextUserUpdate bool paths struct { user string system []string watching map[string] struct { } } data struct { user *File 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 } go func () { for event := range conf.watcher.Events { conf.lockAndProcessEvent(event) } } () return conf, nil } // 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 | fsnotify.Create)) { return } if _, ok := this.paths.watching[event.Name]; !ok { return } if event.Name == this.paths.user { if !this.ignoreNextUserUpdate { previousUser := this.data.user this.reloadUser() newUser := this.data.user this.processUserDiff(newUser.Diff(previousUser)) } this.ignoreNextUserUpdate = false } else { index := slices.Index(this.paths.system, event.Name) if index > 0 { previousSystem := this.data.system[index] this.reloadSystem(index) newSystem := this.data.system[index] this.processSystemDiff(index, diffValueMaps(newSystem, previousSystem)) } } } 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 } this.ignoreNextUserUpdate = true return nil } func (this *config) processUserDiff (changed map[string] struct { }) { for key := range changed { // this is the user file, and nothing has precedence over it, so // the change always matters this.broadcastChange(key) } } func (this *config) processSystemDiff (index int, changed map[string] struct { }) { for key := range changed { // if specified in the user file, the change doesn't matter if this.data.user != nil { if has, _ := this.data.user.Has(key); has { continue } } // if specified in any system files with precedence greater than // this one, the change doesn't matter for _, system := range this.data.system[:index] { if _, has := system[key]; has { continue } } // the change does matter this.broadcastChange(key) } } func (this *config) broadcastChange (key string) { for _, listener := range this.on.change.Listeners() { tomo.Do(func () { listener(key) }) } } func (this *config) get (key string, fallback Value) (Value, error) { // 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 } 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() } func (this *config) GetString (key string, fallback string) (string, error) { this.lock.Lock() defer this.lock.Unlock() value, err := this.get(key, ValueString(fallback)) 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) { this.lock.Lock() defer this.lock.Unlock() value, err := this.get(key, ValueNumber(fallback)) 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) { this.lock.Lock() defer this.lock.Unlock() value, err := this.get(key, ValueBool(fallback)) 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 { this.lock.Lock() defer this.lock.Unlock() if this.data.user == nil { this.data.user = NewFile() } err := this.data.user.Set(key, value) if err != nil { return err } err = this.saveUser() if err != nil { return err } this.broadcastChange(key) return nil } func (this *config) Reset (key string) error { this.lock.Lock() defer this.lock.Unlock() if this.data.user == nil { this.data.user = NewFile() } err := this.data.user.Reset(key) if err != nil { return err } err = this.saveUser() if err != nil { return err } this.broadcastChange(key) return nil } func (this *config) OnChange (callback func (string)) event.Cookie { this.lock.Lock() defer this.lock.Unlock() return this.on.change.Connect(callback) } func diffValueMaps (first, second map[string] Value) map[string] struct { } { diff := make(map[string] struct { }) // - keys only first has // - keys both have, but are different for key, firstValue := range first { secondValue, ok := second[key] if !ok { diff[key] = struct { } { } continue } if !firstValue.Equals(secondValue) { diff[key] = struct { } { } } } // - keys only second has for key := range second { if _, has := first[key]; !has { diff[key] = struct { } { } } } return diff }