fix: prevent Play() deadlock when device is not open

Play() held mu then called SwitchDevice() which also tried to acquire mu,
causing a deadlock. Replace with direct call to openDeviceLocked() which
does not lock.

Add regression test that verifies Play() returns an error (not deadlock)
when called on an uninitialized mixer.
This commit is contained in:
NeilClack
2026-07-11 10:16:15 -05:00
parent 141613f571
commit 72910e7006
2 changed files with 43 additions and 5 deletions
+6 -5
View File
@@ -112,14 +112,15 @@ func Play(path string, loopForever bool, cfg *db.AppConfig) error {
defer mu.Unlock()
// Check if mixer has been initialized and that a device is open
if !inited || !deviceOpen {
if !deviceOpen {
SwitchDevice("") // Re-switch to the default device
Play(path, loopForever, cfg) // Re-run the function
}
if !inited {
logger.Error("audio mixer not initialized")
return errors.New("audio mixer not initialized")
}
if !deviceOpen {
if err := openDeviceLocked(""); err != nil {
return fmt.Errorf("failed to open audio device: %w", err)
}
}
if path == "" {
logger.Error("empty audio path")
return errors.New("empty audio path")
+37
View File
@@ -2,8 +2,45 @@ package mixer
import (
"testing"
"time"
"rhemawavetech.com/rhemawave/internal/db"
)
// TestPlayDoesNotDeadlockWhenUninitialized verifies that Play() returns an error
// (not a deadlock) when the mixer has not been initialized.
// The old code would deadlock because Play() held mu then called SwitchDevice()
// which also tried to acquire mu.
func TestPlayDoesNotDeadlockWhenUninitialized(t *testing.T) {
// Ensure mixer is in uninitialized state
mu.Lock()
wasInited := inited
inited = false
deviceOpen = false
mu.Unlock()
defer func() {
mu.Lock()
inited = wasInited
mu.Unlock()
}()
cfg := &db.AppConfig{AudioEnabled: true}
done := make(chan error, 1)
go func() {
done <- Play("nonexistent.wav", false, cfg)
}()
select {
case err := <-done:
if err == nil {
t.Fatal("Play() returned nil error on uninitialized mixer, want error")
}
case <-time.After(5 * time.Second):
t.Fatal("Play() deadlocked on uninitialized mixer (did not return within 5s)")
}
}
func TestParseSinkLine(t *testing.T) {
tests := []struct {
name string