package main import ( "encoding/json" "fmt" "io/ioutil" "net/http" "os" "strings" "sync" "time" "github.com/fsnotify/fsnotify" ) type Config struct { UpdateURL string `json:"update_url"` CheckIntervalSecs int `json:"check_interval_seconds"` IPCheckURL string `json:"ip_check_url"` } func loadConfig(path string) (*Config, error) { data, err := ioutil.ReadFile(path) if err != nil { return nil, err } var cfg Config if err := json.Unmarshal(data, &cfg); err != nil { return nil, err } return &cfg, nil } type configHolder struct { mu sync.RWMutex cfg *Config } func (h *configHolder) get() *Config { h.mu.RLock() defer h.mu.RUnlock() return h.cfg } func (h *configHolder) set(cfg *Config) { h.mu.Lock() defer h.mu.Unlock() h.cfg = cfg } func watchConfig(path string, holder *configHolder, tickerReset chan<- time.Duration) { watcher, err := fsnotify.NewWatcher() if err != nil { fmt.Println("Config watcher: failed to create watcher:", err) return } defer watcher.Close() if err := watcher.Add(path); err != nil { fmt.Println("Config watcher: failed to watch file:", err) return } // Debounce timer — editors often fire multiple events per save // (truncate then write, or swapping a temp file in). const debounce = 100 * time.Millisecond var debounceTimer *time.Timer reload := func() { newCfg, err := loadConfig(path) if err != nil { fmt.Println("Config watcher: failed to reload config:", err) return } oldCfg := holder.get() holder.set(newCfg) fmt.Println(time.Now(), "Config reloaded") if newCfg.CheckIntervalSecs != oldCfg.CheckIntervalSecs { fmt.Printf("%v Check interval changed: %ds → %ds\n", time.Now(), oldCfg.CheckIntervalSecs, newCfg.CheckIntervalSecs) tickerReset <- time.Duration(newCfg.CheckIntervalSecs) * time.Second } } for { select { case event, ok := <-watcher.Events: if !ok { return } // Write and Create cover both in-place saves and atomic // rename-based saves (used by vim, many editors on Linux). if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) { // If it was an atomic rename the old watch target is gone; // re-add the new file under the same path. if event.Has(fsnotify.Create) { _ = watcher.Add(path) } if debounceTimer != nil { debounceTimer.Stop() } debounceTimer = time.AfterFunc(debounce, reload) } case err, ok := <-watcher.Errors: if !ok { return } fmt.Println("Config watcher: error:", err) } } } func getPublicIP(ipURL string) (string, error) { resp, err := http.Get(ipURL) if err != nil { return "", err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return "", err } return strings.TrimSpace(string(body)), nil } func updateDDNS(url string) error { resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("failed, status code %d", resp.StatusCode) } return nil } func main() { print("Trying out some prints to see what's going on\n"); const configPath = "config.json" cfg, err := loadConfig(configPath) if err != nil { fmt.Println("Failed to load config:", err) os.Exit(1) } holder := &configHolder{cfg: cfg} tickerReset := make(chan time.Duration, 1) go watchConfig(configPath, holder, tickerReset) lastIP, err := getPublicIP(cfg.IPCheckURL) if err != nil { fmt.Println("Error getting initial public IP:", err) lastIP = "" } else { fmt.Println(time.Now(), "Starting with current IP:", lastIP) } if err := updateDDNS(holder.get().UpdateURL); err != nil { fmt.Println("Failed to update DDNS:", err) } else { fmt.Println(time.Now(), "DDNS updated successfully") } ticker := time.NewTicker(time.Duration(cfg.CheckIntervalSecs) * time.Second) defer ticker.Stop() for { select { case newInterval := <-tickerReset: ticker.Reset(newInterval) continue case <-ticker.C: current := holder.get() ip, err := getPublicIP(current.IPCheckURL) if err != nil { fmt.Println("Error getting public IP:", err) continue } if ip == lastIP { fmt.Println(time.Now(), "IP unchanged:", ip) continue } fmt.Println(time.Now(), "IP changed to", ip) if err := updateDDNS(current.UpdateURL); err != nil { fmt.Println("Failed to update DDNS:", err) } else { fmt.Println(time.Now(), "DDNS updated successfully") lastIP = ip } } } }