diff --git a/barnard.go b/barnard.go index b57d300..f794710 100644 --- a/barnard.go +++ b/barnard.go @@ -31,11 +31,12 @@ type Barnard struct { Address string TLSConfig tls.Config - Stream *gumbleopenal.Stream - Tx bool - AutoTransmit bool // auto-start transmission on connect - Connected bool - stateMutex sync.RWMutex + Stream *gumbleopenal.Stream + connectionMutex sync.RWMutex + Tx bool + AutoTransmit bool // auto-start transmission on connect + Connected bool + stateMutex sync.RWMutex Ui *uiterm.Ui UiOutput uiterm.Textview @@ -96,11 +97,13 @@ type Barnard struct { // reconnect replaces them. It is intentionally idempotent for repeated // disconnect notifications. func (b *Barnard) cleanupConnectionAudio() { + b.connectionMutex.Lock() if b.Stream != nil { stream := b.Stream b.Stream = nil stream.Destroy() } + b.connectionMutex.Unlock() b.FileStreamMutex.Lock() if b.FileStream != nil { _ = b.FileStream.Stop() @@ -121,9 +124,21 @@ func (b *Barnard) cleanupToneTestAudio() { } func (b *Barnard) updateUserGain(user *gumble.User) { - if b.Stream != nil { - b.Stream.UpdateUserGain(user) + b.withStream(func(stream *gumbleopenal.Stream) { + stream.UpdateUserGain(user) + }) +} + +// withStream keeps a connection-owned stream alive for the complete operation. +// Reconnect cleanup takes the write lock before destroying or replacing it. +func (b *Barnard) withStream(action func(*gumbleopenal.Stream)) bool { + b.connectionMutex.RLock() + defer b.connectionMutex.RUnlock() + if b.Stream == nil { + return false } + action(b.Stream) + return true } func (b *Barnard) isChannelMuted(channelID uint32) bool { @@ -212,8 +227,8 @@ func (b *Barnard) StopTransmission() { close(b.toneTestStop) b.toneTestStop = nil } - } else if b.Stream != nil { - b.Stream.StopSource() + } else { + b.withStream(func(stream *gumbleopenal.Stream) { _ = stream.StopSource() }) } } } diff --git a/client.go b/client.go index c50845c..4b7696a 100644 --- a/client.go +++ b/client.go @@ -87,11 +87,10 @@ func (b *Barnard) connect(reconnect bool) bool { b.exitWithError(err) return false } - b.Stream = stream - b.Stream.SetMicVolume(b.UserConfig.GetMicVolume(), false) - b.Stream.AttachStream(b.Client) - b.Stream.SetNoiseProcessor(b.NoiseSuppressor) - b.Stream.SetErrorFunc(func(err error) { + stream.SetMicVolume(b.UserConfig.GetMicVolume(), false) + stream.AttachStream(b.Client) + stream.SetNoiseProcessor(b.NoiseSuppressor) + stream.SetErrorFunc(func(err error) { if err != nil { b.AddOutputLine(fmt.Sprintf("Microphone: %s", err.Error())) } else { @@ -110,8 +109,11 @@ func (b *Barnard) connect(reconnect bool) bool { b.Client.DisableStereoEncoder() b.AddOutputLine(fmt.Sprintf("File playback: %s", err.Error())) }) - b.Stream.SetFilePlayer(b.FileStream) + stream.SetFilePlayer(b.FileStream) b.FileStreamMutex.Unlock() + b.connectionMutex.Lock() + b.Stream = stream + b.connectionMutex.Unlock() b.setConnected(true) // Dial delivers OnConnect before connect creates the OpenAL stream, so @@ -166,16 +168,21 @@ func (b *Barnard) OnConnect(e *gumble.ConnectEvent) { } func (b *Barnard) startAutoTransmit() { - if !b.AutoTransmit || b.Stream == nil || b.isTransmitting() { + if !b.AutoTransmit || b.isTransmitting() { return } - if err := b.Stream.StartSource(b.UserConfig.GetInputDevice()); err != nil { - b.AddOutputLine(fmt.Sprintf("auto-transmit failed: %s", err.Error())) + started := b.withStream(func(stream *gumbleopenal.Stream) { + if err := stream.StartSource(b.UserConfig.GetInputDevice()); err != nil { + b.AddOutputLine(fmt.Sprintf("auto-transmit failed: %s", err.Error())) + return + } + b.setTransmitting(true) + b.UpdateGeneralStatus(" AutoTx ", true) + b.AddOutputLine("Auto-transmit started") + }) + if !started { return } - b.setTransmitting(true) - b.UpdateGeneralStatus(" AutoTx ", true) - b.AddOutputLine("Auto-transmit started") } func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) { diff --git a/connection_resource_test.go b/connection_resource_test.go new file mode 100644 index 0000000..5415eae --- /dev/null +++ b/connection_resource_test.go @@ -0,0 +1,13 @@ +package main + +import ( + "testing" + + "git.stormux.org/storm/barnard/gumble/gumbleopenal" +) + +func TestWithStreamHandlesAbsentConnectionResource(t *testing.T) { + if (&Barnard{}).withStream(func(*gumbleopenal.Stream) {}) { + t.Fatal("nil connection stream was treated as available") + } +}