OrangePi3588Media/agent/internal/procctl/procctl.go
sladro 2a562514bc
Some checks are pending
CI / host-build (push) Waiting to run
CI / rk3588-cross-build (push) Waiting to run
修改agent功能,加入重启,准备测试
2026-01-10 11:06:39 +08:00

163 lines
4.0 KiB
Go

package procctl
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"rk3588sys/agent/internal/config"
"rk3588sys/agent/internal/files"
)
var ErrNotSupported = errors.New("not supported")
var ErrConflict = errors.New("conflict")
var ErrInvalidConfig = errors.New("invalid config")
var ErrConfigNotFound = errors.New("config not found")
type Status struct {
Running bool `json:"running"`
Pid int `json:"pid"`
ConfigPath string `json:"config_path"`
}
type pidFile struct {
Pid int `json:"pid"`
ConfigPath string `json:"config_path"`
StartedAtMS int64 `json:"started_at_ms"`
}
type Controller struct {
mu sync.Mutex
proc config.MediaServerProcessConfig
defCfg string
}
func New(agentCfg config.AgentConfig) *Controller {
return &Controller{
proc: agentCfg.MediaServerProcess,
defCfg: agentCfg.ConfigPath,
}
}
func (c *Controller) Enabled() bool { return c != nil && c.proc.Enable }
func (c *Controller) Start(configName string) (Status, error) {
c.mu.Lock()
defer c.mu.Unlock()
resolved, err := c.resolveConfigPath(configName)
if err != nil {
return Status{}, err
}
pf, _ := c.readPidFile()
if pf != nil {
alive, _ := isAlive(pf.Pid)
if alive {
if filepath.Clean(pf.ConfigPath) == filepath.Clean(resolved) {
return Status{Running: true, Pid: pf.Pid, ConfigPath: pf.ConfigPath}, nil
}
return Status{}, fmt.Errorf("%w: already running with config %s", ErrConflict, pf.ConfigPath)
}
_ = os.Remove(c.proc.PidFile)
}
pid, err := startProcess(c.proc.ExecPath, c.proc.WorkDir, resolved)
if err != nil {
return Status{}, err
}
pf2 := pidFile{Pid: pid, ConfigPath: resolved, StartedAtMS: time.Now().UnixMilli()}
b, _ := json.Marshal(pf2)
b = append(b, '\n')
if err := files.WriteFileAtomic(c.proc.PidFile, b, 0o644); err != nil {
return Status{}, fmt.Errorf("write pid file: %w", err)
}
return Status{Running: true, Pid: pid, ConfigPath: resolved}, nil
}
func (c *Controller) Stop() (Status, error) {
c.mu.Lock()
defer c.mu.Unlock()
pf, err := c.readPidFile()
if err != nil {
return Status{}, err
}
if pf == nil {
return Status{Running: false}, nil
}
alive, _ := isAlive(pf.Pid)
if !alive {
_ = os.Remove(c.proc.PidFile)
return Status{Running: false, Pid: pf.Pid, ConfigPath: pf.ConfigPath}, nil
}
if err := stopProcess(pf.Pid, time.Duration(c.proc.GracefulTimeoutMS)*time.Millisecond); err != nil {
return Status{}, err
}
_ = os.Remove(c.proc.PidFile)
return Status{Running: false, Pid: pf.Pid, ConfigPath: pf.ConfigPath}, nil
}
func (c *Controller) Restart(configName string) (Status, error) {
_, _ = c.Stop()
return c.Start(configName)
}
func (c *Controller) resolveConfigPath(name string) (string, error) {
n := strings.TrimSpace(name)
if n == "" {
if strings.TrimSpace(c.defCfg) == "" {
return "", fmt.Errorf("%w: default config_path is empty", ErrInvalidConfig)
}
return c.defCfg, nil
}
if strings.Contains(n, "..") || strings.ContainsAny(n, "/\\") {
return "", fmt.Errorf("%w: contains invalid characters", ErrInvalidConfig)
}
if !strings.HasSuffix(n, ".json") {
n += ".json"
}
base := strings.TrimSpace(c.proc.ConfigsDir)
if base == "" {
return "", fmt.Errorf("%w: configs_dir is empty", ErrInvalidConfig)
}
p := filepath.Join(base, n)
st, err := os.Stat(p)
if err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("%w: %s", ErrConfigNotFound, p)
}
return "", fmt.Errorf("stat config: %w", err)
}
if st.IsDir() {
return "", fmt.Errorf("%w: is a directory", ErrInvalidConfig)
}
return p, nil
}
func (c *Controller) readPidFile() (*pidFile, error) {
b, err := os.ReadFile(c.proc.PidFile)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("read pid file: %w", err)
}
var pf pidFile
if err := json.Unmarshal(b, &pf); err != nil {
return nil, fmt.Errorf("parse pid file: %w", err)
}
if pf.Pid <= 0 {
return nil, fmt.Errorf("pid file invalid pid: %d", pf.Pid)
}
return &pf, nil
}