diff --git a/player/player.go b/player/player.go index 6f83f46..7445fcc 100644 --- a/player/player.go +++ b/player/player.go @@ -9,10 +9,9 @@ import ( // Player handles alarm sound playback by shelling out to system audio tools. type Player struct { - mu sync.Mutex - cmd *exec.Cmd - playing bool - stopCh chan struct{} + mu sync.Mutex + stopCh chan struct{} + doneCh chan struct{} } func New() *Player { @@ -61,104 +60,105 @@ func findPlayer() (string, func(string) []string) { // PlayLoop starts playing a sound file in a loop until Stop() is called. func (p *Player) PlayLoop(path string) { - p.mu.Lock() - defer p.mu.Unlock() + p.Stop() // kill any previous playback and wait for it to finish - p.stop() + p.mu.Lock() + p.stopCh = make(chan struct{}) + p.doneCh = make(chan struct{}) + stopCh := p.stopCh + doneCh := p.doneCh + p.mu.Unlock() resolved := resolveSound(path) if resolved == "" { - // No sound file — bell loop - p.stopCh = make(chan struct{}) - p.playing = true - go func() { - ticker := time.NewTicker(2 * time.Second) - defer ticker.Stop() - os.Stdout.WriteString("\a") - for { - select { - case <-p.stopCh: - return - case <-ticker.C: - os.Stdout.WriteString("\a") - } - } - }() + go p.bellLoop(stopCh, doneCh) return } name, argsFn := findPlayer() if name == "" { os.Stdout.WriteString("\a") + close(doneCh) return } - p.stopCh = make(chan struct{}) - p.playing = true - - go func() { - for { - select { - case <-p.stopCh: - return - default: - } - - cmd := exec.Command(name, argsFn(resolved)...) - cmd.Stdout = nil - cmd.Stderr = nil - - p.mu.Lock() - p.cmd = cmd - p.mu.Unlock() - - err := cmd.Run() - - p.mu.Lock() - p.cmd = nil - p.mu.Unlock() - - if err != nil { - // Check if we were told to stop - select { - case <-p.stopCh: - return - default: - } - } - - // Brief pause between loops to avoid hammering - select { - case <-p.stopCh: - return - case <-time.After(300 * time.Millisecond): - } - } - }() + go p.audioLoop(name, argsFn, resolved, stopCh, doneCh) } -// Stop stops any currently playing sound. +// audioLoop runs the audio player in a loop. The goroutine fully owns the +// process — no other goroutine touches cmd. Coordination is only via channels. +func (p *Player) audioLoop(name string, argsFn func(string) []string, path string, stopCh, doneCh chan struct{}) { + defer close(doneCh) + + for { + // Check if we should stop before starting a new process + select { + case <-stopCh: + return + default: + } + + cmd := exec.Command(name, argsFn(path)...) + cmd.Stdout = nil + cmd.Stderr = nil + + if err := cmd.Start(); err != nil { + return + } + + // Wait for either the process to finish or a stop signal + processDone := make(chan error, 1) + go func() { + processDone <- cmd.Wait() + }() + + select { + case <-stopCh: + // Kill the process and wait for it to exit + _ = cmd.Process.Kill() + <-processDone + return + case <-processDone: + // Process finished naturally, loop again after a brief pause + } + + // Pause between loops + select { + case <-stopCh: + return + case <-time.After(300 * time.Millisecond): + } + } +} + +func (p *Player) bellLoop(stopCh, doneCh chan struct{}) { + defer close(doneCh) + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + os.Stdout.WriteString("\a") + for { + select { + case <-stopCh: + return + case <-ticker.C: + os.Stdout.WriteString("\a") + } + } +} + +// Stop stops any currently playing sound and waits for the playback goroutine to exit. func (p *Player) Stop() { p.mu.Lock() - defer p.mu.Unlock() - p.stop() -} + stopCh := p.stopCh + doneCh := p.doneCh + p.stopCh = nil + p.doneCh = nil + p.mu.Unlock() -func (p *Player) stop() { - if p.stopCh != nil { - close(p.stopCh) - p.stopCh = nil + if stopCh != nil { + close(stopCh) } - if p.cmd != nil && p.cmd.Process != nil { - _ = p.cmd.Process.Kill() - _ = p.cmd.Wait() - p.cmd = nil + if doneCh != nil { + <-doneCh // wait for goroutine to actually finish } - p.playing = false -} - -func (p *Player) IsPlaying() bool { - p.mu.Lock() - defer p.mu.Unlock() - return p.playing } diff --git a/woke b/woke index dc2c1cb..a58f85e 100755 Binary files a/woke and b/woke differ