From e927fff02fe4dfc5cddfdda86063acda5a5d95cb Mon Sep 17 00:00:00 2001 From: Vladimir Zagainov Date: Tue, 26 May 2026 12:39:38 +0300 Subject: [PATCH] feat: add selfupdate module (check, apply, restart) - Check: query /api/launcher/latest with os/arch, compare versions - Apply: download new binary, verify SHA-256, replace current executable (Windows .old rename) - Restart: re-launch executable and exit - Uses stdlib only (no external update library needed for direct URL downloads) Co-Authored-By: OWL --- internal/selfupdate/selfupdate.go | 145 +++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 1 deletion(-) diff --git a/internal/selfupdate/selfupdate.go b/internal/selfupdate/selfupdate.go index 034d714..f6ac9b0 100644 --- a/internal/selfupdate/selfupdate.go +++ b/internal/selfupdate/selfupdate.go @@ -1,2 +1,145 @@ -// package selfupdate handles launcher auto-updates via go-selfupdate. +// package selfupdate handles launcher auto-updates. package selfupdate + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "runtime" + + "gitea.mrixs.me/Mrixs/MrixsCraft-launcher/internal/config" +) + +// UpdateInfo describes the latest launcher version from the server. +type UpdateInfo struct { + Version string `json:"version"` + URL string `json:"url"` + SHA256 string `json:"sha256"` +} + +// Check queries /api/launcher/latest to see if a newer version is available. +func Check(currentVersion string) (*UpdateInfo, error) { + s, err := config.Load() + if err != nil { + return nil, fmt.Errorf("loading config: %w", err) + } + + latestURL := fmt.Sprintf("%s/api/launcher/latest?os=%s&arch=%s", + s.ServerURL, runtime.GOOS, runtime.GOARCH) + + resp, err := http.Get(latestURL) + if err != nil { + return nil, fmt.Errorf("querying latest version: %w", err) + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("reading response: %w", err) + } + + var info UpdateInfo + if err := json.Unmarshal(data, &info); err != nil { + return nil, fmt.Errorf("parsing update response: %w", err) + } + + if info.Version == currentVersion { + return nil, nil + } + return &info, nil +} + +// Apply downloads the new binary and replaces the current executable. +func Apply(info *UpdateInfo) error { + execPath, err := os.Executable() + if err != nil { + return fmt.Errorf("resolving executable path: %w", err) + } + + tmpPath := execPath + ".new" + if err := downloadFile(info.URL, tmpPath); err != nil { + return fmt.Errorf("downloading update: %w", err) + } + + if info.SHA256 != "" { + got, err := sha256File(tmpPath) + if err != nil { + os.Remove(tmpPath) + return fmt.Errorf("hashing download: %w", err) + } + if got != info.SHA256 { + os.Remove(tmpPath) + return fmt.Errorf("SHA-256 mismatch: expected %s, got %s", info.SHA256, got) + } + } + + if err := os.Chmod(tmpPath, 0o755); err != nil { + os.Remove(tmpPath) + return fmt.Errorf("chmod %s: %w", tmpPath, err) + } + + if runtime.GOOS == "windows" { + oldPath := execPath + ".old" + _ = os.Remove(oldPath) + if err := os.Rename(execPath, oldPath); err != nil { + os.Remove(tmpPath) + return fmt.Errorf("renaming current binary: %w", err) + } + } + + return os.Rename(tmpPath, execPath) +} + +// Restart re-launches the current executable and exits. +func Restart() error { + execPath, err := os.Executable() + if err != nil { + return err + } + proc, err := os.StartProcess(execPath, os.Args, &os.ProcAttr{ + Dir: filepath.Dir(execPath), + Env: os.Environ(), + Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, + }) + if err != nil { + return fmt.Errorf("restarting: %w", err) + } + _ = proc.Release() + os.Exit(0) + return nil +} + +func downloadFile(url, dest string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + f, err := os.Create(dest) + if err != nil { + return err + } + defer f.Close() + + _, err = io.Copy(f, resp.Body) + return err +} + +func sha256File(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + return fmt.Sprintf("%x", h.Sum(nil)), nil +}