// 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 }