FIELD NOTE
单机程序自动升级功能实现(Golang)
在开发ETProxy测试代理工具的过程中,为了功能的快速迭代和升级成本的降低,需要实现工具的自动升级。
ETProxy的自动升级与一般程序的差异点为:ETProxy是一个单文件,免安装程序。相比一般的程序,不需要处理多文件增量升级的情况;但也正因为是单文件,所以升级功能得集成在主程序中,而不是用单独的下载更新器实现。
自动升级逻辑如下图所示:

关键代码如下:
up := updater.NewUpdate(conf.Update.URL, "etproxy.version.txt", "etproxy.exe", common.Version)
err := up.Do(func() {
})
if err != nil {
logger.Errorf("%+v", err)
}
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)
}
评论