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:
@@ -29,10 +29,17 @@ class AudioBackend {
|
|||||||
static void volumeModifyCb(pa_context*, int, void*);
|
static void volumeModifyCb(pa_context*, int, void*);
|
||||||
static void sourceVolumeModifyCb(pa_context*, int, void*);
|
static void sourceVolumeModifyCb(pa_context*, int, void*);
|
||||||
void connectContext();
|
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_threaded_mainloop* mainloop_;
|
||||||
pa_mainloop_api* mainloop_api_;
|
pa_mainloop_api* mainloop_api_;
|
||||||
pa_context* context_;
|
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_volume_;
|
||||||
pa_cvolume pa_source_volume_;
|
pa_cvolume pa_source_volume_;
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
void AudioBackend::contextStateCb(pa_context* c, void* data) {
|
||||||
auto* backend = static_cast<AudioBackend*>(data);
|
auto* backend = static_cast<AudioBackend*>(data);
|
||||||
switch (pa_context_get_state(c)) {
|
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.
|
// When pulseaudio server restarts, the connection is "failed". Try to reconnect.
|
||||||
// pa_threaded_mainloop_lock is already acquired in callback threads.
|
// pa_threaded_mainloop_lock is already acquired in callback threads.
|
||||||
// So there is no need to lock it again.
|
// 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) {
|
if (backend->context_ != nullptr) {
|
||||||
pa_context_disconnect(backend->context_);
|
pa_context_disconnect(backend->context_);
|
||||||
pa_context_unref(backend->context_);
|
pa_context_unref(backend->context_);
|
||||||
backend->context_ = nullptr;
|
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;
|
break;
|
||||||
case PA_CONTEXT_CONNECTING:
|
case PA_CONTEXT_CONNECTING:
|
||||||
|
|||||||
Reference in New Issue
Block a user