release the audio stream a reconnect replaces
connect assigned b.Stream without releasing the stream already there, and OnDisconnect started a reconnect loop unconditionally, so two connects could race to install a stream. The loser was simply overwritten. An overwritten stream is never destroyed, so it keeps its OpenAL device, its render thread, and its entry in the shared audio listener list, which only Destroy removes. It therefore stays subscribed for the life of the process and every later audio packet from every user is dispatched to it as well: a goroutine, a packet queue and a set of playback buffers per orphan, per user. The file playback stream and the tone test saver were replaced the same way. Release whatever is being replaced, and allow only one reconnect loop at a time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
da1c6bdc26
commit
77fad24560
@@ -91,6 +91,8 @@ type Barnard struct {
|
||||
|
||||
reconnectStop chan struct{}
|
||||
reconnectStopOnce sync.Once
|
||||
reconnectMutex sync.Mutex
|
||||
reconnecting bool
|
||||
}
|
||||
|
||||
// cleanupConnectionAudio releases connection-owned audio resources before a
|
||||
|
||||
@@ -104,6 +104,7 @@ func (b *Barnard) connect(reconnect bool) bool {
|
||||
|
||||
// Initialize file player
|
||||
b.FileStreamMutex.Lock()
|
||||
previousFile := b.FileStream
|
||||
b.FileStream = fileplayback.New(b.Client)
|
||||
b.FileStream.SetErrorFunc(func(err error) {
|
||||
// Disable stereo when file finishes or errors
|
||||
@@ -113,9 +114,25 @@ func (b *Barnard) connect(reconnect bool) bool {
|
||||
stream.SetFilePlayer(b.FileStream)
|
||||
b.FileStreamMutex.Unlock()
|
||||
b.connectionMutex.Lock()
|
||||
previousStream := b.Stream
|
||||
b.Stream = stream
|
||||
b.connectionMutex.Unlock()
|
||||
|
||||
// A disconnect that lands while the OpenAL devices are opening starts a
|
||||
// second reconnect, so two connects can race to install a stream. The one
|
||||
// that loses must be released here: an orphaned stream keeps its OpenAL
|
||||
// device, its render thread and — because only Destroy detaches it — its
|
||||
// entry in the shared audio listener list, so every later audio packet
|
||||
// from every user is dispatched to it as well, for the life of the
|
||||
// process. Release outside the locks, since Destroy waits on the
|
||||
// per-user audio goroutines.
|
||||
if previousFile != nil {
|
||||
_ = previousFile.Stop()
|
||||
}
|
||||
if previousStream != nil {
|
||||
previousStream.Destroy()
|
||||
}
|
||||
|
||||
b.setConnected(true)
|
||||
// Dial delivers OnConnect before connect creates the OpenAL stream, so
|
||||
// start auto-transmit here as well for initial connections and reconnects.
|
||||
@@ -223,7 +240,27 @@ func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) {
|
||||
b.UiTree.Rebuild()
|
||||
b.Ui.Refresh()
|
||||
})
|
||||
go b.reconnectGoroutine()
|
||||
b.startReconnect()
|
||||
}
|
||||
|
||||
// startReconnect launches the reconnect loop unless one is already running.
|
||||
// Disconnect notifications can arrive more than once for a connection, and
|
||||
// every extra loop is another connect racing to install its own audio stream.
|
||||
func (b *Barnard) startReconnect() {
|
||||
b.reconnectMutex.Lock()
|
||||
defer b.reconnectMutex.Unlock()
|
||||
if b.reconnecting {
|
||||
return
|
||||
}
|
||||
b.reconnecting = true
|
||||
go func() {
|
||||
defer func() {
|
||||
b.reconnectMutex.Lock()
|
||||
b.reconnecting = false
|
||||
b.reconnectMutex.Unlock()
|
||||
}()
|
||||
b.reconnectGoroutine()
|
||||
}()
|
||||
}
|
||||
|
||||
func (b *Barnard) reconnectGoroutine() {
|
||||
|
||||
Reference in New Issue
Block a user