在开发ETProxy测试代理工具的过程中,为了功能的快速迭代和升级成本的降低,需要实现工具的自动升级。

ETProxy的自动升级与一般程序的差异点为:ETProxy是一个单文件,免安装程序。相比一般的程序,不需要处理多文件增量升级的情况;但也正因为是单文件,所以升级功能得集成在主程序中,而不是用单独的下载更新器实现。

自动升级逻辑如下图所示:

正在加载嵌入笔记…

关键代码如下:

go
up := updater.NewUpdate(conf.Update.URL, "etproxy.version.txt", "etproxy.exe", common.Version)
err := up.Do(func() {
})
if err != nil {
	logger.Errorf("%+v", err)
}
go
type Updater struct {
	UpdateURL          string
	VersionFileName    string
	ExeFileName        string
	CompareVersionFunc func(v1, v2 string) int
	CurrentVersion     string
}

func NewUpdate(updateURL, versionFileName, exeFileName, currentVersion string) *Updater {
	return &Updater{
		UpdateURL:          updateURL,
		VersionFileName:    versionFileName,
		ExeFileName:        exeFileName,
		CurrentVersion:     currentVersion,
		CompareVersionFunc: compareVersion,
	}
}

func (up *Updater) Do(afterFunc func()) error {
	currentExePath, err := os.Executable()
	if err != nil {
		return err
	}
	logger.Infof("当前执行文件路径为:%s", currentExePath)
	currentExeName := filepath.Base(currentExePath)
	if strings.HasPrefix(currentExeName, "autoupdate_") {
		// 覆盖原程序并拉起新程序
		desPath := filepath.Join(filepath.Dir(currentExePath), "..", up.ExeFileName)
		logger.Infof("开始将新版本程序复制到目标路径:%s", desPath)
		err := copy(currentExePath, desPath)
		if err != nil {
			return err
		}
		logger.Info("复制完成,开始启动新版本程序")
		afterFunc()
		err = openExe(desPath, false)
		if err != nil {
			return err
		}
		logger.Info("新版本程序启动成功,当前程序自动退出")
		os.Exit(0)
	} else {
		versionURL := strings.TrimSuffix(up.UpdateURL, "/") + "/" + up.VersionFileName
		logger.Infof("开始从服务器获取最新版本信息:%s", versionURL)
		latest, err := getVersion(versionURL)
		if err != nil {
			return err
		}
		logger.Infof("服务器最新版本为:%s", latest)
		if up.CompareVersionFunc(up.CurrentVersion, latest) >= 0 {
			return nil
		}
		exeURL := strings.TrimSuffix(up.UpdateURL, "/") + "/" + up.ExeFileName
		exeTempPath := filepath.Join(filepath.Dir(currentExePath), "tmp", "autoupdate_"+up.ExeFileName+"_"+latest+".exe")
		logger.Infof("开始执行更新包下载,下载地址:%s,目标路径:%s", exeURL, exeTempPath)
		err = download(exeURL, exeTempPath)
		if err != nil {
			return err
		}
		logger.Info("下载完成,开始启动临时新版本程序")
		err = openExe(exeTempPath, true)
		if err != nil {
			return err
		}
		logger.Info("临时新版本程序启动成功,当前程序自动退出")
		os.Exit(0)
	}
	return nil
}

func copy(src, des string) error {
	s, err := os.Open(src)
	if err != nil {
		return err
	}
	defer s.Close()
	d, err := os.Create(des)
	if err != nil {
		return err
	}
	defer d.Close()
	_, err = io.Copy(d, s)
	if err != nil {
		return err
	}
	return nil
}

func getVersion(versionURL string) (string, error) {
	resp, err := http.Get(versionURL)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(string(b)), nil
}

func download(downloadURL, filePath string) error {
	resp, err := http.Get(downloadURL)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	fileDir := filepath.Dir(filePath)
	if _, err := os.Stat(fileDir); err != nil {
		err := os.MkdirAll(fileDir, 0555)
		if err != nil {
			return err
		}
	}
	f, err := os.Create(filePath)
	if err != nil {
		return err
	}
	defer f.Close()
	_, err = io.Copy(f, resp.Body)
	if err != nil {
		return err
	}
	return nil
}

var (
	cmd      = "url.dll,FileProtocolHandler"
	runDll32 = filepath.Join(os.Getenv("SYSTEMROOT"), "System32", "rundll32.exe")
)

func openExe(exePath string, hideWin bool) error {
	p, err := filepath.Abs(exePath)
	if err != nil {
		return err
	}
	cmd := exec.Command(runDll32, cmd, p)
	if hideWin {
		cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
	}
	cmd.Start()
	cmd.Wait()
	return nil
}

func compareVersion(version1, version2 string) int {
	version1 = strings.TrimSuffix(version1, "-next")
	version2 = strings.TrimSuffix(version2, "-next")

	a1 := strings.Split(version1, ".")
	a2 := strings.Split(version2, ".")

	for i := 0; i < len(a1) && i < len(a2); i++ {
		v1, err := strconv.Atoi(a1[i])
		if err != nil {
			return 1
		}
		v2, err := strconv.Atoi(a2[i])
		if err != nil {
			return 1
		}
		if v1 != v2 {
			return v1 - v2
		}
	}
	return len(a1) - len(a2)
}