diff --git a/client_notification_test.go b/client_notification_test.go index 59b3bf4..c39eba6 100644 --- a/client_notification_test.go +++ b/client_notification_test.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "io" "strings" "testing" @@ -8,10 +9,24 @@ import ( "unicode/utf8" "git.stormux.org/storm/barnard/gumble/gumble" + "git.stormux.org/storm/barnard/gumble/gumbleopenal" ) // Regression: HTML escaping left terminal control sequences in server text, // allowing ANSI/OSC sequences to alter the terminal that rendered it. +// Regression: a capture-device open error left the application alive with no +// usable microphone. These errors are fatal and use the post-TUI stderr path. +func TestFatalAudioOpenError(t *testing.T) { + for _, err := range []error{gumbleopenal.ErrMic, gumbleopenal.ErrInputDevice, gumbleopenal.ErrOutputDevice, fmt.Errorf("wrapped: %w", gumbleopenal.ErrMic)} { + if !fatalAudioOpenError(err) { + t.Fatalf("%v was not fatal", err) + } + } + if fatalAudioOpenError(gumbleopenal.ErrState) { + t.Fatal("state error should remain recoverable") + } +} + func TestEscRemovesTerminalControlSequences(t *testing.T) { got := esc("name\x1b]0;spoof\a\x7f\u202e") if got != "name]0;spoof" { diff --git a/ui.go b/ui.go index 9c9d3cc..972387d 100644 --- a/ui.go +++ b/ui.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "os" "strings" @@ -8,6 +9,7 @@ import ( "unicode" "git.stormux.org/storm/barnard/gumble/gumble" + "git.stormux.org/storm/barnard/gumble/gumbleopenal" "git.stormux.org/storm/barnard/uiterm" "github.com/kennygrant/sanitize" "github.com/nsf/termbox-go" @@ -328,6 +330,13 @@ func (b *Barnard) setTransmit(ui *uiterm.Ui, val int) { } else { err := b.Stream.StartSource(b.UserConfig.GetInputDevice()) if err != nil { + b.Tx = false + if fatalAudioOpenError(err) { + // A missing capture device cannot recover through normal + // transmission controls; exit so option 1 reports it on stderr. + b.exitWithError(fmt.Errorf("audio device initialization failed: %w", err)) + return + } b.Notify("error", "me", err.Error()) b.UpdateGeneralStatus(err.Error(), true) } else { @@ -338,6 +347,10 @@ func (b *Barnard) setTransmit(ui *uiterm.Ui, val int) { } } +func fatalAudioOpenError(err error) bool { + return errors.Is(err, gumbleopenal.ErrMic) || errors.Is(err, gumbleopenal.ErrInputDevice) || errors.Is(err, gumbleopenal.ErrOutputDevice) +} + func (b *Barnard) OnMicVolumeDown(ui *uiterm.Ui, key uiterm.Key) { if b.ToneTest || b.Stream == nil { return