package main import ( "log" "os" "path" "strings" "sync/atomic" "github.com/fsnotify/fsnotify" ) type Lang struct { Code string Name string } // Wrapper over atomic.Value; contains []string type atomicStringSlice struct { v atomic.Value } // Wrapper over atomic.Value; contains []Lang type atomicLangSlice struct { v atomic.Value } // A config that is just a list of lines of text type lineListConfig struct { path string items atomicStringSlice } // A config that is list of two-column lines sepparated by a tab character type langListConfig struct { path string langs atomicLangSlice } // Interface for all types of configs that can be updated/reloaded type updatableConfig interface { update() error } // The data type that will watch for changes in some updatableConfig types and // call their update() methods on changes type configWatcher struct { lists map[string]updatableConfig watcher *fsnotify.Watcher dirpath string } var watcher configWatcher func (v *atomicStringSlice) Load() []string { return v.v.Load().([]string) } func (v *atomicStringSlice) Store(val []string) { v.v.Store(val) } func (v *atomicLangSlice) Load() []Lang { return v.v.Load().([]Lang) } func (v *atomicLangSlice) Store(val []Lang) { v.v.Store(val) } // Re-read the file of this and update the items list. func (c *lineListConfig) update() error { data, err := os.ReadFile(c.path) if err != nil { return err } // Cut the string into substrings line by line omitting empty lines. s := string(data) sep := "\n" items := make([]string, 0, 8) for { if i := strings.Index(s, sep); i >= 0 { sub := s[:i] if len(sub) > 0 { items = append(items, sub) } s = s[i+len(sep):] continue } break } c.items.Store(items) return nil } // Return the slice of strings atomically func (c *lineListConfig) getList() []string { return c.items.Load() } // Re-read the file of this and update the items list. func (c *langListConfig) update() error { data, err := os.ReadFile(c.path) if err != nil { return err } // Cut the string into substring line by line omitting empty lines, and // sepparating the code and the name of the language. s := string(data) sep := "\n" langs := make([]Lang, 0, 8) for { if i := strings.Index(s, sep); i >= 0 { sub := s[:i] if len(sub) > 0 { cols := strings.SplitN(sub, "\t", 2) if len(cols) < 2 { log.Println("WARNING: bad language format:", sub) } else { langs = append(langs, Lang{ cols[0], cols[1] }) } } s = s[i+len(sep):] continue } break } c.langs.Store(langs) return nil } // Return the slice of strings atomically func (c *langListConfig) getList() []Lang { return c.langs.Load() } // Make a new lineListConfig from the file that is inside the configWatcher dir // with the name give in this method. The name of the file will be automatically // watched by the configWatcher. Returns a new instance of a lineListConfig if // there were no errors. func newLineListConfig(name string) (*lineListConfig, error) { var err error path := path.Join(watcher.dirpath, name) c := &lineListConfig{path: path} err = c.update() watcher.lists[path] = c return c, err } // Make a new langListConfig; the same story as with lineListConfig func newLangListConfig(name string) (*langListConfig, error) { var err error path := path.Join(watcher.dirpath, name) c := &langListConfig{path: path} err = c.update() watcher.lists[path] = c return c, err } // Initialize and start the config watcher. func startConfigWatcher(path string) error { var err error watcher.lists = make(map[string]updatableConfig) watcher.dirpath = path watcher.watcher, err = fsnotify.NewWatcher() if err != nil { return err } err = watcher.watcher.Add(path) if err != nil { return err } go func() { for { select { case e, ok := <-watcher.watcher.Events: if !ok { return } if e.Has(fsnotify.Write) { cl, ok := watcher.lists[e.Name] if ok { if err := cl.update(); err != nil { log.Println( "read error: couldn't update", e.Name, ":", err, ) break } log.Println("reloaded config list from file:", e.Name) } } case err, ok := <-watcher.watcher.Errors: if !ok { return } log.Println("watcher error:", err) } } }() return err } // Stop the configList watcher. func stopConfigWatcher() { err := watcher.watcher.Close() if err != nil { log.Println("watcher error: on close:", err) } }