package settings import ( "bytes" "context" "errors" "fmt" "io" "log" "os" "path/filepath" "strings" "sync" "sync/atomic" "time" ) var ( MaintenanceRoutinePace = time.Second WarnIfFileNotFound = false TheSettings = NewSettings("settings.conf", true) ) type Settings struct { mutex sync.RWMutex contents map[string]string filename string modtime time.Time LogUpdates atomic.Bool } func (s *Settings) GetFilename() string { s.mutex.RLock() defer s.mutex.RUnlock() return s.filename } func (s *Settings) SetFilename(filename string) (updated bool) { s.mutex.Lock() defer s.mutex.Unlock() if filename == s.filename { return false } if s.LogUpdates.Load() { log.Printf("Settings::SetFilename changing from '%s' to '%s'\n", s.filename, filename) } s.filename = strings.Clone(filename) s.modtime = time.Time{} err := s.lockedUpdate() if err != nil { if !errors.Is(err, os.ErrNotExist) || WarnIfFileNotFound { log.Printf("Settings::SetFilename %s: error from lockedUpdate: %v\n", filepath.Base(s.filename), err) } } return true } func (s *Settings) lockedSetKeyValue(key, value string) bool { logUpdates := s.LogUpdates.Load() baseFilename := filepath.Base(s.filename) if stored, found := s.contents[key]; found && stored == value { return false } else if found && logUpdates { log.Printf("Settings::lockedKeyValue %s: updating key '%s' from value '%s' to '%s'\n", baseFilename, key, stored, value) } else if !found && logUpdates { log.Printf("Settings::lockedKeyValue %s: storing key '%s' with value '%s'\n", baseFilename, key, value) } s.contents[strings.Clone(key)] = strings.Clone(value) return true } func (s *Settings) SetKeyValue(key, value string) (updated bool) { s.mutex.Lock() defer s.mutex.Unlock() return s.lockedSetKeyValue(key, value) } func (s *Settings) lockedContainsKey(key string) bool { _, found := s.contents[key] return found } func (s *Settings) ContainsKey(key string) bool { s.mutex.RLock() defer s.mutex.RUnlock() return s.lockedContainsKey(key) } func (s *Settings) lockedGetKeyValue(key string) string { return s.contents[key] } func (s *Settings) GetKeyValue(key string) string { s.mutex.RLock() defer s.mutex.RUnlock() return s.lockedGetKeyValue(key) } func (s *Settings) lockedUpdate() error { stat, err := os.Stat(s.filename) if err != nil { return fmt.Errorf("error getting os.FileInfo for %s: %w", s.filename, err) } else if !stat.ModTime().After(s.modtime) { return nil } contents, err := os.ReadFile(s.filename) if err != nil { return fmt.Errorf("error reading %s: %w", s.filename, err) } scanner := NewScanner(bytes.NewReader(contents)) loop: for { key, value, err := scanner.ReadNextLine() if err != nil { if !errors.Is(err, io.EOF) { log.Printf("error from scanner.ReadNextLine: %v\n", err) } else { break loop } } s.lockedSetKeyValue(key, value) } s.modtime = stat.ModTime() return nil } func (s *Settings) Update() error { s.mutex.Lock() defer s.mutex.Unlock() return s.lockedUpdate() } func (s *Settings) maintenanceRoutine(ctx context.Context) { for { select { case <-ctx.Done(): return case <-time.After(MaintenanceRoutinePace): err := s.Update() if err != nil { if !errors.Is(err, os.ErrNotExist) || WarnIfFileNotFound { log.Printf("Settings::maintenanceRoutine %s error from Update: %v\n", filepath.Base(s.GetFilename()), err) } } } } } func NewSettings(filename string, logUpdates bool) *Settings { return NewSettingsWithContext(context.Background(), filename, logUpdates) } func NewSettingsWithContext(ctx context.Context, filename string, logUpdates bool) *Settings { settings := &Settings{ contents: make(map[string]string), filename: strings.Clone(filename), } settings.LogUpdates.Store(logUpdates) if err := settings.lockedUpdate(); err != nil && (!errors.Is(err, os.ErrNotExist) || WarnIfFileNotFound) { log.Printf("NewSettingsWithContext %s error from initial update: %v\n", filepath.Base(filename), err) } go settings.maintenanceRoutine(ctx) return settings } func ParseBool(value string) (bool, error) { switch strings.ToLower(strings.TrimSpace(value)) { case "1", "true", "t", "yes", "y", "on": return true, nil case "0", "false", "f", "no", "n", "off": return false, nil default: return false, fmt.Errorf("cannot parse '%s' as bool", value) } }