safesight/edge/agent/internal/procctl/systemctl.go
tian aae5bc36de fix(agent): systemctl 模式 Status 补充失败原因(LastError)
enable=false 时走 NewSystemCtlController,其 Status() 在服务未运行时
只返回 Running:false 无原因。现通过 systemctl show 获取
ActiveState/SubState/Result/ExecMainStatus/NRestarts,
给出可读提示: '启动失败(退出码 X, 已重启 N 次)' 或 '未运行(systemctl: x/y)'
2026-08-02 12:10:44 +08:00

165 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package procctl
import (
"bytes"
"fmt"
"io"
"os/exec"
"strconv"
"strings"
"time"
)
// SystemCtlController 使用 systemctl 管理 Media Server
type SystemCtlController struct {
serviceName string
configPath string
}
func NewSystemCtlController(serviceName string, configPath string) *SystemCtlController {
return &SystemCtlController{
serviceName: serviceName,
configPath: configPath,
}
}
func (s *SystemCtlController) Enabled() bool { return s != nil }
func (s *SystemCtlController) Status() (Status, error) {
// 检查服务是否运行
cmd := exec.Command("systemctl", "is-active", "--quiet", s.serviceName)
err := cmd.Run()
if err != nil {
// 服务未运行:附带失败原因(退出码/重启次数),供 UI 展示
return Status{Running: false, LastError: s.failureHint()}, nil
}
// 获取 PID
pidCmd := exec.Command("systemctl", "show", "--property=MainPID", "--value", s.serviceName)
out, err := pidCmd.Output()
if err != nil {
return Status{Running: true}, nil // 运行但无法获取 PID
}
pid, _ := strconv.Atoi(strings.TrimSpace(string(out)))
if pid <= 0 {
return Status{Running: true}, nil
}
return Status{
Running: true,
Pid: pid,
ConfigPath: s.configPath,
}, nil
}
// failureHint 查询 systemctl 状态,给出服务未运行的可用原因(如启动失败/退出码)
func (s *SystemCtlController) failureHint() string {
out, err := exec.Command("systemctl", "show", "--property=ActiveState,SubState,Result,ExecMainStatus,NRestarts", s.serviceName).Output()
if err != nil {
return fmt.Sprintf("媒体服务未运行 (%s)", s.serviceName)
}
props := map[string]string{}
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
if idx := strings.IndexByte(line, '='); idx > 0 {
props[strings.TrimSpace(line[:idx])] = strings.TrimSpace(line[idx+1:])
}
}
switch {
case props["SubState"] == "failed" || props["Result"] == "exit-code":
code := props["ExecMainStatus"]
if code == "" {
code = "?"
}
restarts := props["NRestarts"]
if restarts == "" {
restarts = "0"
}
return fmt.Sprintf("媒体服务启动失败(退出码 %s已重启 %s 次)", code, restarts)
case props["ActiveState"] != "" || props["SubState"] != "":
return fmt.Sprintf("媒体服务未运行systemctl: %s/%s", props["ActiveState"], props["SubState"])
default:
return fmt.Sprintf("媒体服务未运行 (%s)", s.serviceName)
}
}
func (s *SystemCtlController) Version() (string, error) {
// 从 systemctl 获取 ExecStart 路径,再调用 --version
// Output format: { path=/path/to/binary ; argv[]=... }
binOut, err := exec.Command("systemctl", "show", "--property=ExecStart", "--value", s.serviceName).Output()
if err != nil {
return "", fmt.Errorf("get version failed: %w", err)
}
out := strings.TrimSpace(string(binOut))
// Parse path= from systemctl ExecStart format
const prefix = "path="
idx := strings.Index(out, prefix)
if idx < 0 {
return "", fmt.Errorf("get version failed: cannot parse ExecStart")
}
rest := out[idx+len(prefix):]
end := strings.IndexByte(rest, ' ')
if end < 0 {
end = strings.IndexByte(rest, ';')
}
if end < 0 {
end = len(rest)
}
binPath := rest[:end]
if binPath == "" {
return "", fmt.Errorf("get version failed: empty ExecStart")
}
cmd := exec.Command(binPath, "--version")
out2, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("get version failed: %w", err)
}
return strings.TrimSpace(string(out2)), nil
}
func (s *SystemCtlController) Start(configName string) (Status, error) {
// 使用 systemctl start
cmd := exec.Command("systemctl", "start", s.serviceName)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return Status{}, fmt.Errorf("start failed: %w, stderr: %s", err, stderr.String())
}
// 等待服务启动
time.Sleep(500 * time.Millisecond)
return s.Status()
}
func (s *SystemCtlController) Stop() (Status, error) {
cmd := exec.Command("systemctl", "stop", s.serviceName)
if err := cmd.Run(); err != nil {
return Status{}, fmt.Errorf("stop failed: %w", err)
}
return Status{Running: false}, nil
}
func (s *SystemCtlController) Restart(configName string) (Status, error) {
cmd := exec.Command("systemctl", "restart", s.serviceName)
if err := cmd.Run(); err != nil {
return Status{}, fmt.Errorf("restart failed: %w", err)
}
time.Sleep(500 * time.Millisecond)
return s.Status()
}
// 二进制更新相关功能在 systemctl 模式下不支持
func (s *SystemCtlController) BinaryInfo() (BinaryUpdateResult, error) {
return BinaryUpdateResult{}, ErrNotSupported
}
func (s *SystemCtlController) UpdateBinary(r io.Reader, contentLength int64, expectedSha256 string) (BinaryUpdateResult, error) {
return BinaryUpdateResult{}, ErrNotSupported
}
func (s *SystemCtlController) RollbackBinary(backupPath string) (BinaryUpdateResult, error) {
return BinaryUpdateResult{}, ErrNotSupported
}