1
0
mirror of https://github.com/MetaCubeX/mihomo.git synced 2025-09-20 04:25:59 +08:00

chore: rebuild core updater

This commit is contained in:
wwqgtxx
2025-07-24 02:06:50 +08:00
parent b6dde7ded7
commit dfe6e0509b
8 changed files with 208 additions and 377 deletions

View File

@ -1,4 +0,0 @@
package updater
// getGOAMD64level is implemented in cpu_amd64.s. Returns number in [1,4].
func getGOAMD64level() int32

View File

@ -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

View File

@ -1,8 +0,0 @@
//go:build !amd64
package updater
// getGOAMD64level is always return 0 when not in amd64 platfrom.
func getGOAMD64level() int32 {
return 0
}

View File

@ -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))
}
}

View File

@ -8,7 +8,6 @@ import (
"io" "io"
"net/http" "net/http"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
@ -17,79 +16,91 @@ import (
mihomoHttp "github.com/metacubex/mihomo/component/http" mihomoHttp "github.com/metacubex/mihomo/component/http"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/features"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
) )
// modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/updater/updater.go const (
// Updater is the mihomo updater. baseReleaseURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/"
var ( versionReleaseURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt"
goarm string
gomips string
goamd64 string
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. // MaxPackageFileSize is a maximum package file length in bytes. The largest
mu sync.Mutex // package whose size is limited by this constant currently has the size of
// approximately 32 MiB.
currentExeName string // 当前可执行文件 MaxPackageFileSize = 32 * 1024 * 1024
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
) )
var mihomoBaseName string
func init() { func init() {
if runtime.GOARCH == "amd64" { switch runtime.GOARCH {
switch getGOAMD64level() { case "arm":
case 1: // mihomo-linux-armv5
goamd64 = "-v1" mihomoBaseName = fmt.Sprintf("mihomo-%s-%sv%s", runtime.GOOS, runtime.GOARCH, features.GOARM)
case 2: case "arm64":
goamd64 = "-v2" if runtime.GOOS == "android" {
case 3: // mihomo-android-arm64-v8
goamd64 = "-v3" 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)
} }
} case "mips", "mipsle":
if !strings.HasPrefix(C.Version, "alpha") { // mihomo-linux-mips-hardfloat
baseURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/mihomo" mihomoBaseName = fmt.Sprintf("mihomo-%s-%s-%s", runtime.GOOS, runtime.GOARCH, features.GOMIPS)
versionURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt" 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 { // CoreUpdater is the mihomo updater.
Message string // 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) { func UpdateCore(execPath string) (err error) {
mu.Lock() return CoreUpdater.Update(execPath)
defer mu.Unlock()
latestVersion, err = getLatestVersion()
if err != nil {
return err
} }
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 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) log.Infoln("current version %s, latest version %s", C.Version, latestVersion)
if latestVersion == C.Version { if latestVersion == C.Version {
err := &updateError{Message: "already using latest version"} return fmt.Errorf("update error: %s is the latest version", C.Version)
return err
} }
updateDownloadURL()
defer func() { defer func() {
if err != nil { if err != nil {
log.Errorln("updater: failed: %v", err) 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 { if err != nil {
return fmt.Errorf("preparing: %w", err) return fmt.Errorf("downloading: %w", err)
} }
defer clean() err = u.unpack(updateDir, packagePath)
err = downloadPackageFile()
if err != nil {
return fmt.Errorf("downloading package file: %w", err)
}
err = unpack()
if err != nil { if err != nil {
return fmt.Errorf("unpacking: %w", err) return fmt.Errorf("unpacking: %w", err)
} }
err = backup() err = u.backup(currentExePath, backupExePath, backupDir)
if err != nil { if err != nil {
return fmt.Errorf("backuping: %w", err) return fmt.Errorf("backuping: %w", err)
} }
err = replace() err = u.replace(updateExePath, currentExePath)
if err != nil { if err != nil {
return fmt.Errorf("replacing: %w", err) return fmt.Errorf("replacing: %w", err)
} }
@ -130,116 +158,30 @@ func UpdateCore(execPath string) (err error) {
return nil return nil
} }
// prepare fills all necessary fields in Updater object. func (u *coreUpdater) getLatestVersion(versionURL string) (version string, err error) {
func prepare(exePath string) (err error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
updateDir = filepath.Join(workDir, "meta-update") defer cancel()
currentExeName = exePath resp, err := mihomoHttp.HttpRequest(ctx, versionURL, http.MethodGet, nil, nil)
_, 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)
if err != nil { if err != nil {
return fmt.Errorf("checking %q: %w", currentExeName, err) return "", err
} }
defer func() {
return nil closeErr := resp.Body.Close()
if closeErr != nil && err == nil {
err = closeErr
} }
}()
// unpack extracts the files from the downloaded archive. body, err := io.ReadAll(resp.Body)
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 { if err != nil {
return fmt.Errorf(".zip unpack failed: %w", err) return "", err
}
content := strings.TrimRight(string(body), "\n")
return content, nil
} }
} else if strings.HasSuffix(pkgNameOnly, ".gz") { // download package file and save it to disk
_, err = gzFileUnpack(packageName, updateDir) func (u *coreUpdater) download(updateDir, packagePath, packageURL string) (err error) {
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)
if err != nil {
return err
}
return 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) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
defer cancel() defer cancel()
resp, err := mihomoHttp.HttpRequest(ctx, packageURL, http.MethodGet, nil, nil) 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") log.Debugln("updater: reading http body")
// This use of ReadAll is now safe, because we limited body's Reader. // 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 { if err != nil {
return fmt.Errorf("io.ReadAll() failed: %w", err) return fmt.Errorf("io.ReadAll() failed: %w", err)
} }
@ -273,19 +209,79 @@ func downloadPackageFile() (err error) {
return fmt.Errorf("mkdir error: %w", err) return fmt.Errorf("mkdir error: %w", err)
} }
log.Debugln("updater: saving package to file %s", packageName) log.Debugln("updater: saving package to file %s", packagePath)
err = os.WriteFile(packageName, body, 0o644) err = os.WriteFile(packagePath, body, 0o644)
if err != nil { if err != nil {
return fmt.Errorf("os.WriteFile() failed: %w", err) return fmt.Errorf("os.WriteFile() failed: %w", err)
} }
return nil 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 // Unpack a single .gz file to the specified directory
// Existing files are overwritten // Existing files are overwritten
// All files are created inside outDir, subdirectories are not created // All files are created inside outDir, subdirectories are not created
// Return the output file name // 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) f, err := os.Open(gzfile)
if err != nil { if err != nil {
return "", fmt.Errorf("os.Open(): %w", err) return "", fmt.Errorf("os.Open(): %w", err)
@ -349,7 +345,7 @@ func gzFileUnpack(gzfile, outDir string) (string, error) {
// Existing files are overwritten // Existing files are overwritten
// All files are created inside 'outDir', subdirectories are not created // All files are created inside 'outDir', subdirectories are not created
// Return the output file name // 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) zrc, err := zip.OpenReader(zipfile)
if err != nil { if err != nil {
return "", fmt.Errorf("zip.OpenReader(): %w", err) return "", fmt.Errorf("zip.OpenReader(): %w", err)
@ -408,7 +404,7 @@ func zipFileUnpack(zipfile, outDir string) (string, error) {
} }
// Copy file on disk // Copy file on disk
func copyFile(src, dst string) error { func (u *coreUpdater) copyFile(src, dst string) error {
d, e := os.ReadFile(src) d, e := os.ReadFile(src)
if e != nil { if e != nil {
return e return e
@ -419,86 +415,3 @@ func copyFile(src, dst string) error {
} }
return nil 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
}

View File

@ -0,0 +1,10 @@
package updater
import (
"fmt"
"testing"
)
func TestBaseName(t *testing.T) {
fmt.Println("mihomoBaseName =", mihomoBaseName)
}

View File

@ -2,15 +2,12 @@ package updater
import ( import (
"context" "context"
"fmt"
"io" "io"
"net/http" "net/http"
"os" "os"
"time" "time"
mihomoHttp "github.com/metacubex/mihomo/component/http" mihomoHttp "github.com/metacubex/mihomo/component/http"
"golang.org/x/exp/constraints"
) )
const defaultHttpTimeout = time.Second * 90 const defaultHttpTimeout = time.Second * 90
@ -30,62 +27,3 @@ func downloadForBytes(url string) ([]byte, error) {
func saveFile(bytes []byte, path string) error { func saveFile(bytes []byte, path string) error {
return os.WriteFile(path, bytes, 0o644) 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
}

View File

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