diff --git a/main.go b/main.go index 9ed9c3d..ac2d55a 100644 --- a/main.go +++ b/main.go @@ -13,10 +13,12 @@ import ( "net/http" "os" "os/exec" + "os/signal" "path/filepath" "runtime" "strings" "sync" + "syscall" "time" ) @@ -62,6 +64,8 @@ var ( taskIndex = map[string]*Task{} started time.Time csrfKey []byte + cfgPath = "tasks.json" // <- Dateipfad zentral + cfgMu sync.Mutex // <- schützt Speichervorgang ) //go:embed web/* @@ -329,6 +333,11 @@ func handleSetInterval(w http.ResponseWriter, r *http.Request) { return } t.setInterval(interval, enable) + + // PERSISTENZ + // auch die Werte im cfg.Tasks stecken bereits in t, also genügt: + persistOrLog() + io.WriteString(w, "OK") } @@ -344,6 +353,10 @@ func handleToggle(w http.ResponseWriter, r *http.Request) { t.Enabled = enable t.mutex.Unlock() t.scheduleNext() + + // PERSISTENZ + persistOrLog() + io.WriteString(w, "OK") } @@ -399,16 +412,62 @@ func handleLogs(w http.ResponseWriter, r *http.Request) { /* ====== Init/Load/Serve ====== */ -func loadConfig() { - f, err := os.Open("tasks.json") +func saveConfigAtomic() error { + cfgMu.Lock() + defer cfgMu.Unlock() + + // Schön formatiert schreiben + b, err := json.MarshalIndent(cfg, "", " ") if err != nil { - log.Fatalf("tasks.json nicht gefunden: %v", err) + return err + } + + // In dasselbe Verzeichnis wie cfgPath schreiben (wichtig für Rename-Atomizität) + dir := filepath.Dir(cfgPath) + base := filepath.Base(cfgPath) + tmp := filepath.Join(dir, "."+base+".tmp") + + // Temp-Datei erzeugen, schreiben, flushen + f, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return err + } + if _, err := f.Write(b); err != nil { + f.Close() + return err + } + if err := f.Sync(); err != nil { + f.Close() + return err + } + if err := f.Close(); err != nil { + return err + } + + // Atomisch ersetzen + if err := os.Rename(tmp, cfgPath); err != nil { + return err + } + return nil +} + +// bequemer Helfer zum Loggen statt Abbrechen +func persistOrLog() { + if err := saveConfigAtomic(); err != nil { + log.Printf("WARN: Konnte %s nicht speichern: %v", cfgPath, err) + } +} + +func loadConfig() { + f, err := os.Open(cfgPath) + if err != nil { + log.Fatalf("%s nicht gefunden: %v", cfgPath, err) } defer f.Close() dec := json.NewDecoder(f) dec.DisallowUnknownFields() if err := dec.Decode(&cfg); err != nil { - log.Fatalf("tasks.json fehlerhaft: %v", err) + log.Fatalf("%s fehlerhaft: %v", cfgPath, err) } if cfg.Listen == "" { @@ -495,8 +554,25 @@ func main() { Handler: basicAuth(mux), ReadHeaderTimeout: 10 * time.Second, } + + // GRACEFUL SHUTDOWN + PERSISTENZ + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM) + go func() { + <-stop + log.Println("Beende... speichere Konfiguration.") + persistOrLog() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _ = server.Shutdown(ctx) + }() + log.Printf("Weboberfläche: http://%s (Basic Auth aktiv)", addr) - log.Fatal(server.ListenAndServe()) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatal(err) + } + log.Println("Server gestoppt.") } /* ====== Embedded HTML (Tailwind-lite via CDN) ====== */ diff --git a/tasks.json b/tasks.json index 7db79d7..87a635e 100644 --- a/tasks.json +++ b/tasks.json @@ -3,21 +3,19 @@ "username": "admin", "password": "change-me", "scripts_dir": "C:\\scripts", - "csrf_secret": "ersetzenMitEigenemSecret", "tasks": [ { "name": "Nacht-Backup", "path": "C:\\scripts\\backup.ps1", "interval": "24h", - "enabled": true, "timeout": "2h" }, { "name": "IIS-Log-Rotation", "path": "C:\\scripts\\rotate.ps1", - "interval": "0", - "enabled": false, + "interval": "5m", "timeout": "30m" } - ] -} + ], + "csrf_secret": "ersetzenMitEigenemSecret" +} \ No newline at end of file