diff --git a/barnard.go b/barnard.go index 20665d3..1bb105f 100644 --- a/barnard.go +++ b/barnard.go @@ -83,6 +83,9 @@ type Barnard struct { adminBanList gumble.BanList adminUserList gumble.RegisteredUsers adminACL *gumble.ACL + + reconnectStop chan struct{} + reconnectStopOnce sync.Once } // cleanupConnectionAudio releases connection-owned audio resources before a @@ -174,6 +177,26 @@ func (b *Barnard) setConnected(connected bool) { b.stateMutex.Unlock() } +func (b *Barnard) stopReconnects() { + b.reconnectStopOnce.Do(func() { + if b.reconnectStop != nil { + close(b.reconnectStop) + } + }) +} + +func (b *Barnard) reconnectCanceled() bool { + if b.reconnectStop == nil { + return false + } + select { + case <-b.reconnectStop: + return true + default: + return false + } +} + func (b *Barnard) StopTransmission() { if b.isTransmitting() { b.Notify("micdown", "me", "") diff --git a/client.go b/client.go index 2e66860..c50845c 100644 --- a/client.go +++ b/client.go @@ -14,6 +14,7 @@ import ( ) func (b *Barnard) start() { + b.reconnectStop = make(chan struct{}) b.Config.Attach(gumbleutil.AutoBitrate) b.Config.Attach(b) b.Config.Address = b.Address @@ -218,12 +219,15 @@ func (b *Barnard) OnDisconnect(e *gumble.DisconnectEvent) { } func (b *Barnard) reconnectGoroutine() { - for { - res := b.connect(true) - if res == true { - break + for !b.reconnectCanceled() { + if b.connect(true) { + return + } + select { + case <-b.reconnectStop: + return + case <-time.After(15 * time.Second): } - time.Sleep(15 * time.Second) } } diff --git a/client_notification_test.go b/client_notification_test.go index f82f36e..7e223b9 100644 --- a/client_notification_test.go +++ b/client_notification_test.go @@ -193,6 +193,23 @@ func TestCleanupConnectionAudioIsIdempotent(t *testing.T) { // Tone test mode intentionally does not create an OpenAL stream. Tree // controls must therefore keep local mute state without trying to update one. +func TestReconnectCancellationIsSafeBeforeStartup(t *testing.T) { + b := &Barnard{} + b.stopReconnects() + if b.reconnectCanceled() { + t.Fatal("nil reconnect channel should not report cancellation") + } +} + +func TestReconnectCancellationStopsWaiters(t *testing.T) { + b := &Barnard{reconnectStop: make(chan struct{})} + b.stopReconnects() + if !b.reconnectCanceled() { + t.Fatal("expected reconnect cancellation") + } + b.stopReconnects() // repeated shutdown must not panic +} + func TestUpdateUserGainAllowsToneTestWithoutStream(t *testing.T) { (&Barnard{ToneTest: true}).updateUserGain(&gumble.User{}) } diff --git a/ui.go b/ui.go index 6c1e498..be7cbdb 100644 --- a/ui.go +++ b/ui.go @@ -393,12 +393,14 @@ func (b *Barnard) OnMicVolumeUp(ui *uiterm.Ui, key uiterm.Key) { } func (b *Barnard) OnQuitPress(ui *uiterm.Ui, key uiterm.Key) { + b.stopReconnects() b.StopRecordingIfActive(true) b.Client.Disconnect() b.Ui.Close() } func (b *Barnard) CommandExit(ui *uiterm.Ui, cmd string) { + b.stopReconnects() b.StopRecordingIfActive(true) b.Client.Disconnect() b.Ui.Close()