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 }