fix(audio_backend): never throw across the PulseAudio callback boundary

connectContext() throws std::runtime_error when pa_context_connect() fails.
It was called directly from contextStateCb (the libpulse mainloop thread,
running pure-C callback frames) on the PA_CONTEXT_FAILED reconnect path, so on
a pipewire/pulse restart the exception unwound across the C callback boundary
and triggered std::terminate/SIGABRT.

Add reconnectContext() noexcept which wraps connectContext() and logs failures
instead of throwing, and use it from the callback. Guard against the
FAILED -> connect -> FAILED recursion/busy loop with a reentrancy flag. The
constructor-time connectContext() still throws as before.

Fixes #5141.
This commit is contained in:
Alex
2026-07-04 03:14:13 +02:00
parent d3bfec13cc
commit 3831524ba8
2 changed files with 41 additions and 1 deletions
+7
View File
@@ -29,10 +29,17 @@ class AudioBackend {
static void volumeModifyCb(pa_context*, int, void*);
static void sourceVolumeModifyCb(pa_context*, int, void*);
void connectContext();
// Non-throwing reconnect used from the PulseAudio callback thread. Throwing
// across the libpulse C callback boundary calls std::terminate, so this
// swallows any failure and reports it via the return value instead.
bool reconnectContext() noexcept;
pa_threaded_mainloop* mainloop_;
pa_mainloop_api* mainloop_api_;
pa_context* context_;
// Guards against the FAILED -> connect -> FAILED recursion / busy loop when a
// reconnect attempt fails synchronously inside pa_context_connect().
bool reconnecting_{false};
pa_cvolume pa_volume_;
pa_cvolume pa_source_volume_;
+34 -1
View File
@@ -74,6 +74,22 @@ void AudioBackend::connectContext() {
}
}
// Reconnect the context without ever throwing. This is safe to call from within
// a PulseAudio state callback (which runs in pure C libpulse frames), where an
// escaping C++ exception cannot be unwound and would abort the process.
bool AudioBackend::reconnectContext() noexcept {
try {
connectContext();
return true;
} catch (const std::exception& e) {
spdlog::error("PulseAudio reconnect failed: {}", e.what());
return false;
} catch (...) {
spdlog::error("PulseAudio reconnect failed: unknown error");
return false;
}
}
void AudioBackend::contextStateCb(pa_context* c, void* data) {
auto* backend = static_cast<AudioBackend*>(data);
switch (pa_context_get_state(c)) {
@@ -104,12 +120,29 @@ void AudioBackend::contextStateCb(pa_context* c, void* data) {
// When pulseaudio server restarts, the connection is "failed". Try to reconnect.
// pa_threaded_mainloop_lock is already acquired in callback threads.
// So there is no need to lock it again.
//
// Guard against re-entrancy: pa_context_connect() can fire this callback
// synchronously with PA_CONTEXT_FAILED again, which would otherwise
// recurse (FAILED -> connect -> FAILED -> ...) and busy-loop.
if (backend->reconnecting_) {
break;
}
if (backend->context_ != nullptr) {
pa_context_disconnect(backend->context_);
pa_context_unref(backend->context_);
backend->context_ = nullptr;
}
backend->connectContext();
backend->reconnecting_ = true;
// Never throw across the libpulse C callback boundary: a failed reconnect
// is logged and left for a later PA event to retry instead of aborting.
if (!backend->reconnectContext()) {
spdlog::warn("PulseAudio context reconnect failed; will retry on next event");
if (backend->context_ != nullptr) {
pa_context_unref(backend->context_);
backend->context_ = nullptr;
}
}
backend->reconnecting_ = false;
}
break;
case PA_CONTEXT_CONNECTING: