package updater import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "runtime" "time" ) // ReleaseInfo enthält Informationen über die aktuell verfügbare Agent-Version. type ReleaseInfo struct { Version string `json:"version"` DownloadUrl string `json:"downloadUrl"` Checksum string `json:"checksum"` // SHA256 hex } // CheckForUpdate fragt den Backend-Server nach der aktuellen Version. // serverAddress: z.B. "localhost:5000" (REST Port, nicht gRPC) func CheckForUpdate(ctx context.Context, serverAddress, currentVersion string) (*ReleaseInfo, error) { url := fmt.Sprintf("http://%s/api/v1/agent/releases/latest?os=%s&arch=%s", serverAddress, runtime.GOOS, runtime.GOARCH) httpCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() req, err := http.NewRequestWithContext(httpCtx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("creating request: %w", err) } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("fetching release info: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status %d from release endpoint", resp.StatusCode) } var info ReleaseInfo if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { return nil, fmt.Errorf("decoding release info: %w", err) } if info.Version == currentVersion { return nil, nil // bereits aktuell } return &info, nil } // Update lädt die neue Version herunter und startet einen Austausch-Prozess. // Nach erfolgreichem Download wird das neue Binary neben das aktuelle gelegt // und ein Neustartprozess gestartet. func Update(ctx context.Context, info *ReleaseInfo) error { binaryPath, err := os.Executable() if err != nil { return fmt.Errorf("determining executable path: %w", err) } // Download der neuen Version dlCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) defer cancel() req, err := http.NewRequestWithContext(dlCtx, http.MethodGet, info.DownloadUrl, nil) if err != nil { return fmt.Errorf("creating download request: %w", err) } resp, err := http.DefaultClient.Do(req) if err != nil { return fmt.Errorf("downloading new binary: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("unexpected status %d during download", resp.StatusCode) } fileBytes, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("reading download body: %w", err) } // SHA256-Prüfung (überspringen wenn Checksum leer = Dev-Modus) if info.Checksum != "" { sum := sha256.Sum256(fileBytes) got := hex.EncodeToString(sum[:]) if got != info.Checksum { return fmt.Errorf("checksum mismatch: expected %s, got %s", info.Checksum, got) } } newBinaryPath := binaryPath + ".new" if err := os.WriteFile(newBinaryPath, fileBytes, 0755); err != nil { return fmt.Errorf("writing new binary: %w", err) } // Platform-spezifischer Neustart if runtime.GOOS == "windows" { return restartWindows(binaryPath, newBinaryPath) } return restartUnix(binaryPath, newBinaryPath) } func restartWindows(binaryPath, newBinaryPath string) error { batchContent := fmt.Sprintf(`@echo off timeout /t 2 /nobreak > nul move /y "%s" "%s" start "" "%s" del "%%~f0" `, newBinaryPath, binaryPath, binaryPath) batchPath := filepath.Join(os.TempDir(), "nexus-agent-update.bat") if err := os.WriteFile(batchPath, []byte(batchContent), 0644); err != nil { return fmt.Errorf("writing update batch script: %w", err) } if err := exec.Command("cmd", "/c", "start", "", batchPath).Start(); err != nil { return fmt.Errorf("starting update batch script: %w", err) } os.Exit(0) return nil } func restartUnix(binaryPath, newBinaryPath string) error { scriptContent := fmt.Sprintf(`#!/bin/sh sleep 2 mv -f "%s" "%s" "%s" & rm -f "$0" `, newBinaryPath, binaryPath, binaryPath) scriptPath := filepath.Join(os.TempDir(), "nexus-agent-update.sh") if err := os.WriteFile(scriptPath, []byte(scriptContent), 0755); err != nil { return fmt.Errorf("writing update shell script: %w", err) } if err := exec.Command("sh", scriptPath).Start(); err != nil { return fmt.Errorf("starting update shell script: %w", err) } os.Exit(0) return nil }