Skip to content

Commit c295b7c

Browse files
committed
Fix tray icon stuck on "listening" after Stop (deadlock) + bump 0.3.29
Engine.Stop() held e.mu while calling cap.Close(). Close -> device.Uninit() blocks until the in-flight audio callback returns, and that callback (onFrame) starts by taking e.mu — so Stop and the audio thread deadlocked against each other whenever a frame was in flight at click time. The UI goroutine froze inside Stop: running stayed true, the mic was never released (kept listening), and the icon never reset. Flip running/cap under the lock, release it, then tear the device down outside the lock so the in-flight onFrame can finish. Also force setProcessing(false) afterwards: stopping mid-speech leaves the VAD segmenter InSpeech, so the "processing" icon would otherwise never clear. 🤖 Generated with Claude Code
1 parent 83fc0ee commit c295b7c

2 files changed

Lines changed: 19 additions & 6 deletions

File tree

internal/engine/engine.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -269,17 +269,30 @@ func (e *Engine) Start() error {
269269
}
270270

271271
// Stop closes the microphone.
272+
//
273+
// We must NOT hold e.mu while calling cap.Close(): Close -> device.Uninit()
274+
// blocks until the in-flight audio callback returns, and that callback
275+
// (onFrame) starts by taking e.mu. Holding the lock here would deadlock the
276+
// two against each other — leaving running=true, the mic open, and the tray
277+
// icon stuck on "listening". So we flip the state under the lock, release it,
278+
// and only then tear the device down.
272279
func (e *Engine) Stop() {
273280
e.mu.Lock()
274-
defer e.mu.Unlock()
275281
if !e.running {
282+
e.mu.Unlock()
276283
return
277284
}
278-
if e.cap != nil {
279-
e.cap.Close()
280-
e.cap = nil
281-
}
285+
cap := e.cap
286+
e.cap = nil
282287
e.running = false
288+
e.mu.Unlock()
289+
290+
if cap != nil {
291+
cap.Close() // any in-flight onFrame can now take e.mu and finish
292+
}
293+
// We may have stopped mid-speech (segmenter left InSpeech), so the
294+
// "processing" icon would never be cleared on its own — force it back.
295+
e.setProcessing(false)
283296
}
284297

285298
// Running reports whether listening is active.

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import (
3636
// icons_other.go uses .png), because the Windows tray needs ICO format.
3737

3838
// version of goto, printed at the start of the log when the app opens.
39-
const version = "0.3.28"
39+
const version = "0.3.29"
4040

4141
// startPaused: show the tray without turning listening on (used by the login
4242
// autostart, so the mic does not go live by itself). Set by `--paused`.

0 commit comments

Comments
 (0)