diff --git a/Makefile b/Makefile index e7fe911..d62e206 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,14 @@ APP_NAME = DDNSUpdater -BIN_DIR = bin -VERSION = 0.1.0 +BIN_DIR = bin +VERSION = 0.1.0 +OUT = $(BIN_DIR)\$(APP_NAME).exe +FLAGS = -trimpath -ldflags="-s -w -X main.version=$(VERSION)" all: build run -# Default build build: @if not exist $(BIN_DIR) mkdir $(BIN_DIR) - set CGO_ENABLED=0 && \ - set GOOS=windows && set GOARCH=amd64 && \ - go build -ldflags="-s -w -trimpath -X main.version=$(VERSION)" -o $(BIN_DIR)\$(APP_NAME).exe . + cmd /c "set CGO_ENABLED=0 && go build $(FLAGS) -o $(OUT) ." run: go run . @@ -17,13 +16,6 @@ run: clean: @if exist $(BIN_DIR) rmdir /s /q $(BIN_DIR) -# Windows-specific optimized build -windows: - @if not exist $(BIN_DIR) mkdir $(BIN_DIR) - set CGO_ENABLED=0 && \ - set GOOS=windows && set GOARCH=amd64 && \ - go build -ldflags="-s -w -trimpath -X main.version=$(VERSION)" -o $(BIN_DIR)\$(APP_NAME).exe . - tidy: go mod tidy diff --git a/bin/DDNSUpdater.exe b/bin/DDNSUpdater.exe index 062d665..b690059 100644 Binary files a/bin/DDNSUpdater.exe and b/bin/DDNSUpdater.exe differ diff --git a/go.mod b/go.mod index 70dd311..9749e24 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,7 @@ module triztech/hamza-zakaria/DDNSUpdater go 1.25.5 + +require github.com/fsnotify/fsnotify v1.9.0 + +require golang.org/x/sys v0.13.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c1e3272 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go index bb05e3e..9d26c00 100644 --- a/main.go +++ b/main.go @@ -7,13 +7,16 @@ import ( "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"` + UpdateURL string `json:"update_url"` + CheckIntervalSecs int `json:"check_interval_seconds"` + IPCheckURL string `json:"ip_check_url"` } func loadConfig(path string) (*Config, error) { @@ -22,25 +25,103 @@ func loadConfig(path string) (*Config, error) { return nil, err } var cfg Config - err = json.Unmarshal(data, &cfg) - if err != nil { + 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 } @@ -50,7 +131,6 @@ func updateDDNS(url string) error { return err } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { return fmt.Errorf("failed, status code %d", resp.StatusCode) } @@ -58,16 +138,23 @@ func updateDDNS(url string) error { } func main() { - cfg, err := loadConfig("config.json") + 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) } - //var lastIP string + 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 intial public IP:", err) + fmt.Println("Error getting initial public IP:", err) lastIP = "" } else { fmt.Println(time.Now(), "Starting with current IP:", lastIP) @@ -77,21 +164,31 @@ func main() { defer ticker.Stop() for { - ip, err := getPublicIP(cfg.IPCheckURL) - if err != nil { - fmt.Println("Error getting public IP:", err) - } else if ip != lastIP { + 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(cfg.UpdateURL); err != nil { + if err := updateDDNS(current.UpdateURL); err != nil { fmt.Println("Failed to update DDNS:", err) } else { fmt.Println(time.Now(), "DDNS updated successfully") lastIP = ip } - } else { - fmt.Println(time.Now(), "IP unchanged:", ip) } - - <-ticker.C } }