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 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_;
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user