Next.js logomark实现 Go 程序自更新

2023年10月8日

Openai logomark
Hamster1963

这篇文章是《用 Go 构建一套网络监控系统》的第二篇,主要针对监控客户端程序在员工电脑安装后如何通过自动更新来实现更多的监控服务功能。

自动更新核心模块

在 Windows 电脑上实现客户端程序的自动更新并不是一件容易的事情,我们想要实现的目标为:

  1. 通过版本号进行客户端的管理
  2. 客户端自动获取最新版本号与本地版本号进行比对
  3. 如需更新,自动在后台进行更新操作,无需用户介入

最终我们采用 git tag 进行客户端版本的管理,使用 GitHub Release 进行二进制文件的存储与分发,最终在客户端上获取最新版本的客户端进行自更新。

'Untitled.png

客户端版本管理

程序的版本号管理有着许多的方案,可以将版本号人工记录在程序中,也可以从外部注入,无论如何,有一个统一且可追溯的打版本方案就是最好的,因此我们采用了 git tag 作为版本号管理的方式,使用 git tag 有许多的好处:

  1. 使用统一的 git tag v0.. 的命令进行打点,学习成本很低
  2. 可在 git 提交时间轴上清晰的看到每个版本所对应的提交,在问题追溯或版本回退上十分灵活。
  3. 使用 git tag 打点可以搭配 GitHub Actions 进行自动化注入版本号与编译分发,使得客户端的版本号管理轻量却强大。

GitHub Actions

在开发测试完成后,通过 git tag 对当前最新提交进行打版本号,在推送版本号后,GitHub Actions 将会进行一系列的流程以保证当前程序的可靠性。

  1. 进行更新日志的生成
  2. 进行代码规范性检查
  3. 进行代码编译测试
  4. 进行客户端自动化测试
  5. 进行最终编译并生成 GitHub Realease

更新日志

在更新日志方面,也可以参考先前的文章,利用 git-cliff进行版本更新日志的生成

搭配GitHub Actions为Release增加Changelog | Hamster

生成日志后便可以在 GitHub Realease 中统一看到每个版本号的更新日志,更新日志生成与上传无需人工介入,还是蛮好的。

代码规范性检查

虽然在每次提交后已经有自动化的代码规范性检查,但为了保险起见还需要再一次进行检查,

采用golangci-lint 对代码的规范性再一次进行检查,保证没有不必要的赋值或错误的返回参数影响程序运行或降低程序性能。

Untitled

代码编译测试

在编译测试中,同时对 Go 1.20/1.21/stable 三个版本进行编译测试,这是因为部分杀毒软件会将太新的 go 版本编译出的二进制文件当作病毒(360 杀毒…),因此我们需要在多个Go版本中保证程序都可以正常编译,以留出临时降低 Go 编译版本的调整空间。

在 workflow 文件中通过 strategy-**matrix** 的方式定义多个 Go 版本并行进行编译测试。

Untitled

客户端自动化测试

对于客户端程序而言,是否能在用户电脑上正常运行功能才是最关键的,因此需要在用户电脑环境(Windows)中进行功能的测试,因此我们在 Go 代码中编写单元测试以进行核心功能的测试。

在编译测试版本时,通过注入测试参数以让单元测试代码可以正确获取到各种环境参数。

go test -v ./g_test -tag="@test" -baseurl="${{ secrets.BACKEND_URL }}"

在单元测试代码中,通过flag.String 来正确获取传入的参数

InputGithubTagFlag = flag.String("tag", "", "get github tag")

Untitled

在客户端自动化测试完成之后,就可以进行最终的编译与分发过程了。

最终编译

在最终编译与分发上,采用hamster1963/go-release-action@v1.2 的 action 来进行。

只需要指定需要发布到 GitHub Release 的文件名即可方便的将代码编译并分发。

Untitled

注入版本号

