Improves Bluetooth deadlock resilience with a targeted fix based on spindump evidence from a live deadlock capture.
Root cause (refined)
v0.9.1 dispatched stopRunning() to a background thread to avoid blocking the main thread during Bluetooth teardown. This worked, but the leaked background thread still held a CMIO semaphore that coreaudiod was waiting on, creating a circular deadlock chain:
- coreaudiod IO thread stuck in
HALB_Guard::WaitFor()on dead Bluetooth device (96% of spindump samples) - BTAudioHALPlugin blocked by mutex held by stuck IO thread
- coreaudiod AudioDeviceManager blocked by CMIO semaphore owned by mic-warm
- mic-warm's CMIO thread blocked waiting for coreaudiod to complete teardown
Fixes
- CMIO pipeline teardown:
removeInput()/removeOutput()called on the main thread before dispatchingstopRunning()to background, releasing the semaphore coreaudiod blocks on. Measured teardown at 30-44ms with no deadlock (vs potentially hanging forever) - Stale delegate cleanup: old session's
setSampleBufferDelegate(nil, queue: nil)called during teardown to prevent stale callbacks from corrupting new session's sample count - Thread-safe logging: replaced
DateFormatterwithISO8601DateFormatter(thread-safe by design) to prevent crashes from concurrent logging across main and background threads
Docs
- README updated: describes both delay triggers (hardware sleep after ~60s idle AND Bluetooth device switching regardless of idle state), documents deadlock resilience approach
- Apple Feedback draft updated: includes precise spindump call stack evidence and kTCCServiceAudioCapture finding on macOS 26.2
Full Changelog: v0.9.1...v0.9.2