enable=false 时走 NewSystemCtlController,其 Status() 在服务未运行时 只返回 Running:false 无原因。现通过 systemctl show 获取 ActiveState/SubState/Result/ExecMainStatus/NRestarts, 给出可读提示: '启动失败(退出码 X, 已重启 N 次)' 或 '未运行(systemctl: x/y)'
165 lines
4.9 KiB
Go
165 lines
4.9 KiB
Go
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
|
||
}
|