在最终编译过程中,在客户端程序在编译时,通过 -ldflags 进行版本号与编译信息的注入,在程序中再对版本号进行使用。

具体实现方式可以查看之前的文章:

利用Github Actions为Go程序添加git与编译信息 | Hamster

在注入版本号之后,在客户端程序中就可以以变量的形式获取到当前客户端的版本号。

Untitled

客户端自动更新

在 GitHub 整套流程搭建好后,在客户端就是定时比对本地版本与最新版本的差异,如不同便进行更新即可,说起来容易,可做起来还是遇到了不少的坑。

我们主要遇到了几个问题:

  1. 如何将客户端一直运行在用户电脑上
  2. 如果实现获取最新版本
  3. 获取最新版本后如何进行自更新

服务注册

首先第一个问题便是非常经典的如何保持后台运行的问题,在 Windows 中进行服务注册没有像 Linux 或 macOS 中如此顺畅,需要许多的外部程序帮助我们去实现在系统中进行注册。

在寻找的过程中,主要发现了两个可能会使用的外部程序,winsw 与 nssm

https://github.com/winsw/winsw

NSSM - the Non-Sucking Service Manager

两者都是针对服务在 Windows 环境下的注册为核心功能的。

最终采用了 winsw ,原因还蛮简单的,就是 nssm 太久没更新了,而 winsw 还在积极开发中。

我们在用户电脑上安装时,搭配 Go 编写的安装程序与 winsw 配置文件来进行服务的注册。

speed cron process.xml

'Ray.so Export.png

在配置文件中,核心在于配置 onfailure 的行为,不仅可以帮助程序在崩溃后可以重新启动,这个功能也正是可以实现自动更新的核心行为。

获取程序最新版本

由于版本管理是基于 git tag 与 GitHub Realease,因此客户端只需要定时查询在 GitHub 上的最新客户端版本即可。

获取 GitHub 上的最新版本可以通过 API 来进行获取,但 GitHub 对于 API 的每分钟请求次数有限制。因此需要在后端进行最新版本的获取后进行缓存,在后续的客户端请求最新版本时直接返还缓存的最新版本,直到缓存过期再去 GitHub 获取最新的客户端版本再进行缓存。

缓存用到了 gcache 的gcache.MustGetOrSetFuncLock 方法,客户端请求最新版本,后端未获取到缓存时进行获取,如缓存中有数据则直接返回。这种方法可以降低后端的请求压力,同时使用 Lock 确保同一时间只会有一次的外部请求获取后缓存的行为。

定时检查最新版本

在客户端程序中,采用定时任务的方式对检查更新方法进行注册,每一分钟进行一次版本的检查,如在进行测速或其他任务则跳过检查,等待运行任务完成后再进行版本的检查。

对比的逻辑很简单,客户端只需要将本地的版本号(编译时注入)与获取到的最新版本号进行比对,如版本号不相同则进行客户端更新操作。

在具体的版本号管理中,在数据库建立version字段,客户端每次的上传数据中都会携带 version 字段,在管理端就可以很清晰的看到新版本发布后客户端的更新情况。

Untitled

更新本地客户端文件

实现更新的核心思路是替换原有的二进制文件,退出当前进程,让 winsw 重新启动,在启动的时候就会以最新获取到的二进制文件作为程序启动,更新就完成了。

具体的实现是通过 go-update.v0 库来实现的。

Untitled

需要注意的是,在 Windows 中,无法直接覆盖旧文件,只能将旧文件进行重命名,本项目将旧二进制文件后缀加上.old 表示为旧文件。

在更新完成后,使用os.Exit(1)退出,等待winsw使用新二进制文件重新启动,更新完成。

Untitled

后记

在客户端获取最新二进制文件的过程中,可能由于国内网络的原因比较难进行下载,因此可以通过搭建 GitHub Proxy 的方式,通过代理URL 的方式进行二进制文件的加速下载即可。

谢谢阅读🙏。