From dfe6e0509b0db9e969d82d3e031dd044b4fd5ea8 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Thu, 24 Jul 2025 02:06:50 +0800 Subject: [PATCH] chore: rebuild core updater --- component/updater/cpu_amd64.go | 4 - component/updater/cpu_amd64.s | 22 -- component/updater/cpu_others.go | 8 - component/updater/cpu_test.go | 20 -- component/updater/update_core.go | 435 +++++++++++--------------- component/updater/update_core_test.go | 10 + component/updater/utils.go | 62 ---- constant/features/goflags.go | 24 ++ 8 files changed, 208 insertions(+), 377 deletions(-) delete mode 100644 component/updater/cpu_amd64.go delete mode 100644 component/updater/cpu_amd64.s delete mode 100644 component/updater/cpu_others.go delete mode 100644 component/updater/cpu_test.go create mode 100644 component/updater/update_core_test.go create mode 100644 constant/features/goflags.go diff --git a/component/updater/cpu_amd64.go b/component/updater/cpu_amd64.go deleted file mode 100644 index b3b29bbe..00000000 --- a/component/updater/cpu_amd64.go +++ /dev/null @@ -1,4 +0,0 @@ -package updater - -// getGOAMD64level is implemented in cpu_amd64.s. Returns number in [1,4]. -func getGOAMD64level() int32 diff --git a/component/updater/cpu_amd64.s b/component/updater/cpu_amd64.s deleted file mode 100644 index bfe068a8..00000000 --- a/component/updater/cpu_amd64.s +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -#include "textflag.h" - -// func getGOAMD64level() int32 -TEXT ·getGOAMD64level(SB),NOSPLIT,$0-4 -#ifdef GOAMD64_v4 - MOVL $4, ret+0(FP) -#else -#ifdef GOAMD64_v3 - MOVL $3, ret+0(FP) -#else -#ifdef GOAMD64_v2 - MOVL $2, ret+0(FP) -#else - MOVL $1, ret+0(FP) -#endif -#endif -#endif - RET diff --git a/component/updater/cpu_others.go b/component/updater/cpu_others.go deleted file mode 100644 index 7abf61c5..00000000 --- a/component/updater/cpu_others.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build !amd64 - -package updater - -// getGOAMD64level is always return 0 when not in amd64 platfrom. -func getGOAMD64level() int32 { - return 0 -} diff --git a/component/updater/cpu_test.go b/component/updater/cpu_test.go deleted file mode 100644 index b7359e0a..00000000 --- a/component/updater/cpu_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package updater - -import ( - "fmt" - "runtime" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGOAMD64level(t *testing.T) { - level := getGOAMD64level() - fmt.Printf("GOAMD64=%d\n", level) - if runtime.GOARCH == "amd64" { - assert.True(t, level > 0) - assert.True(t, level <= 4) - } else { - assert.Equal(t, level, int32(0)) - } -} diff --git a/component/updater/update_core.go b/component/updater/update_core.go index fd665b18..4e90bd0d 100644 --- a/component/updater/update_core.go +++ b/component/updater/update_core.go @@ -8,7 +8,6 @@ import ( "io" "net/http" "os" - "os/exec" "path/filepath" "runtime" "strings" @@ -17,79 +16,91 @@ import ( mihomoHttp "github.com/metacubex/mihomo/component/http" C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/constant/features" "github.com/metacubex/mihomo/log" ) -// modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/updater/updater.go -// Updater is the mihomo updater. -var ( - goarm string - gomips string - goamd64 string +const ( + baseReleaseURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/" + versionReleaseURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt" - workDir string + baseAlphaURL = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/" + versionAlphaURL = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt" - // mu protects all fields below. - mu sync.Mutex - - currentExeName string // 当前可执行文件 - updateDir string // 更新目录 - packageName string // 更新压缩文件 - backupDir string // 备份目录 - backupExeName string // 备份文件名 - updateExeName string // 更新后的可执行文件 - - baseURL string = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/mihomo" - versionURL string = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt" - packageURL string - latestVersion string + // MaxPackageFileSize is a maximum package file length in bytes. The largest + // package whose size is limited by this constant currently has the size of + // approximately 32 MiB. + MaxPackageFileSize = 32 * 1024 * 1024 ) +var mihomoBaseName string + func init() { - if runtime.GOARCH == "amd64" { - switch getGOAMD64level() { - case 1: - goamd64 = "-v1" - case 2: - goamd64 = "-v2" - case 3: - goamd64 = "-v3" + switch runtime.GOARCH { + case "arm": + // mihomo-linux-armv5 + mihomoBaseName = fmt.Sprintf("mihomo-%s-%sv%s", runtime.GOOS, runtime.GOARCH, features.GOARM) + case "arm64": + if runtime.GOOS == "android" { + // mihomo-android-arm64-v8 + mihomoBaseName = fmt.Sprintf("mihomo-%s-%s-v8", runtime.GOOS, runtime.GOARCH) + } else { + // mihomo-linux-arm64 + mihomoBaseName = fmt.Sprintf("mihomo-%s-%s", runtime.GOOS, runtime.GOARCH) } - } - if !strings.HasPrefix(C.Version, "alpha") { - baseURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/mihomo" - versionURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt" + case "mips", "mipsle": + // mihomo-linux-mips-hardfloat + mihomoBaseName = fmt.Sprintf("mihomo-%s-%s-%s", runtime.GOOS, runtime.GOARCH, features.GOMIPS) + case "amd64": + // mihomo-linux-amd64-v1 + mihomoBaseName = fmt.Sprintf("mihomo-%s-%s-%s", runtime.GOOS, runtime.GOARCH, features.GOAMD64) + default: + // mihomo-linux-386 + // mihomo-linux-mips64 + // mihomo-linux-riscv64 + // mihomo-linux-s390x + mihomoBaseName = fmt.Sprintf("mihomo-%s-%s", runtime.GOOS, runtime.GOARCH) } } -type updateError struct { - Message string -} +// CoreUpdater is the mihomo updater. +// modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/updater/updater.go +var CoreUpdater = coreUpdater{} -func (e *updateError) Error() string { - return fmt.Sprintf("update error: %s", e.Message) -} - -// Update performs the auto-updater. It returns an error if the updater failed. -// If firstRun is true, it assumes the configuration file doesn't exist. func UpdateCore(execPath string) (err error) { - mu.Lock() - defer mu.Unlock() + return CoreUpdater.Update(execPath) +} - latestVersion, err = getLatestVersion() +type coreUpdater struct { + mu sync.Mutex +} + +func (u *coreUpdater) Update(currentExePath string) (err error) { + u.mu.Lock() + defer u.mu.Unlock() + + _, err = os.Stat(currentExePath) if err != nil { - return err + return fmt.Errorf("check currentExePath %q: %w", currentExePath, err) } + baseURL := baseAlphaURL + versionURL := versionAlphaURL + if !strings.HasPrefix(C.Version, "alpha") { + baseURL = baseReleaseURL + versionURL = versionReleaseURL + } + + latestVersion, err := u.getLatestVersion(versionURL) + if err != nil { + return fmt.Errorf("get latest version: %w", err) + } log.Infoln("current version %s, latest version %s", C.Version, latestVersion) if latestVersion == C.Version { - err := &updateError{Message: "already using latest version"} - return err + return fmt.Errorf("update error: %s is the latest version", C.Version) } - updateDownloadURL() - defer func() { if err != nil { log.Errorln("updater: failed: %v", err) @@ -98,31 +109,48 @@ func UpdateCore(execPath string) (err error) { } }() - workDir = filepath.Dir(execPath) + // ---- prepare ---- + packageName := mihomoBaseName + "-" + latestVersion + if runtime.GOOS == "windows" { + packageName = packageName + ".zip" + } else { + packageName = packageName + ".gz" + } + packageURL := baseURL + packageName + log.Infoln("updater: updating using url: %s", packageURL) - err = prepare(execPath) + workDir := filepath.Dir(currentExePath) + backupDir := filepath.Join(workDir, "meta-backup") + updateDir := filepath.Join(workDir, "meta-update") + packagePath := filepath.Join(updateDir, packageName) + //log.Infoln(packagePath) + + updateExeName := mihomoBaseName + if runtime.GOOS == "windows" { + updateExeName = updateExeName + ".exe" + } + log.Infoln("updateExeName: %s ", updateExeName) + updateExePath := filepath.Join(updateDir, updateExeName) + backupExePath := filepath.Join(backupDir, filepath.Base(currentExePath)) + + defer u.clean(updateDir) + + err = u.download(updateDir, packagePath, packageURL) if err != nil { - return fmt.Errorf("preparing: %w", err) + return fmt.Errorf("downloading: %w", err) } - defer clean() - - err = downloadPackageFile() - if err != nil { - return fmt.Errorf("downloading package file: %w", err) - } - - err = unpack() + err = u.unpack(updateDir, packagePath) if err != nil { return fmt.Errorf("unpacking: %w", err) } - err = backup() + err = u.backup(currentExePath, backupExePath, backupDir) if err != nil { return fmt.Errorf("backuping: %w", err) } - err = replace() + err = u.replace(updateExePath, currentExePath) if err != nil { return fmt.Errorf("replacing: %w", err) } @@ -130,116 +158,30 @@ func UpdateCore(execPath string) (err error) { return nil } -// prepare fills all necessary fields in Updater object. -func prepare(exePath string) (err error) { - updateDir = filepath.Join(workDir, "meta-update") - currentExeName = exePath - _, pkgNameOnly := filepath.Split(packageURL) - if pkgNameOnly == "" { - return fmt.Errorf("invalid PackageURL: %q", packageURL) - } - - packageName = filepath.Join(updateDir, pkgNameOnly) - //log.Infoln(packageName) - backupDir = filepath.Join(workDir, "meta-backup") - - if runtime.GOOS == "windows" { - updateExeName = "mihomo" + "-" + runtime.GOOS + "-" + runtime.GOARCH + goamd64 + ".exe" - } else if runtime.GOOS == "android" && runtime.GOARCH == "arm64" { - updateExeName = "mihomo-android-arm64-v8" - } else { - updateExeName = "mihomo" + "-" + runtime.GOOS + "-" + runtime.GOARCH + goamd64 - } - - log.Infoln("updateExeName: %s ", updateExeName) - - backupExeName = filepath.Join(backupDir, filepath.Base(exePath)) - updateExeName = filepath.Join(updateDir, updateExeName) - - log.Infoln( - "updater: updating using url: %s", - packageURL, - ) - - currentExeName = exePath - _, err = os.Stat(currentExeName) +func (u *coreUpdater) getLatestVersion(versionURL string) (version string, err error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + resp, err := mihomoHttp.HttpRequest(ctx, versionURL, http.MethodGet, nil, nil) if err != nil { - return fmt.Errorf("checking %q: %w", currentExeName, err) + return "", err } - - return nil -} - -// unpack extracts the files from the downloaded archive. -func unpack() error { - var err error - _, pkgNameOnly := filepath.Split(packageURL) - - log.Infoln("updater: unpacking package") - if strings.HasSuffix(pkgNameOnly, ".zip") { - _, err = zipFileUnpack(packageName, updateDir) - if err != nil { - return fmt.Errorf(".zip unpack failed: %w", err) + defer func() { + closeErr := resp.Body.Close() + if closeErr != nil && err == nil { + err = closeErr } + }() - } else if strings.HasSuffix(pkgNameOnly, ".gz") { - _, err = gzFileUnpack(packageName, updateDir) - if err != nil { - return fmt.Errorf(".gz unpack failed: %w", err) - } - - } else { - return fmt.Errorf("unknown package extension") - } - - return nil -} - -// backup makes a backup of the current executable file -func backup() (err error) { - log.Infoln("updater: backing up current ExecFile:%s to %s", currentExeName, backupExeName) - _ = os.Mkdir(backupDir, 0o755) - - err = os.Rename(currentExeName, backupExeName) + body, err := io.ReadAll(resp.Body) if err != nil { - return err + return "", err } - - return nil + content := strings.TrimRight(string(body), "\n") + return content, nil } -// replace moves the current executable with the updated one -func replace() error { - var err error - - log.Infoln("replacing: %s to %s", updateExeName, currentExeName) - if runtime.GOOS == "windows" { - // rename fails with "File in use" error - err = copyFile(updateExeName, currentExeName) - } else { - err = os.Rename(updateExeName, currentExeName) - } - if err != nil { - return err - } - - log.Infoln("updater: renamed: %s to %s", updateExeName, currentExeName) - - return nil -} - -// clean removes the temporary directory itself and all it's contents. -func clean() { - _ = os.RemoveAll(updateDir) -} - -// MaxPackageFileSize is a maximum package file length in bytes. The largest -// package whose size is limited by this constant currently has the size of -// approximately 32 MiB. -const MaxPackageFileSize = 32 * 1024 * 1024 - -// Download package file and save it to disk -func downloadPackageFile() (err error) { +// download package file and save it to disk +func (u *coreUpdater) download(updateDir, packagePath, packageURL string) (err error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) defer cancel() resp, err := mihomoHttp.HttpRequest(ctx, packageURL, http.MethodGet, nil, nil) @@ -254,15 +196,9 @@ func downloadPackageFile() (err error) { } }() - var r io.Reader - r, err = LimitReader(resp.Body, MaxPackageFileSize) - if err != nil { - return fmt.Errorf("http request failed: %w", err) - } - log.Debugln("updater: reading http body") // This use of ReadAll is now safe, because we limited body's Reader. - body, err := io.ReadAll(r) + body, err := io.ReadAll(io.LimitReader(resp.Body, MaxPackageFileSize)) if err != nil { return fmt.Errorf("io.ReadAll() failed: %w", err) } @@ -273,19 +209,79 @@ func downloadPackageFile() (err error) { return fmt.Errorf("mkdir error: %w", err) } - log.Debugln("updater: saving package to file %s", packageName) - err = os.WriteFile(packageName, body, 0o644) + log.Debugln("updater: saving package to file %s", packagePath) + err = os.WriteFile(packagePath, body, 0o644) if err != nil { return fmt.Errorf("os.WriteFile() failed: %w", err) } return nil } +// unpack extracts the files from the downloaded archive. +func (u *coreUpdater) unpack(updateDir, packagePath string) error { + log.Infoln("updater: unpacking package") + if strings.HasSuffix(packagePath, ".zip") { + _, err := u.zipFileUnpack(packagePath, updateDir) + if err != nil { + return fmt.Errorf(".zip unpack failed: %w", err) + } + + } else if strings.HasSuffix(packagePath, ".gz") { + _, err := u.gzFileUnpack(packagePath, updateDir) + if err != nil { + return fmt.Errorf(".gz unpack failed: %w", err) + } + + } else { + return fmt.Errorf("unknown package extension") + } + + return nil +} + +// backup makes a backup of the current executable file +func (u *coreUpdater) backup(currentExePath, backupExePath, backupDir string) (err error) { + log.Infoln("updater: backing up current ExecFile:%s to %s", currentExePath, backupExePath) + _ = os.Mkdir(backupDir, 0o755) + + err = os.Rename(currentExePath, backupExePath) + if err != nil { + return err + } + + return nil +} + +// replace moves the current executable with the updated one +func (u *coreUpdater) replace(updateExePath, currentExePath string) error { + var err error + + log.Infoln("replacing: %s to %s", updateExePath, currentExePath) + if runtime.GOOS == "windows" { + // rename fails with "File in use" error + err = u.copyFile(updateExePath, currentExePath) + } else { + err = os.Rename(updateExePath, currentExePath) + } + if err != nil { + return err + } + + log.Infoln("updater: renamed: %s to %s", updateExePath, currentExePath) + + return nil +} + +// clean removes the temporary directory itself and all it's contents. +func (u *coreUpdater) clean(updateDir string) { + _ = os.RemoveAll(updateDir) +} + // Unpack a single .gz file to the specified directory // Existing files are overwritten // All files are created inside outDir, subdirectories are not created // Return the output file name -func gzFileUnpack(gzfile, outDir string) (string, error) { +func (u *coreUpdater) gzFileUnpack(gzfile, outDir string) (string, error) { f, err := os.Open(gzfile) if err != nil { return "", fmt.Errorf("os.Open(): %w", err) @@ -349,7 +345,7 @@ func gzFileUnpack(gzfile, outDir string) (string, error) { // Existing files are overwritten // All files are created inside 'outDir', subdirectories are not created // Return the output file name -func zipFileUnpack(zipfile, outDir string) (string, error) { +func (u *coreUpdater) zipFileUnpack(zipfile, outDir string) (string, error) { zrc, err := zip.OpenReader(zipfile) if err != nil { return "", fmt.Errorf("zip.OpenReader(): %w", err) @@ -408,7 +404,7 @@ func zipFileUnpack(zipfile, outDir string) (string, error) { } // Copy file on disk -func copyFile(src, dst string) error { +func (u *coreUpdater) copyFile(src, dst string) error { d, e := os.ReadFile(src) if e != nil { return e @@ -419,86 +415,3 @@ func copyFile(src, dst string) error { } return nil } - -func getLatestVersion() (version string, err error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - resp, err := mihomoHttp.HttpRequest(ctx, versionURL, http.MethodGet, nil, nil) - if err != nil { - return "", fmt.Errorf("get Latest Version fail: %w", err) - } - defer func() { - closeErr := resp.Body.Close() - if closeErr != nil && err == nil { - err = closeErr - } - }() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("get Latest Version fail: %w", err) - } - content := strings.TrimRight(string(body), "\n") - return content, nil -} - -func updateDownloadURL() { - var middle string - - if runtime.GOARCH == "arm" && probeGoARM() { - //-linux-armv7-alpha-e552b54.gz - middle = fmt.Sprintf("-%s-%s%s-%s", runtime.GOOS, runtime.GOARCH, goarm, latestVersion) - } else if runtime.GOARCH == "arm64" { - //-linux-arm64-alpha-e552b54.gz - if runtime.GOOS == "android" { - middle = fmt.Sprintf("-%s-%s-v8-%s", runtime.GOOS, runtime.GOARCH, latestVersion) - } else { - middle = fmt.Sprintf("-%s-%s-%s", runtime.GOOS, runtime.GOARCH, latestVersion) - } - } else if isMIPS(runtime.GOARCH) && gomips != "" { - middle = fmt.Sprintf("-%s-%s-%s-%s", runtime.GOOS, runtime.GOARCH, gomips, latestVersion) - } else { - middle = fmt.Sprintf("-%s-%s%s-%s", runtime.GOOS, runtime.GOARCH, goamd64, latestVersion) - } - - if runtime.GOOS == "windows" { - middle += ".zip" - } else { - middle += ".gz" - } - packageURL = baseURL + middle - //log.Infoln(packageURL) -} - -// isMIPS returns true if arch is any MIPS architecture. -func isMIPS(arch string) (ok bool) { - switch arch { - case - "mips", - "mips64", - "mips64le", - "mipsle": - return true - default: - return false - } -} - -// linux only -func probeGoARM() (ok bool) { - cmd := exec.Command("cat", "/proc/cpuinfo") - output, err := cmd.Output() - if err != nil { - log.Errorln("probe goarm error:%s", err) - return false - } - cpuInfo := string(output) - if strings.Contains(cpuInfo, "vfpv3") || strings.Contains(cpuInfo, "vfpv4") { - goarm = "v7" - } else if strings.Contains(cpuInfo, "vfp") { - goarm = "v6" - } else { - goarm = "v5" - } - return true -} diff --git a/component/updater/update_core_test.go b/component/updater/update_core_test.go new file mode 100644 index 00000000..808fc24f --- /dev/null +++ b/component/updater/update_core_test.go @@ -0,0 +1,10 @@ +package updater + +import ( + "fmt" + "testing" +) + +func TestBaseName(t *testing.T) { + fmt.Println("mihomoBaseName =", mihomoBaseName) +} diff --git a/component/updater/utils.go b/component/updater/utils.go index b5c694ff..a77c86fe 100644 --- a/component/updater/utils.go +++ b/component/updater/utils.go @@ -2,15 +2,12 @@ package updater import ( "context" - "fmt" "io" "net/http" "os" "time" mihomoHttp "github.com/metacubex/mihomo/component/http" - - "golang.org/x/exp/constraints" ) const defaultHttpTimeout = time.Second * 90 @@ -30,62 +27,3 @@ func downloadForBytes(url string) ([]byte, error) { func saveFile(bytes []byte, path string) error { return os.WriteFile(path, bytes, 0o644) } - -// LimitReachedError records the limit and the operation that caused it. -type LimitReachedError struct { - Limit int64 -} - -// Error implements the [error] interface for *LimitReachedError. -// -// TODO(a.garipov): Think about error string format. -func (lre *LimitReachedError) Error() string { - return fmt.Sprintf("attempted to read more than %d bytes", lre.Limit) -} - -// limitedReader is a wrapper for [io.Reader] limiting the input and dealing -// with errors package. -type limitedReader struct { - r io.Reader - limit int64 - n int64 -} - -// Read implements the [io.Reader] interface. -func (lr *limitedReader) Read(p []byte) (n int, err error) { - if lr.n == 0 { - return 0, &LimitReachedError{ - Limit: lr.limit, - } - } - - p = p[:Min(lr.n, int64(len(p)))] - - n, err = lr.r.Read(p) - lr.n -= int64(n) - - return n, err -} - -// LimitReader wraps Reader to make it's Reader stop with ErrLimitReached after -// n bytes read. -func LimitReader(r io.Reader, n int64) (limited io.Reader, err error) { - if n < 0 { - return nil, &updateError{Message: "limit must be non-negative"} - } - - return &limitedReader{ - r: r, - limit: n, - n: n, - }, nil -} - -// Min returns the smaller of x or y. -func Min[T constraints.Integer | ~string](x, y T) (res T) { - if x < y { - return x - } - - return y -} diff --git a/constant/features/goflags.go b/constant/features/goflags.go new file mode 100644 index 00000000..78c429ab --- /dev/null +++ b/constant/features/goflags.go @@ -0,0 +1,24 @@ +package features + +import "runtime/debug" + +var ( + GOARM string + GOMIPS string + GOAMD64 string +) + +func init() { + if info, ok := debug.ReadBuildInfo(); ok { + for _, bs := range info.Settings { + switch bs.Key { + case "GOARM": + GOARM = bs.Value + case "GOMIPS": + GOMIPS = bs.Value + case "GOAMD64": + GOAMD64 = bs.Value + } + } + } +}