150 lines
3.6 KiB
Go
150 lines
3.6 KiB
Go
package media
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type ProcessState struct {
|
|
Phase string `json:"phase"`
|
|
PID int `json:"pid,omitempty"`
|
|
Owned bool `json:"owned"`
|
|
External bool `json:"external"`
|
|
Restarts int `json:"restarts"`
|
|
Detail string `json:"detail"`
|
|
LastChanged time.Time `json:"lastChanged"`
|
|
}
|
|
|
|
type Process interface {
|
|
Start(context.Context) error
|
|
Stop(context.Context) error
|
|
State() ProcessState
|
|
}
|
|
|
|
type Supervisor struct {
|
|
binary, config string
|
|
mu sync.Mutex
|
|
command *exec.Cmd
|
|
state ProcessState
|
|
}
|
|
|
|
func NewSupervisor(binary, config string) *Supervisor {
|
|
phase, detail := "stopped", "MediaMTX 尚未启动"
|
|
if binary == "" {
|
|
phase, detail = "not_configured", "未配置 MediaMTX 二进制;可连接外部已启动实例"
|
|
}
|
|
return &Supervisor{binary: binary, config: config, state: ProcessState{Phase: phase, Detail: detail, LastChanged: time.Now().UTC()}}
|
|
}
|
|
|
|
func (s *Supervisor) MarkExternal() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.state.Owned {
|
|
return
|
|
}
|
|
s.state.Phase, s.state.External, s.state.Detail, s.state.LastChanged = "running", true, "检测到外部 MediaMTX;孤儿安全闸禁止 Sense 停止该进程", time.Now().UTC()
|
|
}
|
|
|
|
func (s *Supervisor) Start(_ context.Context) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.state.Owned && s.command != nil {
|
|
return nil
|
|
}
|
|
if s.binary == "" {
|
|
return errors.New("SENSE_MEDIAMTX_BINARY is not configured")
|
|
}
|
|
args := []string{}
|
|
if s.config != "" {
|
|
args = append(args, s.config)
|
|
}
|
|
cmd := exec.Command(s.binary, args...)
|
|
if s.config != "" {
|
|
cmd.Dir = filepath.Dir(s.config)
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
s.state.Phase, s.state.Detail, s.state.LastChanged = "failed", "MediaMTX 进程启动失败", time.Now().UTC()
|
|
return err
|
|
}
|
|
if s.state.Phase == "failed" {
|
|
s.state.Restarts++
|
|
}
|
|
s.command = cmd
|
|
s.state.Phase, s.state.PID, s.state.Owned, s.state.External = "starting", cmd.Process.Pid, true, false
|
|
s.state.Detail, s.state.LastChanged = "等待 MediaMTX Control API 就绪", time.Now().UTC()
|
|
go s.wait(cmd)
|
|
return nil
|
|
}
|
|
|
|
func (s *Supervisor) MarkReady() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.state.Owned {
|
|
s.state.Phase, s.state.Detail, s.state.LastChanged = "running", "MediaMTX Control API 已就绪", time.Now().UTC()
|
|
}
|
|
}
|
|
|
|
func (s *Supervisor) MarkFailed(detail string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.state.Phase, s.state.Detail, s.state.LastChanged = "failed", detail, time.Now().UTC()
|
|
}
|
|
|
|
func (s *Supervisor) wait(cmd *exec.Cmd) {
|
|
err := cmd.Wait()
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.command != cmd {
|
|
return
|
|
}
|
|
s.command = nil
|
|
s.state.Phase, s.state.PID, s.state.Owned, s.state.External = "failed", 0, false, false
|
|
s.state.Detail = "MediaMTX 进程已退出"
|
|
if err == nil {
|
|
s.state.Phase, s.state.Detail = "stopped", "MediaMTX 进程已停止"
|
|
}
|
|
s.state.LastChanged = time.Now().UTC()
|
|
}
|
|
|
|
func (s *Supervisor) Stop(ctx context.Context) error {
|
|
s.mu.Lock()
|
|
cmd := s.command
|
|
owned := s.state.Owned
|
|
s.mu.Unlock()
|
|
if !owned || cmd == nil {
|
|
return nil
|
|
}
|
|
if err := cmd.Process.Signal(os.Interrupt); err != nil {
|
|
if killErr := cmd.Process.Kill(); killErr != nil {
|
|
return errors.Join(err, killErr)
|
|
}
|
|
}
|
|
ticker := time.NewTicker(50 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
_ = cmd.Process.Kill()
|
|
return ctx.Err()
|
|
case <-ticker.C:
|
|
s.mu.Lock()
|
|
finished := s.command != cmd
|
|
s.mu.Unlock()
|
|
if finished {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Supervisor) State() ProcessState {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.state
|
|
}
|