github.com/metacubex/mihomo@v1.18.5/component/updater/update_core.go (about)

     1  package updater
     2  
     3  import (
     4  	"archive/zip"
     5  	"compress/gzip"
     6  	"context"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"runtime"
    14  	"strings"
    15  	"sync"
    16  	"time"
    17  
    18  	mihomoHttp "github.com/metacubex/mihomo/component/http"
    19  	C "github.com/metacubex/mihomo/constant"
    20  	"github.com/metacubex/mihomo/log"
    21  
    22  	"github.com/klauspost/cpuid/v2"
    23  )
    24  
    25  // modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/updater/updater.go
    26  // Updater is the mihomo updater.
    27  var (
    28  	goarm           string
    29  	gomips          string
    30  	amd64Compatible string
    31  
    32  	workDir string
    33  
    34  	// mu protects all fields below.
    35  	mu sync.Mutex
    36  
    37  	currentExeName string // 当前可执行文件
    38  	updateDir      string // 更新目录
    39  	packageName    string // 更新压缩文件
    40  	backupDir      string // 备份目录
    41  	backupExeName  string // 备份文件名
    42  	updateExeName  string // 更新后的可执行文件
    43  
    44  	baseURL       string = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/mihomo"
    45  	versionURL    string = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt"
    46  	packageURL    string
    47  	latestVersion string
    48  )
    49  
    50  func init() {
    51  	if runtime.GOARCH == "amd64" && cpuid.CPU.X64Level() < 3 {
    52  		amd64Compatible = "-compatible"
    53  	}
    54  	if !strings.HasPrefix(C.Version, "alpha") {
    55  		baseURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/mihomo"
    56  		versionURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt"
    57  	}
    58  }
    59  
    60  type updateError struct {
    61  	Message string
    62  }
    63  
    64  func (e *updateError) Error() string {
    65  	return fmt.Sprintf("update error: %s", e.Message)
    66  }
    67  
    68  // Update performs the auto-updater.  It returns an error if the updater failed.
    69  // If firstRun is true, it assumes the configuration file doesn't exist.
    70  func UpdateCore(execPath string) (err error) {
    71  	mu.Lock()
    72  	defer mu.Unlock()
    73  
    74  	latestVersion, err = getLatestVersion()
    75  	if err != nil {
    76  		return err
    77  	}
    78  
    79  	log.Infoln("current version %s, latest version %s", C.Version, latestVersion)
    80  
    81  	if latestVersion == C.Version {
    82  		err := &updateError{Message: "already using latest version"}
    83  		return err
    84  	}
    85  
    86  	updateDownloadURL()
    87  
    88  	defer func() {
    89  		if err != nil {
    90  			log.Errorln("updater: failed: %v", err)
    91  		} else {
    92  			log.Infoln("updater: finished")
    93  		}
    94  	}()
    95  
    96  	workDir = filepath.Dir(execPath)
    97  
    98  	err = prepare(execPath)
    99  	if err != nil {
   100  		return fmt.Errorf("preparing: %w", err)
   101  	}
   102  
   103  	defer clean()
   104  
   105  	err = downloadPackageFile()
   106  	if err != nil {
   107  		return fmt.Errorf("downloading package file: %w", err)
   108  	}
   109  
   110  	err = unpack()
   111  	if err != nil {
   112  		return fmt.Errorf("unpacking: %w", err)
   113  	}
   114  
   115  	err = backup()
   116  	if err != nil {
   117  		return fmt.Errorf("backuping: %w", err)
   118  	}
   119  
   120  	err = replace()
   121  	if err != nil {
   122  		return fmt.Errorf("replacing: %w", err)
   123  	}
   124  
   125  	return nil
   126  }
   127  
   128  // prepare fills all necessary fields in Updater object.
   129  func prepare(exePath string) (err error) {
   130  	updateDir = filepath.Join(workDir, "meta-update")
   131  	currentExeName = exePath
   132  	_, pkgNameOnly := filepath.Split(packageURL)
   133  	if pkgNameOnly == "" {
   134  		return fmt.Errorf("invalid PackageURL: %q", packageURL)
   135  	}
   136  
   137  	packageName = filepath.Join(updateDir, pkgNameOnly)
   138  	//log.Infoln(packageName)
   139  	backupDir = filepath.Join(workDir, "meta-backup")
   140  
   141  	if runtime.GOOS == "windows" {
   142  		updateExeName = "mihomo" + "-" + runtime.GOOS + "-" + runtime.GOARCH + amd64Compatible + ".exe"
   143  	} else if runtime.GOOS == "android" && runtime.GOARCH == "arm64" {
   144  		updateExeName = "mihomo-android-arm64-v8"
   145  	} else {
   146  		updateExeName = "mihomo" + "-" + runtime.GOOS + "-" + runtime.GOARCH + amd64Compatible
   147  	}
   148  
   149  	log.Infoln("updateExeName: %s ", updateExeName)
   150  
   151  	backupExeName = filepath.Join(backupDir, filepath.Base(exePath))
   152  	updateExeName = filepath.Join(updateDir, updateExeName)
   153  
   154  	log.Infoln(
   155  		"updater: updating using url: %s",
   156  		packageURL,
   157  	)
   158  
   159  	currentExeName = exePath
   160  	_, err = os.Stat(currentExeName)
   161  	if err != nil {
   162  		return fmt.Errorf("checking %q: %w", currentExeName, err)
   163  	}
   164  
   165  	return nil
   166  }
   167  
   168  // unpack extracts the files from the downloaded archive.
   169  func unpack() error {
   170  	var err error
   171  	_, pkgNameOnly := filepath.Split(packageURL)
   172  
   173  	log.Infoln("updater: unpacking package")
   174  	if strings.HasSuffix(pkgNameOnly, ".zip") {
   175  		_, err = zipFileUnpack(packageName, updateDir)
   176  		if err != nil {
   177  			return fmt.Errorf(".zip unpack failed: %w", err)
   178  		}
   179  
   180  	} else if strings.HasSuffix(pkgNameOnly, ".gz") {
   181  		_, err = gzFileUnpack(packageName, updateDir)
   182  		if err != nil {
   183  			return fmt.Errorf(".gz unpack failed: %w", err)
   184  		}
   185  
   186  	} else {
   187  		return fmt.Errorf("unknown package extension")
   188  	}
   189  
   190  	return nil
   191  }
   192  
   193  // backup makes a backup of the current executable file
   194  func backup() (err error) {
   195  	log.Infoln("updater: backing up current ExecFile:%s to %s", currentExeName, backupExeName)
   196  	_ = os.Mkdir(backupDir, 0o755)
   197  
   198  	err = os.Rename(currentExeName, backupExeName)
   199  	if err != nil {
   200  		return err
   201  	}
   202  
   203  	return nil
   204  }
   205  
   206  // replace moves the current executable with the updated one
   207  func replace() error {
   208  	var err error
   209  
   210  	log.Infoln("replacing: %s to %s", updateExeName, currentExeName)
   211  	if runtime.GOOS == "windows" {
   212  		// rename fails with "File in use" error
   213  		err = copyFile(updateExeName, currentExeName)
   214  	} else {
   215  		err = os.Rename(updateExeName, currentExeName)
   216  	}
   217  	if err != nil {
   218  		return err
   219  	}
   220  
   221  	log.Infoln("updater: renamed: %s to %s", updateExeName, currentExeName)
   222  
   223  	return nil
   224  }
   225  
   226  // clean removes the temporary directory itself and all it's contents.
   227  func clean() {
   228  	_ = os.RemoveAll(updateDir)
   229  }
   230  
   231  // MaxPackageFileSize is a maximum package file length in bytes. The largest
   232  // package whose size is limited by this constant currently has the size of
   233  // approximately 9 MiB.
   234  const MaxPackageFileSize = 32 * 1024 * 1024
   235  
   236  // Download package file and save it to disk
   237  func downloadPackageFile() (err error) {
   238  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
   239  	defer cancel()
   240  	resp, err := mihomoHttp.HttpRequest(ctx, packageURL, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil)
   241  	if err != nil {
   242  		return fmt.Errorf("http request failed: %w", err)
   243  	}
   244  
   245  	defer func() {
   246  		closeErr := resp.Body.Close()
   247  		if closeErr != nil && err == nil {
   248  			err = closeErr
   249  		}
   250  	}()
   251  
   252  	var r io.Reader
   253  	r, err = LimitReader(resp.Body, MaxPackageFileSize)
   254  	if err != nil {
   255  		return fmt.Errorf("http request failed: %w", err)
   256  	}
   257  
   258  	log.Debugln("updater: reading http body")
   259  	// This use of ReadAll is now safe, because we limited body's Reader.
   260  	body, err := io.ReadAll(r)
   261  	if err != nil {
   262  		return fmt.Errorf("io.ReadAll() failed: %w", err)
   263  	}
   264  
   265  	log.Debugln("updateDir %s", updateDir)
   266  	err = os.Mkdir(updateDir, 0o755)
   267  	if err != nil {
   268  		return fmt.Errorf("mkdir error: %w", err)
   269  	}
   270  
   271  	log.Debugln("updater: saving package to file %s", packageName)
   272  	err = os.WriteFile(packageName, body, 0o644)
   273  	if err != nil {
   274  		return fmt.Errorf("os.WriteFile() failed: %w", err)
   275  	}
   276  	return nil
   277  }
   278  
   279  // Unpack a single .gz file to the specified directory
   280  // Existing files are overwritten
   281  // All files are created inside outDir, subdirectories are not created
   282  // Return the output file name
   283  func gzFileUnpack(gzfile, outDir string) (string, error) {
   284  	f, err := os.Open(gzfile)
   285  	if err != nil {
   286  		return "", fmt.Errorf("os.Open(): %w", err)
   287  	}
   288  
   289  	defer func() {
   290  		closeErr := f.Close()
   291  		if closeErr != nil && err == nil {
   292  			err = closeErr
   293  		}
   294  	}()
   295  
   296  	gzReader, err := gzip.NewReader(f)
   297  	if err != nil {
   298  		return "", fmt.Errorf("gzip.NewReader(): %w", err)
   299  	}
   300  
   301  	defer func() {
   302  		closeErr := gzReader.Close()
   303  		if closeErr != nil && err == nil {
   304  			err = closeErr
   305  		}
   306  	}()
   307  	// Get the original file name from the .gz file header
   308  	originalName := gzReader.Header.Name
   309  	if originalName == "" {
   310  		// Fallback: remove the .gz extension from the input file name if the header doesn't provide the original name
   311  		originalName = filepath.Base(gzfile)
   312  		originalName = strings.TrimSuffix(originalName, ".gz")
   313  	}
   314  
   315  	outputName := filepath.Join(outDir, originalName)
   316  
   317  	// Create the output file
   318  	wc, err := os.OpenFile(
   319  		outputName,
   320  		os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
   321  		0o755,
   322  	)
   323  	if err != nil {
   324  		return "", fmt.Errorf("os.OpenFile(%s): %w", outputName, err)
   325  	}
   326  
   327  	defer func() {
   328  		closeErr := wc.Close()
   329  		if closeErr != nil && err == nil {
   330  			err = closeErr
   331  		}
   332  	}()
   333  
   334  	// Copy the contents of the gzReader to the output file
   335  	_, err = io.Copy(wc, gzReader)
   336  	if err != nil {
   337  		return "", fmt.Errorf("io.Copy(): %w", err)
   338  	}
   339  
   340  	return outputName, nil
   341  }
   342  
   343  // Unpack a single file from .zip file to the specified directory
   344  // Existing files are overwritten
   345  // All files are created inside 'outDir', subdirectories are not created
   346  // Return the output file name
   347  func zipFileUnpack(zipfile, outDir string) (string, error) {
   348  	zrc, err := zip.OpenReader(zipfile)
   349  	if err != nil {
   350  		return "", fmt.Errorf("zip.OpenReader(): %w", err)
   351  	}
   352  
   353  	defer func() {
   354  		closeErr := zrc.Close()
   355  		if closeErr != nil && err == nil {
   356  			err = closeErr
   357  		}
   358  	}()
   359  	if len(zrc.File) == 0 {
   360  		return "", fmt.Errorf("no files in the zip archive")
   361  	}
   362  
   363  	// Assuming the first file in the zip archive is the target file
   364  	zf := zrc.File[0]
   365  	var rc io.ReadCloser
   366  	rc, err = zf.Open()
   367  	if err != nil {
   368  		return "", fmt.Errorf("zip file Open(): %w", err)
   369  	}
   370  
   371  	defer func() {
   372  		closeErr := rc.Close()
   373  		if closeErr != nil && err == nil {
   374  			err = closeErr
   375  		}
   376  	}()
   377  	fi := zf.FileInfo()
   378  	name := fi.Name()
   379  	outputName := filepath.Join(outDir, name)
   380  
   381  	if fi.IsDir() {
   382  		return "", fmt.Errorf("the target file is a directory")
   383  	}
   384  
   385  	var wc io.WriteCloser
   386  	wc, err = os.OpenFile(outputName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode())
   387  	if err != nil {
   388  		return "", fmt.Errorf("os.OpenFile(): %w", err)
   389  	}
   390  
   391  	defer func() {
   392  		closeErr := wc.Close()
   393  		if closeErr != nil && err == nil {
   394  			err = closeErr
   395  		}
   396  	}()
   397  	_, err = io.Copy(wc, rc)
   398  	if err != nil {
   399  		return "", fmt.Errorf("io.Copy(): %w", err)
   400  	}
   401  
   402  	return outputName, nil
   403  }
   404  
   405  // Copy file on disk
   406  func copyFile(src, dst string) error {
   407  	d, e := os.ReadFile(src)
   408  	if e != nil {
   409  		return e
   410  	}
   411  	e = os.WriteFile(dst, d, 0o644)
   412  	if e != nil {
   413  		return e
   414  	}
   415  	return nil
   416  }
   417  
   418  func getLatestVersion() (version string, err error) {
   419  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   420  	defer cancel()
   421  	resp, err := mihomoHttp.HttpRequest(ctx, versionURL, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil)
   422  	if err != nil {
   423  		return "", fmt.Errorf("get Latest Version fail: %w", err)
   424  	}
   425  	defer func() {
   426  		closeErr := resp.Body.Close()
   427  		if closeErr != nil && err == nil {
   428  			err = closeErr
   429  		}
   430  	}()
   431  
   432  	body, err := io.ReadAll(resp.Body)
   433  	if err != nil {
   434  		return "", fmt.Errorf("get Latest Version fail: %w", err)
   435  	}
   436  	content := strings.TrimRight(string(body), "\n")
   437  	return content, nil
   438  }
   439  
   440  func updateDownloadURL() {
   441  	var middle string
   442  
   443  	if runtime.GOARCH == "arm" && probeGoARM() {
   444  		//-linux-armv7-alpha-e552b54.gz
   445  		middle = fmt.Sprintf("-%s-%s%s-%s", runtime.GOOS, runtime.GOARCH, goarm, latestVersion)
   446  	} else if runtime.GOARCH == "arm64" {
   447  		//-linux-arm64-alpha-e552b54.gz
   448  		if runtime.GOOS == "android" {
   449  			middle = fmt.Sprintf("-%s-%s-v8-%s", runtime.GOOS, runtime.GOARCH, latestVersion)
   450  		} else {
   451  			middle = fmt.Sprintf("-%s-%s-%s", runtime.GOOS, runtime.GOARCH, latestVersion)
   452  		}
   453  	} else if isMIPS(runtime.GOARCH) && gomips != "" {
   454  		middle = fmt.Sprintf("-%s-%s-%s-%s", runtime.GOOS, runtime.GOARCH, gomips, latestVersion)
   455  	} else {
   456  		middle = fmt.Sprintf("-%s-%s%s-%s", runtime.GOOS, runtime.GOARCH, amd64Compatible, latestVersion)
   457  	}
   458  
   459  	if runtime.GOOS == "windows" {
   460  		middle += ".zip"
   461  	} else {
   462  		middle += ".gz"
   463  	}
   464  	packageURL = baseURL + middle
   465  	//log.Infoln(packageURL)
   466  }
   467  
   468  // isMIPS returns true if arch is any MIPS architecture.
   469  func isMIPS(arch string) (ok bool) {
   470  	switch arch {
   471  	case
   472  		"mips",
   473  		"mips64",
   474  		"mips64le",
   475  		"mipsle":
   476  		return true
   477  	default:
   478  		return false
   479  	}
   480  }
   481  
   482  // linux only
   483  func probeGoARM() (ok bool) {
   484  	cmd := exec.Command("cat", "/proc/cpuinfo")
   485  	output, err := cmd.Output()
   486  	if err != nil {
   487  		log.Errorln("probe goarm error:%s", err)
   488  		return false
   489  	}
   490  	cpuInfo := string(output)
   491  	if strings.Contains(cpuInfo, "vfpv3") || strings.Contains(cpuInfo, "vfpv4") {
   492  		goarm = "v7"
   493  	} else if strings.Contains(cpuInfo, "vfp") {
   494  		goarm = "v6"
   495  	} else {
   496  		goarm = "v5"
   497  	}
   498  	return true
   499  }