From 051950f220bb8a485fb699c890d9fafc55572d45 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 18 Nov 2025 01:41:39 +0200 Subject: [PATCH] Fix critical deadlock when switching audio sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: When switching audio sources (USB to HDMI or vice versa), the application would hang indefinitely. This was caused by a deadlock between Go and C layers: 1. Main thread calls SetAudioOutputSource() → stopOutputAudio() 2. stopOutputAudio() calls outputRelay.Stop() which waits for goroutine 3. Goroutine is blocked in ReadMessage() holding Go mutex 4. ReadMessage() calls blocking C function jetkvm_audio_read_encode() 5. C function is blocked reading from ALSA device 6. Disconnect() can't acquire Go mutex to clean up 7. Deadlock: Main thread waiting for goroutine, goroutine waiting for ALSA Solution: Release the Go mutex BEFORE calling blocking C functions in ReadMessage() and WriteMessage(). The C layer has its own pthread mutex protection and handles stop requests via atomic flags. This allows: - Disconnect() to acquire the mutex immediately - C layer to detect stop request and return quickly - Goroutines to exit cleanly - Audio source switching to work flawlessly Fixes: - internal/audio/cgo_source.go:ReadMessage() - Release mutex before C call - internal/audio/cgo_source.go:WriteMessage() - Release mutex before C call This fix eliminates the hang when switching between USB and HDMI audio sources. --- internal/audio/cgo_source.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/internal/audio/cgo_source.go b/internal/audio/cgo_source.go index 02cb259f..9cc07a4a 100644 --- a/internal/audio/cgo_source.go +++ b/internal/audio/cgo_source.go @@ -167,17 +167,21 @@ func (c *CgoSource) IsConnected() bool { } func (c *CgoSource) ReadMessage() (uint8, []byte, error) { + // Check connection status with mutex c.mu.Lock() - defer c.mu.Unlock() - if !c.connected { + c.mu.Unlock() return 0, nil, fmt.Errorf("not connected") } if c.direction != "output" { + c.mu.Unlock() return 0, nil, fmt.Errorf("ReadMessage only supported for output direction") } + c.mu.Unlock() + // Call C function without holding mutex to avoid deadlock + // The C layer has its own locking and handles stop requests opusSize := C.jetkvm_audio_read_encode(unsafe.Pointer(&c.opusBuf[0])) if opusSize < 0 { return 0, nil, fmt.Errorf("jetkvm_audio_read_encode failed: %d", opusSize) @@ -195,16 +199,18 @@ func (c *CgoSource) ReadMessage() (uint8, []byte, error) { } func (c *CgoSource) WriteMessage(msgType uint8, payload []byte) error { + // Check connection status and validate parameters with mutex c.mu.Lock() - defer c.mu.Unlock() - if !c.connected { + c.mu.Unlock() return fmt.Errorf("not connected") } if c.direction != "input" { + c.mu.Unlock() return fmt.Errorf("WriteMessage only supported for input direction") } + c.mu.Unlock() if msgType != ipcMsgTypeOpus { return nil @@ -218,6 +224,8 @@ func (c *CgoSource) WriteMessage(msgType uint8, payload []byte) error { return fmt.Errorf("opus packet too large: %d bytes (max 1500)", len(payload)) } + // Call C function without holding mutex to avoid deadlock + // The C layer has its own locking and handles stop requests rc := C.jetkvm_audio_decode_write(unsafe.Pointer(&payload[0]), C.int(len(payload))) if rc < 0 { return fmt.Errorf("jetkvm_audio_decode_write failed: %d", rc)