Documentation
¶
Overview ¶
Package release 负责「把某个版本的 handoff 二进制正确落到某个路径」。
职责:
- 查 GitHub 的 latest release,解出 tag 与本平台资产的下载 URL
- 下载、校 sha256、解包、自检、原子替换,并把旧二进制留成 .prev
边界:
- **不决定何时替换**:那是 internal/selfupdate 的事。本包是一个执行器, 调用方说装就装
- 不知道 agentd、不知道任务、不读 handoff 的配置
- 不做自动回滚(D10):留下 .prev 供人工 handoff upgrade --rollback
install.go —— 下载、校验、自检、原子替换。
边界:
- 临时文件**必须**落在目标二进制的同目录:os.Rename 的原子性只在同一 文件系统内成立,从 /tmp rename 到 /usr/local/bin 会因跨设备直接失败
- 任何一步失败都清干净临时文件:留一份坏二进制在二进制目录里, 下一轮可能被误当成已就绪的 pending
- 不做自动回滚(D10):只把旧二进制留成 .prev,回退是人工命令
- 下载与安装是两件事,前者可跨平台、后者必须在目标平台执行
Index ¶
- Constants
- func Activate(newPath, target string) (string, error)
- func AssetName(tag, goos, goarch string) string
- func AssetURL(repo, tag, name string) string
- func CurrentPlatform() (string, string)
- func DesktopAssetName(tag, goos, goarch string) (string, bool)
- func PrevPath(target string) string
- func Rollback(target string) error
- func TempName(tag string) string
- type Asset
- type Client
- type Installer
- func (i *Installer) Fetch(ctx context.Context, rel Release, destDir string) (string, error)
- func (i *Installer) FetchArchive(ctx context.Context, rel Release, goos, goarch string) ([]byte, string, error)
- func (i *Installer) FetchByTag(ctx context.Context, repo, tag, goos, goarch, wantSum string) ([]byte, error)
- func (i *Installer) FetchChecksum(ctx context.Context, rel Release, goos, goarch string) (string, error)
- func (i *Installer) FetchChecksumFor(ctx context.Context, rel Release, assetName string) (string, error)
- func (i *Installer) InstallArchive(tgz []byte, wantSum, wantTag, destDir string) (string, error)
- type Release
Constants ¶
const ChecksumsName = "checksums.txt"
ChecksumsName 是校验和文件名,与 .github/workflows/release.yml 产出一致。
const DefaultAPIBase = "https://api.github.com"
DefaultAPIBase 是 GitHub REST API 的根。
D11:自动更新链路一律打 GitHub 原生 URL,不走自有域名——域名过期、DNS 故障、 重定向规则改错,任何一样都会让所有机器的自动更新一起哑掉。
const DefaultRepo = "Xsxdot/handoff"
DefaultRepo 是 Release 所在的 GitHub 仓库。
注意 go.mod 的 module path(github.com/Xsxdot/handoff)已与 GitHub owner 一致:`go install github.com/Xsxdot/handoff@latest` 与下载链指向同一个仓库。
const DownloadBase = "https://github.com"
DownloadBase 是 release 资产的下载根(GitHub 的确定性地址)。
D11 同理:自动更新链路一律打 GitHub 原生地址,不走自有域名。
Variables ¶
This section is empty.
Functions ¶
func Activate ¶
Activate 把新二进制换到目标路径,旧的留成 <target>.prev。
参数:
- newPath: Fetch 返回的临时文件路径(必须与 target 同目录)
- target: 目标二进制路径(应已 EvalSymlinks 解析过)
返回:
- 留存的旧二进制路径
- 错误:目录不可写、rename 失败
注意:
- 两次 rename 都是同目录内操作,因而是原子的。中途失败最坏的结果是 「旧的已挪到 .prev、新的还没就位」——此时目标路径暂时缺失, 所以第二次 rename 失败时会把 .prev 挪回来
- **两次 rename 的顺序在 Windows 上是承重的**:Windows 允许 rename 一个 正在运行的 exe,但不允许覆盖或删除它。所以「先把旧的挪走、再把新的挪进来」 恰好就是 Windows 自更新的标准手法。**不要**把它「优化」成先删后写—— 那在 unix 上照样绿,在 Windows 上当场炸
func AssetName ¶
AssetName 拼装某平台的资产名。
参数:
- tag: 版本号,形如 v0.1.0
- goos / goarch: 目标平台
返回:
- 资产文件名
注意:
- 格式必须与 .github/workflows/release.yml 里的产出**逐字一致**。 不一致的症状是查得到版本但下不到东西,且每轮重试
- 扩展名按平台分(见 archiveExt),install.sh / install.ps1 两边也依赖这条
func AssetURL ¶ added in v0.3.0
AssetURL 拼一个 release 资产的下载地址。
参数:
- repo: owner/name,如 Xsxdot/handoff
- tag: 版本号,形如 v0.2.3
- name: 资产文件名,用 AssetName 生成
返回:
- 完整下载地址
注意:
- GitHub 的这个地址是**确定性**的,不需要先查 API 就能拼出来。agentd 自拉时用的正是它——api.github.com 有 60 次/小时/IP 的匿名限流, 而多台执行机很可能共用一个代理出口 IP,走 API 迟早互相打架
func CurrentPlatform ¶
CurrentPlatform 返回当前进程的 goos/goarch,便于调用方少写两个 runtime 引用。
func DesktopAssetName ¶ added in v0.3.3
DesktopAssetName 返回桌面薄壳在某平台的发布物文件名。
参数:tag 形如 v0.3.1;goos/goarch 用 runtime 的取值。 返回:文件名与「该平台有没有薄壳发布物」。ok 为 false 时文件名为空, 调用方必须先判 ok,不能拿空串去拼下载地址。
注意:
- 前缀是 handoff-desktop_,与 CLI 的 handoff_ 不同。checksums.txt 里两者 并列,用 handoff_* 通配是匹配不到薄壳资产的(release.yml 的注释点名了这一条)。
- 不与 AssetName 合并:两者的扩展名规则完全不同,薄壳按 goos 选 dmg/zip/AppImage,合并会让两套互不相干的分支挤在一起。
- 发布流水线只构建 darwin/arm64、windows/amd64、linux/amd64 三种薄壳, 其余平台一律返回 false——判不出就说没有,不猜一个不存在的文件名。
Types ¶
type Client ¶
Client 查 GitHub release。
func NewClient ¶
func NewClient(tr http.RoundTripper) *Client
NewClient 构造 release 查询 client:30s 超时,打 GitHub 官方端点。
参数:
- tr: HTTP transport;**nil = 用标准库默认**(认 HTTPS_PROXY 等环境变量), 与本参数加入前的行为一字不差。要走配置里的代理,传 proxycfg.Transport 的产物
注意:
- 本包不读 handoff 配置(见 package 注释),所以收的是造好的 transport 而不是配置字符串——这条边界是刻意的,别"顺手"改成传 *config.Config
- 30s 而不是更长:查版本是一个可以失败的后台动作(失败就等下一个 interval), 卡住一个 goroutine 几分钟没有任何好处
type Installer ¶
type Installer struct {
HTTP *http.Client
Log *slog.Logger
// DownloadBase 是资产下载根,默认 release.DownloadBase。
// 存在的唯一理由是可测性:不覆盖它,FetchByTag 的测试必须真的打 github.com
DownloadBase string
}
Installer 执行下载与安装。
func NewInstaller ¶
func NewInstaller(log *slog.Logger, tr http.RoundTripper) *Installer
NewInstaller 构造默认 installer(10 分钟超时,覆盖慢网下的 20MB 下载)。
参数:
- log: 日志入口
- tr: HTTP transport;**nil = 用标准库默认**(认 HTTPS_PROXY 等环境变量)
func (*Installer) Fetch ¶
Fetch 下载本平台资产、校验、解包、自检,返回可供 Activate 的临时文件路径。
参数:
- ctx: 上下文
- rel: 目标发布
- destDir: 临时文件落点,**必须**与目标二进制同目录
返回:
- 临时二进制的完整路径(已 chmod 0755 并通过自检)
- 错误:缺资产、下载失败、校验不过、解包失败、自检不过
注意:
- 任何一步失败都会把临时文件删掉,不留残件
- 本函数是 FetchArchive + InstallArchive 的本机组合,行为与拆分前一致
func (*Installer) FetchArchive ¶
func (i *Installer) FetchArchive(ctx context.Context, rel Release, goos, goarch string) ([]byte, string, error)
FetchArchive 按指定平台下载资产并校验完整性,返回字节与期望哈希。
参数:
- ctx: 上下文
- rel: 目标发布
- goos / goarch: **目标机器**的平台,不是本机——跨平台推送时远端可能 是 linux/amd64 而本机是 darwin/arm64,必须知道该下哪份资产
返回:
- tgz: 资产原文(tar.gz 字节,**未经解包**)
- 期望的 sha256(十六进制小写),**来自 checksums.txt 的声明**—— 这是信任链的第一道校验,消费方把它原样传给 InstallArchive 让两端 比同一个来自 release 的声明,不互相背书
- 错误:缺资产、下载失败、校验不过
注意:
- **不解包、不自检**。自检要 exec 执行新二进制,而本机执行别的平台的 二进制必然失败——自检必须在目标平台上做(agentd 收到推送后)
- 不重试:完整性失败重试只会重下同一份坏数据(spec §4.7)
func (*Installer) FetchByTag ¶ added in v0.3.0
func (i *Installer) FetchByTag(ctx context.Context, repo, tag, goos, goarch, wantSum string) ([]byte, error)
FetchByTag 按 tag 拼出下载地址、下载资产并用**给定的** sha256 校验。
参数:
- ctx: 上下文
- repo: owner/name
- tag: 目标版本
- goos / goarch: 本机平台
- wantSum: 期望的 sha256(十六进制小写)。自拉模式下**来自协调者下发**
返回:
- 资产原文(tar.gz / zip 字节,未解包)
- 错误:下载失败、sha256 不符
注意:
- 与 FetchArchive 的区别是**不需要 Release 对象、不查 API**:地址由 AssetURL 确定性拼出,wantSum 由调用方给。这让执行机完全不碰 api.github.com(避开 60 次/小时/IP 的匿名限流)
- wantSum 由调用方给而不是自己去取 checksums,是刻意的:校验和与资产 走两条不同的信任路径,本机代理/镜像被投毒时才抓得住(spec §5.5)。 **别"优化"成自己下 checksums**
- 不重试:完整性失败重试只会重下同一份坏数据(spec §4.7)
func (*Installer) FetchChecksum ¶ added in v0.3.0
func (i *Installer) FetchChecksum(ctx context.Context, rel Release, goos, goarch string) (string, error)
FetchChecksum 只下载 checksums.txt 并解出本平台 CLI 资产的期望哈希。 保留是为了不动既有调用方;新代码请直接用 FetchChecksumFor。
参数:
- ctx: 上下文
- rel: 目标发布(需要它的 Assets 里有 checksums.txt 的 URL)
- goos / goarch: 目标机器的平台
返回:
- 该平台资产的 sha256(十六进制小写)
- 错误:缺 checksums 资产、下载失败、文件里没有该资产的行
注意:
- **不下资产**。这正是自拉模式的省流量点:协调者只下几百字节的 checksums, 20MB 的资产由执行机自己去下(spec §5.5)
- 一次 upgrade --now 涉及多台机器时,调用方应当只调一次并缓存—— 同一个 release 的 checksums.txt 对所有平台是同一份
func (*Installer) FetchChecksumFor ¶ added in v0.3.3
func (i *Installer) FetchChecksumFor(ctx context.Context, rel Release, assetName string) (string, error)
FetchChecksumFor 只下载 checksums.txt,并按调用方给定的资产名解出期望哈希。
参数:
- ctx:上下文,用于取消与超时
- rel:目标发布,需要它的 Assets 里有 checksums.txt 的 URL
- assetName:要查的完整资产名,由调用方给定;checksums.txt 里 CLI 与薄壳 资产并列,按平台推导只能推出其中一种
返回:
- 指定资产的 sha256(十六进制小写)
- 错误:缺 checksums 资产、下载失败、文件里没有该资产的行
注意:不下载资产本身;自拉模式只取几百字节的 checksums,20MB 的资产由 执行机自己下载。解析失败的错误会带上要找的资产名,便于定位发布清单问题。
func (*Installer) InstallArchive ¶
InstallArchive 校验、解包、自检一份已下载的资产,返回可供 Activate 的临时文件路径。
参数:
- tgz: FetchArchive 返回的资产原文
- wantSum: 期望的 sha256(十六进制小写),agentd 侧来自 CLI 推来的 query 参数,是信任链的第二道校验(传输完整性)
- wantTag: 目标版本,自检时拿新二进制 version 首行与它比对
- destDir: 临时文件落点,**必须**与目标二进制同目录
返回:
- 临时二进制的完整路径(已 chmod 0755 并通过自检)
- 错误:校验不过、解包失败、置位失败、自检不过
注意:
- 任何一步失败都会把临时文件删掉,不留残件