go-proxy/config_runtime.go
Virgil 48c6e0fc6d feat(proxy): implement RFC runtime primitives
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 10:39:59 +00:00

115 lines
2.1 KiB
Go

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
}
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()
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)
}
}