package proxy import ( "encoding/json" "errors" "os" "strings" "time" ) // LoadConfig reads `config.json` and returns a validated `Config`. // // cfg, errorValue := proxy.LoadConfig("config.json") func LoadConfig(path string) (*Config, error) { data, errorValue := os.ReadFile(path) if errorValue != nil { return nil, errorValue } config := &Config{} if errorValue = json.Unmarshal(data, config); errorValue != nil { return nil, errorValue } config.sourcePath = path if errorValue = config.Validate(); errorValue != nil { return nil, errorValue } return config, nil } // Validate checks that `bind` and `pools` are present and every enabled pool has a URL. // // if errorValue := cfg.Validate(); errorValue != nil { return errorValue } func (c *Config) Validate() error { if c == nil { return errors.New("config is nil") } if len(c.Bind) == 0 { return errors.New("bind list is empty") } if len(c.Pools) == 0 { return errors.New("pool list is empty") } for _, poolConfig := range c.Pools { if poolConfig.Enabled && strings.TrimSpace(poolConfig.URL) == "" { return errors.New("enabled pool URL is empty") } } return nil } // NewConfigWatcher watches `config.json` and calls `p.Reload(cfg)` on change. // // w := proxy.NewConfigWatcher("config.json", func(cfg *proxy.Config) { p.Reload(cfg) }) func NewConfigWatcher(path string, onChange func(*Config)) *ConfigWatcher { return &ConfigWatcher{ path: path, onChange: onChange, done: make(chan struct{}), } } // Start begins 1-second polling for `config.json`. // // w.Start() func (w *ConfigWatcher) Start() { if w == nil { return } go func() { ticker := time.NewTicker(time.Second) defer ticker.Stop() if info, errorValue := os.Stat(w.path); errorValue == nil { w.lastModified = info.ModTime() } for { select { case <-ticker.C: info, errorValue := os.Stat(w.path) if errorValue != nil { continue } if !info.ModTime().After(w.lastModified) { continue } w.lastModified = info.ModTime() config, errorValue := LoadConfig(w.path) if errorValue == nil && w.onChange != nil { w.onChange(config) } case <-w.done: return } } }() } // Stop ends polling so the watcher can be shut down with `p.Stop()`. // // w.Stop() func (w *ConfigWatcher) Stop() { if w == nil || w.done == nil { return } select { case <-w.done: default: close(w.done) } }