package proxy import ( "encoding/json" "errors" "os" "strings" "time" ) // LoadConfig reads and unmarshals a JSON config file. // // 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 required fields. // // 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 builds a polling watcher for the config file. // // 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 the polling goroutine. // // 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.lastMod = info.ModTime() } for { select { case <-ticker.C: info, errorValue := os.Stat(w.path) if errorValue != nil { continue } if !info.ModTime().After(w.lastMod) { continue } w.lastMod = info.ModTime() config, errorValue := LoadConfig(w.path) if errorValue == nil && w.onChange != nil { w.onChange(config) } case <-w.done: return } } }() } // Stop ends the polling goroutine cleanly. // // w.Stop() func (w *ConfigWatcher) Stop() { if w == nil || w.done == nil { return } select { case <-w.done: default: close(w.done) } }