package main import ( "fmt" "os" "sync" "github.com/fsnotify/fsnotify" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" ) var Cfg *Config type Config struct { General struct { DeleteLogFileAtStart bool `yaml:"delete_log_file_at_start"` LogToFile bool `yaml:"log_to_file"` Callsign string `yaml:"callsign"` LogLevel string `yaml:"log_level"` TelnetServer bool `yaml:"telnetserver"` FlexRadioSpot bool `yaml:"flexradiospot"` SendFreqModeToLog bool `yaml:"sendFreqModeToLog4OM"` SpotColorNewEntity string `yaml:"spot_color_new_entity"` BackgroundColorNewEntity string `yaml:"background_color_new_entity"` SpotColorNewBand string `yaml:"spot_color_new_band"` BackgroundColorNewBand string `yaml:"background_color_new_band"` SpotColorNewMode string `yaml:"spot_color_new_mode"` BackgroundColorNewMode string `yaml:"background_color_new_mode"` SpotColorNewBandMode string `yaml:"spot_color_new_band_mode"` BackgroundColorNewBandMode string `yaml:"background_color_new_band_mode"` SpotColorNewSlot string `yaml:"spot_color_new_slot"` BackgroundColorNewSlot string `yaml:"background_color_new_slot"` SpotColorMyCallsign string `yaml:"spot_color_my_callsign"` BackgroundColorMyCallsign string `yaml:"background_color_my_callsign"` SpotColorWorked string `yaml:"spot_color_worked"` BackgroundColorWorked string `yaml:"background_color_worked"` } `yaml:"general"` Database struct { MySQL bool `yaml:"mysql"` SQLite bool `yaml:"sqlite"` MySQLUser string `yaml:"mysql_db_user"` MySQLPassword string `yaml:"mysql_db_password"` MySQLDbName string `yaml:"mysql_db_name"` MySQLHost string `yaml:"mysql_host"` MySQLPort string `yaml:"mysql_port"` } `yaml:"database"` SQLite struct { SQLitePath string `yaml:"sqlite_path"` } `yaml:"sqlite"` Cluster struct { Server string `yaml:"server"` Port string `yaml:"port"` Login string `yaml:"login"` Password string `yaml:"password"` Skimmer bool `yaml:"skimmer"` FT8 bool `yaml:"ft8"` FT4 bool `yaml:"ft4"` Beacon bool `yaml:"beacon"` Command string `yaml:"command"` LoginPrompt string `yaml:"login_prompt"` } `yaml:"cluster"` Flex struct { Discover bool `yaml:"discovery"` IP string `yaml:"ip"` SpotLife string `yaml:"spot_life"` } `yaml:"flex"` TelnetServer struct { Host string `yaml:"host"` Port string `yaml:"port"` } `yaml:"telnetserver"` Gotify struct { Enable bool `yaml:"enable"` URL string `yaml:"url"` Token string `yaml:"token"` NewDXCC bool `yaml:"NewDXCC"` NewBand bool `yaml:"NewBand"` NewMode bool `yaml:"NewMode"` NewBandAndMode bool `yaml:"NewBandAndMode"` WatchList bool `yaml:"Watchlist"` } `yaml:"gotify"` } type ConfigWatcher struct { watcher *fsnotify.Watcher configPath string mu sync.RWMutex } func NewConfig(configPath string) *Config { Cfg = &Config{} file, err := os.Open(configPath) if err != nil { log.Println("could not open config file") } defer file.Close() d := yaml.NewDecoder(file) if err := d.Decode(&Cfg); err != nil { log.Println("could not decode config file") } return Cfg } func ValidateConfigPath(path string) error { s, err := os.Stat(path) if err != nil { return err } if s.IsDir() { return fmt.Errorf("'%s' is a directory, not a normal file", path) } return nil } func NewConfigWatcher(configPath string) (*ConfigWatcher, error) { watcher, err := fsnotify.NewWatcher() if err != nil { return nil, err } return &ConfigWatcher{ watcher: watcher, configPath: configPath, }, nil } func (cw *ConfigWatcher) Start() error { if err := cw.watcher.Add(cw.configPath); err != nil { return err } go func() { for { select { case event, ok := <-cw.watcher.Events: if !ok { return } if event.Op&fsnotify.Write == fsnotify.Write { Log.Info("Config file modified, reloading...") cw.reloadConfig() } case err, ok := <-cw.watcher.Errors: if !ok { return } Log.Errorf("Config watcher error: %v", err) } } }() return nil } func (cw *ConfigWatcher) reloadConfig() { cw.mu.Lock() defer cw.mu.Unlock() newCfg := &Config{} file, err := os.Open(cw.configPath) if err != nil { Log.Errorf("Could not reload config: %v", err) return } defer file.Close() d := yaml.NewDecoder(file) if err := d.Decode(newCfg); err != nil { Log.Errorf("Could not decode reloaded config: %v", err) return } // Sauvegarder l'ancienne config oldCfg := Cfg // Appliquer la nouvelle config Cfg = newCfg // Vérifier les changements qui nécessitent des actions cw.applyConfigChanges(oldCfg, newCfg) Log.Info("✅ Config reloaded successfully") } func (cw *ConfigWatcher) applyConfigChanges(oldCfg, newCfg *Config) { // Log level if oldCfg.General.LogLevel != newCfg.General.LogLevel { switch newCfg.General.LogLevel { case "DEBUG": Log.SetLevel(log.DebugLevel) case "INFO": Log.SetLevel(log.InfoLevel) case "WARN": Log.SetLevel(log.WarnLevel) default: Log.SetLevel(log.InfoLevel) } Log.Infof("Log level changed to %s", newCfg.General.LogLevel) } // Gotify if oldCfg.Gotify.Enable != newCfg.Gotify.Enable { Log.Infof("Gotify notifications %s", map[bool]string{true: "enabled", false: "disabled"}[newCfg.Gotify.Enable]) } if oldCfg.Cluster.FT8 != newCfg.Cluster.FT8 || oldCfg.Cluster.FT4 != newCfg.Cluster.FT4 || oldCfg.Cluster.Skimmer != newCfg.Cluster.Skimmer || oldCfg.Cluster.Beacon != newCfg.Cluster.Beacon { Log.Info("Cluster filters changed, applying") httpServerInstance.TCPClient.ReloadFilters() } } func (cw *ConfigWatcher) Stop() { cw.watcher.Close() }