- 新增 internal/selfupdate:发行坐标(基址常量+env 覆盖)、Gitea latest tag 查询、sha256 旁车校验下载、原子换名(旧二进制留 .bak、 失败可回滚),含单测 - 主控:UpdatePanels 对 agent 节点改为记录更新意向(最新 tag 的 V 系→A 系目标;已达标报错不记意向);意向经现有 GET config 轮询 捎带下发;心跳收 updateStatus 回报(failed/current 清意向留状态、 过期 requested_at 忽略),遥测版本达标自动确认 success - agent:识别意向→比自身 A 版本(已达标报 current 不下载)→按架构 下载资产+sha256 校验→换名留 .bak→刷一次心跳→stopXray→ syscall.Exec 自重启;exec 失败回滚 .bak 并报 failed;失败 10 分钟 内不重下同一意向;旧版全程保持运行 - NodeView 增 agentUpdateState/Error/Target 供节点页可观测 - 单测:selfupdate 换名/校验、agent 换名重启/坏校验保旧版/已达标 跳过/exec 失败回滚、主控意向记录/已达标跳过、config 捎带意向、 心跳 failed 清意向、心跳新版本确认 success
131 lines
3.7 KiB
Go
131 lines
3.7 KiB
Go
package selfupdate
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseSHA256Sidecar(t *testing.T) {
|
|
sum := sha256.Sum256([]byte("payload"))
|
|
hexSum := hex.EncodeToString(sum[:])
|
|
cases := []struct {
|
|
in string
|
|
want string
|
|
ok bool
|
|
}{
|
|
{hexSum + " x-ui-linux-amd64\n", hexSum, true},
|
|
{hexSum + "\n", hexSum, true},
|
|
{strings.ToUpper(hexSum) + " *x-ui-linux-amd64", hexSum, true},
|
|
{"", "", false},
|
|
{"not-a-digest file", "", false},
|
|
{hexSum[:40] + " file", "", false},
|
|
}
|
|
for _, tc := range cases {
|
|
got, ok := ParseSHA256Sidecar([]byte(tc.in))
|
|
if got != tc.want || ok != tc.ok {
|
|
t.Fatalf("ParseSHA256Sidecar(%q) = %q, %v; want %q, %v", tc.in, got, ok, tc.want, tc.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAssetName(t *testing.T) {
|
|
if name, err := AssetName("amd64"); err != nil || name != "x-ui-linux-amd64" {
|
|
t.Fatalf("amd64: %q, %v", name, err)
|
|
}
|
|
if name, err := AssetName("arm64"); err != nil || name != "x-ui-linux-arm64" {
|
|
t.Fatalf("arm64: %q, %v", name, err)
|
|
}
|
|
if _, err := AssetName("riscv64"); err == nil {
|
|
t.Fatal("unknown arch must error")
|
|
}
|
|
}
|
|
|
|
func TestSwapExecutableKeepsBackup(t *testing.T) {
|
|
dir := t.TempDir()
|
|
exe := filepath.Join(dir, "xui-agent")
|
|
if err := os.WriteFile(exe, []byte("old-binary"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
next := filepath.Join(dir, ".x-ui-update-1")
|
|
if err := os.WriteFile(next, []byte("new-binary"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bak, err := SwapExecutable(exe, next)
|
|
if err != nil {
|
|
t.Fatalf("SwapExecutable: %v", err)
|
|
}
|
|
if bak != exe+".bak" {
|
|
t.Fatalf("backup path = %q", bak)
|
|
}
|
|
got, _ := os.ReadFile(exe)
|
|
if string(got) != "new-binary" {
|
|
t.Fatalf("exe content = %q, want new-binary", got)
|
|
}
|
|
old, _ := os.ReadFile(bak)
|
|
if string(old) != "old-binary" {
|
|
t.Fatalf("bak content = %q, want old-binary", old)
|
|
}
|
|
info, _ := os.Stat(exe)
|
|
if info.Mode().Perm()&0o100 == 0 {
|
|
t.Fatal("new binary is not executable")
|
|
}
|
|
// Rollback restores the old binary.
|
|
if err := RollbackExecutable(exe); err != nil {
|
|
t.Fatalf("RollbackExecutable: %v", err)
|
|
}
|
|
got, _ = os.ReadFile(exe)
|
|
if string(got) != "old-binary" {
|
|
t.Fatalf("after rollback exe = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestDownloadVerified(t *testing.T) {
|
|
payload := []byte("fake-release-binary")
|
|
sum := sha256.Sum256(payload)
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.HasSuffix(r.URL.Path, ".sha256"):
|
|
_, _ = w.Write([]byte(hex.EncodeToString(sum[:]) + " x-ui-linux-amd64\n"))
|
|
default:
|
|
_, _ = w.Write(payload)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dest := filepath.Join(t.TempDir(), "dl")
|
|
if err := DownloadVerified(context.Background(), srv.Client(), srv.URL+"/x-ui-linux-amd64", srv.URL+"/x-ui-linux-amd64.sha256", dest); err != nil {
|
|
t.Fatalf("DownloadVerified: %v", err)
|
|
}
|
|
got, _ := os.ReadFile(dest)
|
|
if string(got) != string(payload) {
|
|
t.Fatalf("downloaded content = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestDownloadVerifiedRejectsBadChecksum(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasSuffix(r.URL.Path, ".sha256") {
|
|
_, _ = w.Write([]byte(strings.Repeat("0", 64) + " x-ui-linux-amd64\n"))
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte("tampered"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dest := filepath.Join(t.TempDir(), "dl")
|
|
err := DownloadVerified(context.Background(), srv.Client(), srv.URL+"/a", srv.URL+"/a.sha256", dest)
|
|
if err == nil || !strings.Contains(err.Error(), "sha256 mismatch") {
|
|
t.Fatalf("want sha256 mismatch error, got %v", err)
|
|
}
|
|
if _, statErr := os.Stat(dest); !os.IsNotExist(statErr) {
|
|
t.Fatal("partial download must be removed on mismatch")
|
|
}
|
|
}
|