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 }