mirror of https://github.com/jetkvm/kvm.git
222 lines
5.2 KiB
Go
222 lines
5.2 KiB
Go
//go:build cgo
|
|
// +build cgo
|
|
|
|
package audio
|
|
|
|
import (
|
|
"context"
|
|
"os/exec"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/jetkvm/kvm/internal/logging"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
// BaseSupervisor provides common functionality for audio supervisors
|
|
type BaseSupervisor struct {
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
logger *zerolog.Logger
|
|
mutex sync.RWMutex
|
|
running int32
|
|
|
|
// Process management
|
|
cmd *exec.Cmd
|
|
processPID int
|
|
|
|
// Process monitoring
|
|
processMonitor *ProcessMonitor
|
|
|
|
// Exit tracking
|
|
lastExitCode int
|
|
lastExitTime time.Time
|
|
|
|
// Channel management
|
|
stopChan chan struct{}
|
|
processDone chan struct{}
|
|
stopChanClosed bool
|
|
processDoneClosed bool
|
|
}
|
|
|
|
// NewBaseSupervisor creates a new base supervisor
|
|
func NewBaseSupervisor(componentName string) *BaseSupervisor {
|
|
logger := logging.GetDefaultLogger().With().Str("component", componentName).Logger()
|
|
return &BaseSupervisor{
|
|
logger: &logger,
|
|
processMonitor: GetProcessMonitor(),
|
|
stopChan: make(chan struct{}),
|
|
processDone: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// IsRunning returns whether the supervisor is currently running
|
|
func (bs *BaseSupervisor) IsRunning() bool {
|
|
return atomic.LoadInt32(&bs.running) == 1
|
|
}
|
|
|
|
// GetProcessPID returns the current process PID
|
|
func (bs *BaseSupervisor) GetProcessPID() int {
|
|
bs.mutex.RLock()
|
|
defer bs.mutex.RUnlock()
|
|
return bs.processPID
|
|
}
|
|
|
|
// GetLastExitInfo returns the last exit code and time
|
|
func (bs *BaseSupervisor) GetLastExitInfo() (exitCode int, exitTime time.Time) {
|
|
bs.mutex.RLock()
|
|
defer bs.mutex.RUnlock()
|
|
return bs.lastExitCode, bs.lastExitTime
|
|
}
|
|
|
|
// logSupervisorStart logs supervisor start event
|
|
func (bs *BaseSupervisor) logSupervisorStart() {
|
|
bs.logger.Info().Msg("Supervisor starting")
|
|
}
|
|
|
|
// logSupervisorStop logs supervisor stop event
|
|
func (bs *BaseSupervisor) logSupervisorStop() {
|
|
bs.logger.Info().Msg("Supervisor stopping")
|
|
}
|
|
|
|
// createContext creates a new context for the supervisor
|
|
func (bs *BaseSupervisor) createContext() {
|
|
bs.ctx, bs.cancel = context.WithCancel(context.Background())
|
|
}
|
|
|
|
// cancelContext cancels the supervisor context
|
|
func (bs *BaseSupervisor) cancelContext() {
|
|
if bs.cancel != nil {
|
|
bs.cancel()
|
|
}
|
|
}
|
|
|
|
// initializeChannels recreates channels for a new supervision cycle
|
|
func (bs *BaseSupervisor) initializeChannels() {
|
|
bs.mutex.Lock()
|
|
defer bs.mutex.Unlock()
|
|
|
|
bs.stopChan = make(chan struct{})
|
|
bs.processDone = make(chan struct{})
|
|
bs.stopChanClosed = false
|
|
bs.processDoneClosed = false
|
|
}
|
|
|
|
// closeStopChan safely closes the stop channel
|
|
func (bs *BaseSupervisor) closeStopChan() {
|
|
bs.mutex.Lock()
|
|
defer bs.mutex.Unlock()
|
|
|
|
if !bs.stopChanClosed {
|
|
close(bs.stopChan)
|
|
bs.stopChanClosed = true
|
|
}
|
|
}
|
|
|
|
// closeProcessDone safely closes the process done channel
|
|
func (bs *BaseSupervisor) closeProcessDone() {
|
|
bs.mutex.Lock()
|
|
defer bs.mutex.Unlock()
|
|
|
|
if !bs.processDoneClosed {
|
|
close(bs.processDone)
|
|
bs.processDoneClosed = true
|
|
}
|
|
}
|
|
|
|
// terminateProcess gracefully terminates the current process with configurable timeout
|
|
func (bs *BaseSupervisor) terminateProcess(timeout time.Duration, processType string) {
|
|
bs.mutex.RLock()
|
|
cmd := bs.cmd
|
|
pid := bs.processPID
|
|
bs.mutex.RUnlock()
|
|
|
|
if cmd == nil || cmd.Process == nil {
|
|
return
|
|
}
|
|
|
|
bs.logger.Info().Int("pid", pid).Msgf("terminating %s process", processType)
|
|
|
|
// Send SIGTERM first
|
|
if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
|
|
bs.logger.Warn().Err(err).Int("pid", pid).Msgf("failed to send SIGTERM to %s process", processType)
|
|
}
|
|
|
|
// Wait for graceful shutdown
|
|
done := make(chan struct{})
|
|
go func() {
|
|
_ = cmd.Wait()
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
bs.logger.Info().Int("pid", pid).Msgf("%s process terminated gracefully", processType)
|
|
case <-time.After(timeout):
|
|
bs.logger.Warn().Int("pid", pid).Msg("process did not terminate gracefully, sending SIGKILL")
|
|
bs.forceKillProcess(processType)
|
|
}
|
|
}
|
|
|
|
// forceKillProcess forcefully kills the current process
|
|
func (bs *BaseSupervisor) forceKillProcess(processType string) {
|
|
bs.mutex.RLock()
|
|
cmd := bs.cmd
|
|
pid := bs.processPID
|
|
bs.mutex.RUnlock()
|
|
|
|
if cmd == nil || cmd.Process == nil {
|
|
return
|
|
}
|
|
|
|
bs.logger.Warn().Int("pid", pid).Msgf("force killing %s process", processType)
|
|
if err := cmd.Process.Kill(); err != nil {
|
|
bs.logger.Error().Err(err).Int("pid", pid).Msg("failed to kill process")
|
|
}
|
|
}
|
|
|
|
// waitForProcessExit waits for the current process to exit and logs the result
|
|
func (bs *BaseSupervisor) waitForProcessExit(processType string) {
|
|
bs.mutex.RLock()
|
|
cmd := bs.cmd
|
|
pid := bs.processPID
|
|
bs.mutex.RUnlock()
|
|
|
|
if cmd == nil {
|
|
return
|
|
}
|
|
|
|
// Wait for process to exit
|
|
err := cmd.Wait()
|
|
|
|
bs.mutex.Lock()
|
|
bs.lastExitTime = time.Now()
|
|
bs.processPID = 0
|
|
|
|
var exitCode int
|
|
if err != nil {
|
|
if exitError, ok := err.(*exec.ExitError); ok {
|
|
exitCode = exitError.ExitCode()
|
|
} else {
|
|
// Process was killed or other error
|
|
exitCode = -1
|
|
}
|
|
} else {
|
|
exitCode = 0
|
|
}
|
|
|
|
bs.lastExitCode = exitCode
|
|
bs.mutex.Unlock()
|
|
|
|
// Remove process from monitoring
|
|
bs.processMonitor.RemoveProcess(pid)
|
|
|
|
if exitCode != 0 {
|
|
bs.logger.Error().Int("pid", pid).Int("exit_code", exitCode).Msgf("%s process exited with error", processType)
|
|
} else {
|
|
bs.logger.Info().Int("pid", pid).Msgf("%s process exited gracefully", processType)
|
|
}
|
|
}
|