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:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user