diff --git a/barnard.go b/barnard.go index f794710..43688db 100644 --- a/barnard.go +++ b/barnard.go @@ -97,6 +97,14 @@ type Barnard struct { // reconnect replaces them. It is intentionally idempotent for repeated // disconnect notifications. func (b *Barnard) cleanupConnectionAudio() { + // Connection audio operations that use both resources take FileStreamMutex + // before connectionMutex, so cleanup follows that order as well. + b.FileStreamMutex.Lock() + if b.FileStream != nil { + _ = b.FileStream.Stop() + b.FileStream = nil + } + b.FileStreamMutex.Unlock() b.connectionMutex.Lock() if b.Stream != nil { stream := b.Stream @@ -104,12 +112,6 @@ func (b *Barnard) cleanupConnectionAudio() { stream.Destroy() } b.connectionMutex.Unlock() - b.FileStreamMutex.Lock() - if b.FileStream != nil { - _ = b.FileStream.Stop() - b.FileStream = nil - } - b.FileStreamMutex.Unlock() } func (b *Barnard) cleanupToneTestAudio() { @@ -266,9 +268,13 @@ func (b *Barnard) TreeItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm for _, u := range users { // Explicitly set user mute state to match channel state if channelWillBeMuted && !u.LocallyMuted() { - b.UserConfig.ToggleMute(u) + if err := b.UserConfig.ToggleMute(u); err != nil { + b.AddOutputLine("Mute: could not save setting: " + err.Error()) + } } else if !channelWillBeMuted && u.LocallyMuted() { - b.UserConfig.ToggleMute(u) + if err := b.UserConfig.ToggleMute(u); err != nil { + b.AddOutputLine("Mute: could not save setting: " + err.Error()) + } } b.updateUserGain(u) @@ -297,7 +303,9 @@ func (b *Barnard) TreeItemKeyPress(ui *uiterm.Ui, tree *uiterm.Tree, item uiterm if treeItem.User != nil { if key == *b.Hotkeys.MuteToggle { // Toggle mute for single user - b.UserConfig.ToggleMute(treeItem.User) + if err := b.UserConfig.ToggleMute(treeItem.User); err != nil { + b.AddOutputLine("Mute: could not save setting: " + err.Error()) + } b.updateUserGain(treeItem.User) b.RebuildUserChannelTreePreservingSelection() b.Ui.Refresh() diff --git a/client.go b/client.go index 4b7696a..9c3a6ec 100644 --- a/client.go +++ b/client.go @@ -47,7 +47,7 @@ func (b *Barnard) exitWithError(err error) { func (b *Barnard) connect(reconnect bool) bool { var err error - _, err = gumble.DialWithDialer(new(net.Dialer), b.Config, &b.TLSConfig) + _, err = gumble.DialWithDialer(&net.Dialer{Timeout: 15 * time.Second}, b.Config, &b.TLSConfig) if err != nil { if reconnect { b.Log(err.Error()) @@ -295,11 +295,11 @@ func (b *Barnard) OnUserChange(e *gumble.UserChangeEvent) { if b.isChannelMuted(e.User.Channel.ID) { // Only mute if not already muted if !e.User.LocallyMuted() { - b.UserConfig.ToggleMute(e.User) - } - if b.Stream != nil { - b.Stream.UpdateUserGain(e.User) + if err := b.UserConfig.ToggleMute(e.User); err != nil { + b.AddOutputLine("Mute: could not save setting: " + err.Error()) + } } + b.updateUserGain(e.User) } } } diff --git a/gumble/gumble/client.go b/gumble/gumble/client.go index 464c2ca..b3064ed 100644 --- a/gumble/gumble/client.go +++ b/gumble/gumble/client.go @@ -116,10 +116,28 @@ func DialWithDialer(dialer *net.Dialer, config *Config, tlsConfig *tls.Config) ( } start := time.Now() - conn, err := tls.DialWithDialer(dialer, "tcp", config.Address, tlsConfig) + rawConn, err := dialer.Dial("tcp", config.Address) if err != nil { return nil, err } + conn := tls.Client(rawConn, tlsConfig) + // net.Dialer.Timeout covers only the TCP dial. Apply the same bounded + // deadline to TLS negotiation so a peer that accepts but never responds + // cannot block startup indefinitely. + if dialer.Timeout > 0 { + if err := conn.SetDeadline(start.Add(dialer.Timeout)); err != nil { + rawConn.Close() + return nil, err + } + } + if err := conn.Handshake(); err != nil { + rawConn.Close() + return nil, err + } + if err := conn.SetDeadline(time.Time{}); err != nil { + rawConn.Close() + return nil, err + } client := &Client{ Conn: NewConn(conn), diff --git a/recording_control.go b/recording_control.go index 6e20f16..614effc 100644 --- a/recording_control.go +++ b/recording_control.go @@ -6,6 +6,7 @@ import ( "time" "git.stormux.org/storm/barnard/gumble/gumble" + "git.stormux.org/storm/barnard/gumble/gumbleopenal" "git.stormux.org/storm/barnard/recording" "git.stormux.org/storm/barnard/uiterm" ) @@ -179,10 +180,10 @@ func (b *Barnard) finishRecordingStart() { } b.Recorder = recorder b.recordingStarting = false + // Recorder operations take RecordingMutex before connectionMutex. This + // prevents disconnect cleanup from destroying a stream during attachment. + b.withStream(func(stream *gumbleopenal.Stream) { stream.SetRecorder(recorder) }) b.RecordingMutex.Unlock() - if b.Stream != nil { - b.Stream.SetRecorder(recorder) - } b.AddOutputLine(fmt.Sprintf("Recording started: %s", recorder.Path())) b.Notify("recordstart", "me", recorder.Path()) b.renderGeneralStatus() @@ -205,9 +206,7 @@ func (b *Barnard) detachRecorder() (*recording.Recorder, string, bool) { } b.Recorder = nil b.recordingStarting = false - if b.Stream != nil { - b.Stream.SetRecorder(nil) - } + b.withStream(func(stream *gumbleopenal.Stream) { stream.SetRecorder(nil) }) return recorder, path, wasPending } diff --git a/ui.go b/ui.go index be7cbdb..9130ac1 100644 --- a/ui.go +++ b/ui.go @@ -129,7 +129,9 @@ func (b *Barnard) OnTimestampToggle(ui *uiterm.Ui, key uiterm.Key) { func (b *Barnard) OnNoiseSuppressionToggle(ui *uiterm.Ui, key uiterm.Key) { enabled := !b.UserConfig.GetNoiseSuppressionEnabled() - b.UserConfig.SetNoiseSuppressionEnabled(enabled) + if err := b.UserConfig.SetNoiseSuppressionEnabled(enabled); err != nil { + b.AddOutputLine("Noise suppression: could not save setting: " + err.Error()) + } b.NoiseSuppressor.SetEnabled(enabled) if enabled { @@ -198,7 +200,9 @@ func (b *Barnard) CommandMicDown(ui *uiterm.Ui, cmd string) { func (b *Barnard) CommandNoiseSuppressionToggle(ui *uiterm.Ui, cmd string) { enabled := !b.UserConfig.GetNoiseSuppressionEnabled() - b.UserConfig.SetNoiseSuppressionEnabled(enabled) + if err := b.UserConfig.SetNoiseSuppressionEnabled(enabled); err != nil { + b.AddOutputLine("Noise suppression: could not save setting: " + err.Error()) + } b.NoiseSuppressor.SetEnabled(enabled) if enabled { @@ -266,11 +270,18 @@ func (b *Barnard) CommandPlayFile(ui *uiterm.Ui, cmd string) { // Enable stereo encoder for file playback b.Client.EnableStereoEncoder() - // Auto-start transmission if not already transmitting - if !b.isTransmitting() && b.Stream != nil { - err := b.Stream.StartSource(b.UserConfig.GetInputDevice()) - if err != nil { - b.AddOutputLine(fmt.Sprintf("Error starting transmission: %s", err.Error())) + // Auto-start transmission if not already transmitting. FileStreamMutex is + // held here, before withStream's connection mutex, matching cleanup. + if !b.isTransmitting() { + var startErr error + started := b.withStream(func(stream *gumbleopenal.Stream) { + startErr = stream.StartSource(b.UserConfig.GetInputDevice()) + }) + if !started { + startErr = errors.New("audio unavailable while reconnecting") + } + if startErr != nil { + b.AddOutputLine(fmt.Sprintf("Error starting transmission: %s", startErr)) b.FileStream.Stop() b.Client.DisableStereoEncoder() return @@ -327,8 +338,8 @@ func (b *Barnard) setTransmit(ui *uiterm.Ui, val int) { close(b.toneTestStop) b.toneTestStop = nil } - } else if b.Stream != nil { - b.Stream.StopSource() + } else { + b.withStream(func(stream *gumbleopenal.Stream) { _ = stream.StopSource() }) } } else if !b.isConnected() { b.Notify("error", "me", "no tx while disconnected") @@ -347,20 +358,26 @@ func (b *Barnard) setTransmit(ui *uiterm.Ui, val int) { b.Notify("micup", "me", "") b.UpdateGeneralStatus(" Tx ", true) } else { - err := b.Stream.StartSource(b.UserConfig.GetInputDevice()) - if err != nil { - b.setTransmitting(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)) + started := b.withStream(func(stream *gumbleopenal.Stream) { + err := stream.StartSource(b.UserConfig.GetInputDevice()) + if err != nil { + b.setTransmitting(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) return } - b.Notify("error", "me", err.Error()) - b.UpdateGeneralStatus(err.Error(), true) - } else { b.Notify("micup", "me", "") b.UpdateGeneralStatus(" Tx ", true) + }) + if !started { + b.setTransmitting(false) + b.UpdateGeneralStatus("audio unavailable while reconnecting", true) } } } @@ -371,25 +388,29 @@ func fatalAudioOpenError(err error) bool { } func (b *Barnard) OnMicVolumeDown(ui *uiterm.Ui, key uiterm.Key) { - if b.ToneTest || b.Stream == nil { + if b.ToneTest { return } - b.Stream.SetMicVolume(-0.1, true) - b.UserConfig.SetMicVolume(b.Stream.GetMicVolume()) - if err := b.UserConfig.SaveConfig(); err != nil { - b.AddOutputLine("Microphone: could not save volume: " + err.Error()) - } + b.withStream(func(stream *gumbleopenal.Stream) { + stream.SetMicVolume(-0.1, true) + b.UserConfig.SetMicVolume(stream.GetMicVolume()) + if err := b.UserConfig.SaveConfig(); err != nil { + b.AddOutputLine("Microphone: could not save volume: " + err.Error()) + } + }) } func (b *Barnard) OnMicVolumeUp(ui *uiterm.Ui, key uiterm.Key) { - if b.ToneTest || b.Stream == nil { + if b.ToneTest { return } - b.Stream.SetMicVolume(0.1, true) - b.UserConfig.SetMicVolume(b.Stream.GetMicVolume()) - if err := b.UserConfig.SaveConfig(); err != nil { - b.AddOutputLine("Microphone: could not save volume: " + err.Error()) - } + b.withStream(func(stream *gumbleopenal.Stream) { + stream.SetMicVolume(0.1, true) + b.UserConfig.SetMicVolume(stream.GetMicVolume()) + if err := b.UserConfig.SaveConfig(); err != nil { + b.AddOutputLine("Microphone: could not save volume: " + err.Error()) + } + }) } func (b *Barnard) OnQuitPress(ui *uiterm.Ui, key uiterm.Key) { diff --git a/ui_tree.go b/ui_tree.go index af824fc..dea6f1f 100644 --- a/ui_tree.go +++ b/ui_tree.go @@ -3,6 +3,7 @@ package main import ( "fmt" "git.stormux.org/storm/barnard/gumble/gumble" + "git.stormux.org/storm/barnard/gumble/gumbleopenal" "git.stormux.org/storm/barnard/uiterm" "sort" ) @@ -37,44 +38,46 @@ func (ti TreeItem) TreeItemStyle(fg, bg uiterm.Attribute, active bool) (uiterm.A } func (b *Barnard) changeVolume(users []*gumble.User, change float32) { - for _, u := range users { - if b.Stream == nil { - continue + b.withStream(func(stream *gumbleopenal.Stream) { + for _, u := range users { + var boost uint16 + var ng float32 + curboost := float32((u.Boost() - 1)) / 10 + ng = u.Volume() + curboost + change + boost = uint16(1) + if ng > 1.0 { + perc := uint16((ng * 10)) - 10 + perc += 1 + boost = perc + ng = 1.0 + } + if ng < 0 { + ng = 0.0 + } + u.SetBoost(boost) + u.SetVolume(ng) + stream.UpdateUserGain(u) + b.UserConfig.UpdateConfig(u) } - var boost uint16 - var ng float32 - curboost := float32((u.Boost() - 1)) / 10 - ng = u.Volume() + curboost + change - boost = uint16(1) - if ng > 1.0 { - perc := uint16((ng * 10)) - 10 - perc += 1 - boost = perc - ng = 1.0 + if err := b.UserConfig.SaveConfig(); err != nil { + b.AddOutputLine("Volume: could not save setting: " + err.Error()) } - if ng < 0 { - ng = 0.0 - } - u.SetBoost(boost) - u.SetVolume(ng) - b.Stream.UpdateUserGain(u) - b.UserConfig.UpdateConfig(u) - } - b.UserConfig.SaveConfig() + }) } func (b *Barnard) resetVolume(users []*gumble.User) { - for _, u := range users { - if b.Stream == nil { - continue + b.withStream(func(stream *gumbleopenal.Stream) { + for _, u := range users { + // Reset to original volume (1.0) and boost (1) + u.SetBoost(uint16(1)) + u.SetVolume(1.0) + stream.UpdateUserGain(u) + b.UserConfig.UpdateConfig(u) } - // Reset to original volume (1.0) and boost (1) - u.SetBoost(uint16(1)) - u.SetVolume(1.0) - b.Stream.UpdateUserGain(u) - b.UserConfig.UpdateConfig(u) - } - b.UserConfig.SaveConfig() + if err := b.UserConfig.SaveConfig(); err != nil { + b.AddOutputLine("Volume: could not save setting: " + err.Error()) + } + }) } func makeUsersArray(users gumble.Users) []*gumble.User {