Browse Source

完成upgrade.service自动升级服务编码,编译通过,等待自测试

niujiuru 2 days ago
parent
commit
ebc3ededf4

+ 9 - 1
Makefile

@@ -33,7 +33,7 @@ D_LIBS2 += -L$(RTU_LINUX_MODULES_PATH)/dh_takephoto/lib/armv7hf -lMVSDK -liImage
 D_LIBS2 += -L$(RTU_LINUX_MODULES_PATH)/dh_takephoto/lib/armv7hf/GenICam/bin -lGCBase_gcc483_v3_0 -lGenApi_gcc483_v3_0 -lLog_gcc483_v3_0 -llog4cpp_gcc483_v3_0 -lMathParser_gcc483_v3_0 -lNodeMapData_gcc483_v3_0 -lXmlParser_gcc483_v3_0
 
 # 编译的目标
-all: networkd timesyncd gnss sshd-mqtt-bridge camera-capture app-install web-ui
+all: networkd timesyncd gnss sshd-mqtt-bridge camera-capture app-install web-ui upgrade
 
 # 可执行程序
 networkd : LK_LIBS = $(S_LIBS0) $(S_LIBS1) $(D_LIBS0)
@@ -83,6 +83,14 @@ web-ui:
 	$(SETGO_ENV) CGO_LDFLAGS="$(LK_LIBS)" $(GO_BUILD) $(GO_FLAGS) -o $@ ./web-ui.service/*.go
 	cp -f $@ ./build/ && rm -f $@
 
+upgrade : LK_LIBS = $(S_LIBS0) $(S_LIBS1) $(D_LIBS0)
+upgrade : libswapi.a libair720u.a libec200u.a
+upgrade:
+	mkdir -p ./build
+	$(GO) mod tidy
+	$(SETGO_ENV) CGO_LDFLAGS="$(LK_LIBS)" $(GO_BUILD) $(GO_FLAGS) -o $@ ./upgrade.service/*.go
+	cp -f $@ ./build/ && rm -f $@
+
 # 静态库模块
 libswapi.a :
 	$(MAKE) -C $(RTU_LINUX_MODULES_PATH)/swapi         target=armv7hf $@

+ 16 - 0
config/yfkj-upgrade.service

@@ -0,0 +1,16 @@
+[Unit]
+Description=Yunfei Service Upgrade
+After=yfkj-networkd.service
+Wants=yfkj-networkd.service
+
+[Service]
+Environment="PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
+ExecStart=/opt/yfkj/upgrade.service/upgrade
+Restart=always
+RestartSec=30
+StandardOutput=journal
+StandardError=journal
+
+[Install]
+WantedBy=multi-user.target
+

+ 3 - 0
go.mod

@@ -12,6 +12,9 @@ require (
 require (
 	github.com/alexflint/go-filemutex v1.3.0 // indirect
 	github.com/beevik/ntp v1.5.0 // indirect
+	github.com/hashicorp/errwrap v1.0.0 // indirect
+	github.com/hashicorp/go-multierror v1.1.1 // indirect
+	github.com/jlaffaye/ftp v0.2.0 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
 	github.com/vishvananda/netlink v1.3.1 // indirect
 	github.com/vishvananda/netns v0.0.5 // indirect

+ 6 - 0
go.sum

@@ -9,6 +9,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
+github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=
 github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk=
 github.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

+ 71 - 0
servicelib/utils.go

@@ -1,7 +1,12 @@
 package servicelib
 
 import (
+	"crypto/sha256"
+	"encoding/hex"
+	"fmt"
+	"io"
 	"net"
+	"net/url"
 	"os"
 	"path/filepath"
 	"strings"
@@ -39,3 +44,69 @@ func ReadVersionFile(dir string) (string, error) {
 	}
 	return strings.TrimSpace(string(data)), nil
 }
+
+type UpgradeURL struct {
+	Scheme string
+	Host   string
+	User   string
+	Pass   string
+	Path   string
+}
+
+func ParseUpgradeURL(rawURL string) (*UpgradeURL, error) {
+	u, err := url.Parse(rawURL)
+	if err != nil {
+		return nil, fmt.Errorf("invalid url: %w", err)
+	}
+
+	if u.Scheme == "" {
+		return nil, fmt.Errorf("scheme is required")
+	}
+
+	scheme := strings.ToLower(u.Scheme)
+	switch scheme {
+	case "ftp", "ftps", "http", "https":
+	default:
+		return nil, fmt.Errorf("unsupported scheme: %s", scheme)
+	}
+
+	if u.Host == "" {
+		return nil, fmt.Errorf("host is required")
+	}
+
+	result := &UpgradeURL{
+		Scheme: scheme,
+		Host:   u.Host,
+		Path:   u.Path,
+	}
+
+	if u.User != nil {
+		result.User = u.User.Username()
+		if pass, ok := u.User.Password(); ok {
+			result.Pass = pass
+		}
+	}
+
+	return result, nil
+}
+
+func CheckFileSHA256(file string, expected string) error {
+	f, err := os.Open(file)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	hash := sha256.New()
+	if _, err := io.Copy(hash, f); err != nil {
+		return err
+	}
+
+	actual := hex.EncodeToString(hash.Sum(nil))
+
+	if !strings.EqualFold(actual, expected) {
+		return fmt.Errorf("文件校验失败: 期望 %s, 实际 %s", expected, actual)
+	}
+
+	return nil
+}

+ 10 - 1
upgrade.service/main.go

@@ -29,6 +29,8 @@ type program struct {
 	name        string
 	core_server *jsonrpc2.RPCServer
 	http_server *http.Server
+	ctx         context.Context
+	cancel      context.CancelFunc
 }
 
 // Start should not block. Do the actual work async.
@@ -73,6 +75,9 @@ func (p *program) Start(s service.Service) error {
 		Handler: mux,
 	}
 
+	p.ctx, p.cancel = context.WithCancel(context.Background())
+	go p.upgradeLoop()
+
 	go p.run()
 
 	return nil
@@ -96,6 +101,10 @@ func (p *program) run() {
 
 // Stop should not block. Return with a few seconds.
 func (p *program) Stop(s service.Service) error {
+	if p.cancel != nil {
+		p.cancel()
+	}
+
 	if p.core_server != nil {
 		p.core_server.Stop()
 	}
@@ -113,7 +122,7 @@ func (p *program) Stop(s service.Service) error {
 }
 
 func main() {
-	baseapp.SetOptDirs(true, true, false, false)
+	baseapp.SetOptDirs(true, false, true, false)
 	baseapp.InitPath()
 
 	svcFlag := flag.String("service", "", "Control the yfkj-upgrade service.")

+ 16 - 0
upgrade.service/test.txt

@@ -0,0 +1,16 @@
+接口测试:
+
+// ping联通测试
+curl -s -X POST http://127.0.0.1:7005/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"ping","params":{},"id":1}'
+
+// 获取软件版本
+curl -s -X POST http://127.0.0.1:7005/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"basic.getBuildVer","params":{},"id":2}'
+
+// 获取日志级别
+curl -s -X POST http://127.0.0.1:7005/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"basic.getLogLevel","params":{},"id":3}'
+
+// 设置日志级别
+curl -s -X POST http://127.0.0.1:7005/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"basic.setLogLevel","params":{"log_level":"trace"},"id":4}'
+
+// 保存日志设置
+curl -s -X POST http://127.0.0.1:7005/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"basic.saveLogConf","params":{},"id":5}'

+ 380 - 0
upgrade.service/upgrade.go

@@ -1 +1,381 @@
 package main
+
+import (
+	"context"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"rtu_linux_services/servicelib"
+
+	"hnyfkj.com.cn/rtu/linux/baseapp"
+	"hnyfkj.com.cn/rtu/linux/netmgrd"
+	"hnyfkj.com.cn/rtu/linux/utils/ftpclient"
+)
+
+type serviceUpgradeConfig struct {
+	Name        string
+	Enable      bool
+	CheckVerURL string
+	ClonePkgURL string
+	InstallPath string
+}
+
+var upgradeServices = []serviceUpgradeConfig{
+	{ // 1, 网络管理服务
+		Name:        "networkd",
+		Enable:      true,
+		CheckVerURL: "",
+		ClonePkgURL: "",
+		InstallPath: "/opt/yfkj/networkd.service",
+	},
+	{ // 2, 时间同步服务
+		Name:        "timesyncd",
+		Enable:      true,
+		CheckVerURL: "",
+		ClonePkgURL: "",
+		InstallPath: "/opt/yfkj/timesyncd.service",
+	},
+	{ // 3, 地理位置服务
+		Name:        "gnss",
+		Enable:      true,
+		CheckVerURL: "",
+		ClonePkgURL: "",
+		InstallPath: "/opt/yfkj/gnss.service",
+	},
+	{ // 4, 相机采集服务
+		Name:        "camera-capture",
+		Enable:      true,
+		CheckVerURL: "",
+		ClonePkgURL: "",
+		InstallPath: "/opt/yfkj/camera-capture.service",
+	},
+	{ // 5, 应用安装服务
+		Name:        "app-install",
+		Enable:      true,
+		CheckVerURL: "",
+		ClonePkgURL: "",
+		InstallPath: "/opt/yfkj/app-install.service",
+	},
+	{ // 6, 远程运维服务
+		Name:        "sshd-mqtt-bridge",
+		Enable:      true,
+		CheckVerURL: "",
+		ClonePkgURL: "",
+		InstallPath: "/opt/yfkj/sshd-mqtt-bridge.service",
+	},
+	{ // 7, 管理后台服务
+		Name:        "web-ui",
+		Enable:      true,
+		CheckVerURL: "",
+		ClonePkgURL: "",
+		InstallPath: "/opt/yfkj/web-ui.service",
+	},
+	{ // 8, 自动升级服务
+		Name:        "upgrade",
+		Enable:      true,
+		CheckVerURL: "",
+		ClonePkgURL: "",
+		InstallPath: "/opt/yfkj/upgrade.service",
+	},
+}
+
+func isNetworkOK() bool {
+	_, _, tcpOK, httpOK, _ := netmgrd.CheckNetwork()
+	return tcpOK || httpOK
+}
+
+func (p *program) upgradeLoop() {
+	for !isNetworkOK() {
+		select {
+		case <-time.After(30 * time.Second):
+		case <-p.ctx.Done():
+			return
+		}
+	}
+
+	p.checkUpgrade()
+
+	ticker := time.NewTicker(24 * time.Hour)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-ticker.C:
+			if isNetworkOK() {
+				p.checkUpgrade()
+			}
+		case <-p.ctx.Done():
+			return
+		}
+	}
+}
+
+func (p *program) checkUpgrade() {
+	baseapp.Logger.Infof("[%s] ### 开始自动升级检查 ###", p.name)
+	defer baseapp.Logger.Infof("[%s] ### 自动升级检查结束 ###", p.name)
+
+	for _, service := range upgradeServices {
+		if !service.Enable || service.CheckVerURL == "" || service.ClonePkgURL == "" {
+			continue
+		}
+
+		select {
+		case <-p.ctx.Done():
+			return
+		default:
+		}
+
+		oldVersion, newVersion, upgraded, err := p.upgradeService(service)
+		if err != nil {
+			baseapp.Logger.Errorf("[%s] 服务 yfkj-%s.service 自动升级失败: %v!!", p.name, service.Name, err)
+			continue
+		}
+
+		if upgraded {
+			baseapp.Logger.Infof("[%s] 服务 yfkj-%s.service 自动升级成功: %s -> %s", p.name, service.Name, oldVersion, newVersion)
+		}
+	}
+}
+
+func (p *program) upgradeService(service serviceUpgradeConfig) (string, string, bool, error) {
+	currentVersion, err := servicelib.ReadVersionFile(service.InstallPath)
+	if err != nil {
+		return "", "", false, fmt.Errorf("读取当前版本信息失败: %w", err)
+	}
+
+	info, err := getUpgradeInfo(p.ctx, service.CheckVerURL, 30*time.Second)
+	if err != nil {
+		return "", "", false, fmt.Errorf("获取远程升级信息失败: %w", err)
+	}
+
+	var currentVersionCode, newVersionCode baseapp.VersionCode
+
+	if err := currentVersionCode.FromString(currentVersion); err != nil {
+		return "", "", false, fmt.Errorf("解析当前版本信息失败: %w", err)
+	}
+
+	if err := newVersionCode.FromString(info.Version); err != nil {
+		return "", "", false, fmt.Errorf("解析远程版本信息失败: %w", err)
+	}
+
+	if newVersionCode <= currentVersionCode { // 只允许升级,不允许降级
+		return currentVersion, info.Version, false, nil
+	}
+
+	currentDir, err := getCurrentVersionDir(service.InstallPath, service.Name)
+	if err != nil {
+		return "", "", false, fmt.Errorf("获取当前版本目录失败: %w", err)
+	}
+
+	targetName := nextVersionDir(currentDir)
+	targetDir := filepath.Join(service.InstallPath, targetName)
+	pkg := filepath.Join(os.TempDir(), service.Name+".tar.gz")
+	defer os.Remove(pkg)
+
+	if err := downloadUpgradePackage(p.ctx, service.ClonePkgURL, pkg, 10*time.Minute); err != nil {
+		return currentVersion, "", false, fmt.Errorf("下载服务升级包失败: %w", err)
+	}
+
+	if err := servicelib.CheckFileSHA256(pkg, info.SHA256); err != nil {
+		return currentVersion, "", false, fmt.Errorf("校验服务升级包失败: %w", err)
+	}
+
+	if err := installPackage(pkg, targetDir, service); err != nil {
+		return currentVersion, "", false, fmt.Errorf("安装服务升级包失败: %w", err)
+	}
+
+	if err := switchServiceVersion(service.Name, service.InstallPath, targetName); err != nil {
+		return currentVersion, "", false, fmt.Errorf("切换服务版本失败: %w", err)
+	}
+
+	return currentVersion, info.Version, true, nil
+}
+
+type upgradeInfo struct {
+	Version string `json:"version"`
+	SHA256  string `json:"sha256"`
+}
+
+// 获取远程升级信息,返回版本号和升级包SHA256校检值
+func getUpgradeInfo(ctx context.Context, rawURL string, timeout time.Duration) (upgradeInfo, error) {
+	u, err := servicelib.ParseUpgradeURL(rawURL)
+	if err != nil {
+		return upgradeInfo{}, err
+	}
+
+	switch u.Scheme {
+	case "ftp":
+		return getFtpUpgradeInfo(ctx, u, timeout)
+	default:
+		return upgradeInfo{}, fmt.Errorf("暂不支持的升级协议: %s", u.Scheme)
+	}
+}
+
+func getFtpUpgradeInfo(ctx context.Context, u *servicelib.UpgradeURL, timeout time.Duration) (upgradeInfo, error) {
+	localFile, err := ftpclient.DownloadFileFromFtp(ctx, u.Host, u.User, u.Pass, u.Path, timeout)
+	if err != nil {
+		return upgradeInfo{}, err
+	}
+
+	defer os.Remove(localFile)
+
+	data, err := os.ReadFile(localFile)
+	if err != nil {
+		return upgradeInfo{}, err
+	}
+
+	var info upgradeInfo
+	if err := json.Unmarshal(data, &info); err != nil {
+		return upgradeInfo{}, err
+	}
+
+	info.Version = strings.TrimSpace(info.Version)
+	info.SHA256 = strings.TrimSpace(info.SHA256)
+
+	if info.Version == "" {
+		return upgradeInfo{}, fmt.Errorf("版本号字段为空")
+	}
+
+	if info.SHA256 == "" {
+		return upgradeInfo{}, fmt.Errorf("SHA256字段为空")
+	}
+
+	if len(info.SHA256) != 64 {
+		return upgradeInfo{}, fmt.Errorf("SHA256格式错误")
+	}
+
+	if _, err := hex.DecodeString(info.SHA256); err != nil {
+		return upgradeInfo{}, fmt.Errorf("SHA256格式错误")
+	}
+
+	return info, nil
+}
+
+// 下载升级包.tar.gz文件,下载完成后存入 localFile
+func downloadUpgradePackage(ctx context.Context, rawURL string, localFile string, timeout time.Duration) error {
+	u, err := servicelib.ParseUpgradeURL(rawURL)
+	if err != nil {
+		return err
+	}
+
+	switch u.Scheme {
+	case "ftp":
+		return downloadFtpUpgradePackage(ctx, u, localFile, timeout)
+	default:
+		return fmt.Errorf("暂不支持的升级协议: %s", u.Scheme)
+	}
+}
+
+func downloadFtpUpgradePackage(ctx context.Context, u *servicelib.UpgradeURL, localFile string, timeout time.Duration) error {
+	file, err := ftpclient.DownloadFileFromFtp(ctx, u.Host, u.User, u.Pass, u.Path, timeout)
+	if err != nil {
+		return err
+	}
+
+	defer os.Remove(file)
+
+	return os.Rename(file, localFile)
+}
+
+// 获取当前软链接指向的版本目录,返回: a、b、c 之一
+func getCurrentVersionDir(installPath string, serviceName string) (string, error) {
+	currentLink := filepath.Join(installPath, serviceName)
+
+	target, err := os.Readlink(currentLink)
+	if err != nil {
+		return "", err
+	}
+
+	if !filepath.IsAbs(target) {
+		target = filepath.Join(installPath, target)
+	}
+
+	target = filepath.Clean(target)
+
+	name := filepath.Base(target)
+	switch name {
+	case "a", "b", "c":
+		return target, nil
+	default:
+		return "", fmt.Errorf("当前软链接指向无效的版本目录: %s", target)
+	}
+}
+
+// a 为出厂版本,固定不变;升级版本在 b、c 之间轮转
+func nextVersionDir(currentDir string) string {
+	switch filepath.Base(currentDir) {
+	case "a":
+		return "b"
+	case "b":
+		return "c"
+	case "c":
+		return "b"
+	default:
+		return "b"
+	}
+}
+
+// 升级包仅包含与服务同名的可执行程序,只解压该程序
+func installPackage(pkg string, targetDir string, service serviceUpgradeConfig) error {
+	if err := os.MkdirAll(targetDir, 0755); err != nil {
+		return fmt.Errorf("创建版本目录失败: %w", err)
+	}
+
+	cmd := exec.Command("tar", "-xzf", pkg, "-C", targetDir, service.Name)
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		return fmt.Errorf("安装 %s 失败: %w: %s", service.Name, err,
+			strings.TrimSpace(string(output)))
+	}
+
+	return nil
+}
+
+// 切换服务版本,新版本启动失败时自动回滚并重启服务
+func switchServiceVersion(serviceName string, installPath string, targetName string) error {
+	service := "yfkj-" + serviceName + ".service"
+	currentLink := filepath.Join(installPath, serviceName)
+	tempLink := filepath.Join(installPath, serviceName+".tmp")
+
+	currentTarget, err := os.Readlink(currentLink)
+	if err != nil {
+		return err
+	}
+
+	if err := exec.Command("systemctl", "stop", service).Run(); err != nil {
+		return err
+	}
+
+	_ = os.Remove(tempLink)
+
+	if err := os.Symlink(targetName, tempLink); err != nil {
+		return err
+	}
+
+	if err := os.Rename(tempLink, currentLink); err != nil {
+		_ = os.Remove(tempLink)
+		return err
+	}
+
+	if err := exec.Command("systemctl", "start", service).Run(); err != nil {
+		startErr := err
+		_ = os.Remove(tempLink)
+
+		if err := os.Symlink(currentTarget, tempLink); err == nil {
+			if err := os.Rename(tempLink, currentLink); err == nil {
+				_ = exec.Command("systemctl", "start", service).Run()
+			}
+		}
+
+		return startErr
+	}
+
+	return nil
+}

+ 0 - 58
upgrade.service/upgrade.json

@@ -1,58 +0,0 @@
-{
-  "yfkj-service-upgrade": {
-    "networkd": {
-      "enable": true,
-      "versionUrl": "",
-      "packageUrl": "",
-      "installDir": "/opt/yfkj/networkd.service"
-    },
-    "timesyncd": {
-      "enable": true,
-      "versionUrl": "",
-      "packageUrl": "",
-      "installDir": "/opt/yfkj/timesyncd.service"
-    },
-    "gnss": {
-      "enable": true,
-      "versionUrl": "",
-      "packageUrl": "",
-      "installDir": "/opt/yfkj/gnss.service"
-    },
-    "camera-capture": {
-      "enable": true,
-      "versionUrl": "",
-      "packageUrl": "",
-      "installDir": "/opt/yfkj/camera-capture.service"
-    },
-    "app-install": {
-      "enable": true,
-      "versionUrl": "",
-      "packageUrl": "",
-      "installDir": "/opt/yfkj/app-install.service"
-    },
-    "sshd-mqtt-bridge": {
-      "enable": true,
-      "versionUrl": "",
-      "packageUrl": "",
-      "installDir": "/opt/yfkj/sshd-mqtt-bridge.service"
-    },
-    "web-ui": {
-      "enable": true,
-      "versionUrl": "",
-      "packageUrl": "",
-      "installDir": "/opt/yfkj/web-ui.service"
-    },
-    "upgrade": {
-      "enable": true,
-      "versionUrl": "",
-      "packageUrl": "",
-      "installDir": "/opt/yfkj/upgrade.service"
-    },
-    "user-app": {
-    "enable": true,
-    "versionUrl": "",
-    "packageUrl": "",
-    "installDir": "/home/root/yfkj-user-app"
-    }
-  }
-}

+ 3 - 3
web-ui.service/web/pages/page1.html

@@ -196,15 +196,15 @@
 
         <div class="info-item">
           <div class="info-info">
-            <div class="info-name">开机自动升级</div>
+            <div class="info-name">设备自动升级</div>
             <div class="info-desc">upgrade.service</div>
           </div>
           <span class="info-value" id="var9">--</span>
           <span class="info-value" id="var99">--</span>
-          <button class="btn" style="margin-right:02px;" onclick="switchAutoUpgrade(event, 'enable')">
+          <button class="btn" style="margin-right:02px;" onclick="switchAutoUpgrade(event, 'enable --now')">
             开启
           </button>
-          <button class="btn" style="margin-right:32px;" onclick="switchAutoUpgrade(event, 'disable')">
+          <button class="btn" style="margin-right:32px;" onclick="switchAutoUpgrade(event, 'disable --now')">
             关闭
           </button>
           <button class="btn" onclick="downloadUpgradeLogFile()">

+ 4 - 2
web-ui.service/web/static/js/page1.js

@@ -239,7 +239,9 @@ function switchServiceVersion(service, action)
 }
 
 function switchAutoUpgrade(event, action) {
-  let msg = action === "enable"
+  const enable = action.startsWith("enable");
+
+  let msg = enable
     ? "确认要启用 【开机自动升级】 吗?"
     : "确认要关闭 【开机自动升级】 吗?";
 
@@ -276,7 +278,7 @@ function switchAutoUpgrade(event, action) {
   })
   .then(d => {
     if (d.ok) {
-      alert(`【开机自动升级】\n\n${action === "enable" ? "✅ 已启用" : "✅ 已禁用"}`);
+      alert(`【开机自动升级】\n\n${enable ? "✅ 已启用" : "✅ 已禁用"}`);
     } else {
       alert("❌ 操作失败");
     }

+ 5 - 1
web-ui.service/web_handler.go

@@ -445,7 +445,11 @@ func serviceStatus(name string) (SystemdServiceStatus, error) {
 	return st, nil
 }
 
-func systemctl(args ...string) error {
+func systemctl(action, name string) error {
+	var args []string
+	args = strings.Fields(action)
+	args = append(args, name)
+
 	out, err := exec.Command("systemctl", args...).CombinedOutput()
 	if err != nil {
 		return fmt.Errorf("systemctl %v failed: %v %s", args, err, string(out))