fix(audio_backend): release leaked pa_operation handles

Every pa_operation* returned by the PulseAudio context introspection,
subscribe and volume/mute calls was discarded without pa_operation_unref,
leaking one operation object per call. Over a long session the periodic
subscription events accumulate an unbounded number of these handles.

Capture each returned handle and unref it (guarded against NULL) at every
discard site. Callback behavior is unchanged; these calls already run under
the threaded-mainloop lock, where unref is safe.
This commit is contained in:
Alex
2026-07-05 10:27:28 +02:00
parent fc297a7df3
commit b527376985
+57 -36
View File
@@ -4,6 +4,7 @@
#include <pulse/def.h>
#include <pulse/error.h>
#include <pulse/introspect.h>
#include <pulse/operation.h>
#include <pulse/subscribe.h>
#include <pulse/volume.h>
#include <spdlog/spdlog.h>
@@ -104,16 +105,17 @@ void AudioBackend::contextStateCb(pa_context* c, void* data) {
}
break;
case PA_CONTEXT_READY:
pa_context_get_server_info(c, serverInfoCb, data);
if (auto* o = pa_context_get_server_info(c, serverInfoCb, data)) pa_operation_unref(o);
pa_context_set_subscribe_callback(c, subscribeCb, data);
pa_context_subscribe(c,
static_cast<enum pa_subscription_mask>(
static_cast<int>(PA_SUBSCRIPTION_MASK_SERVER) |
static_cast<int>(PA_SUBSCRIPTION_MASK_SINK) |
static_cast<int>(PA_SUBSCRIPTION_MASK_SINK_INPUT) |
static_cast<int>(PA_SUBSCRIPTION_MASK_SOURCE) |
static_cast<int>(PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT)),
nullptr, nullptr);
if (auto* o = pa_context_subscribe(c,
static_cast<enum pa_subscription_mask>(
static_cast<int>(PA_SUBSCRIPTION_MASK_SERVER) |
static_cast<int>(PA_SUBSCRIPTION_MASK_SINK) |
static_cast<int>(PA_SUBSCRIPTION_MASK_SINK_INPUT) |
static_cast<int>(PA_SUBSCRIPTION_MASK_SOURCE) |
static_cast<int>(PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT)),
nullptr, nullptr))
pa_operation_unref(o);
break;
case PA_CONTEXT_FAILED:
if (pa_context_errno(c) != PA_ERR_CONNECTIONREFUSED) {
@@ -164,15 +166,18 @@ void AudioBackend::subscribeCb(pa_context* context, pa_subscription_event_type_t
return;
}
if (facility == PA_SUBSCRIPTION_EVENT_SERVER) {
pa_context_get_server_info(context, serverInfoCb, data);
if (auto* o = pa_context_get_server_info(context, serverInfoCb, data)) pa_operation_unref(o);
} else if (facility == PA_SUBSCRIPTION_EVENT_SINK) {
pa_context_get_sink_info_by_index(context, idx, sinkInfoCb, data);
if (auto* o = pa_context_get_sink_info_by_index(context, idx, sinkInfoCb, data))
pa_operation_unref(o);
} else if (facility == PA_SUBSCRIPTION_EVENT_SINK_INPUT) {
pa_context_get_sink_info_list(context, sinkInfoCb, data);
if (auto* o = pa_context_get_sink_info_list(context, sinkInfoCb, data)) pa_operation_unref(o);
} else if (facility == PA_SUBSCRIPTION_EVENT_SOURCE) {
pa_context_get_source_info_by_index(context, idx, sourceInfoCb, data);
if (auto* o = pa_context_get_source_info_by_index(context, idx, sourceInfoCb, data))
pa_operation_unref(o);
} else if (facility == PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT) {
pa_context_get_source_info_list(context, sourceInfoCb, data);
if (auto* o = pa_context_get_source_info_list(context, sourceInfoCb, data))
pa_operation_unref(o);
}
}
@@ -184,7 +189,9 @@ void AudioBackend::volumeModifyCb(pa_context* c, int success, void* data) {
if (success != 0) {
if ((backend->context_ != nullptr) &&
pa_context_get_state(backend->context_) == PA_CONTEXT_READY) {
pa_context_get_sink_info_by_index(backend->context_, backend->sink_idx_, sinkInfoCb, data);
if (auto* o = pa_context_get_sink_info_by_index(backend->context_, backend->sink_idx_,
sinkInfoCb, data))
pa_operation_unref(o);
}
} else {
spdlog::debug("Volume modification failed");
@@ -196,8 +203,9 @@ void AudioBackend::sourceVolumeModifyCb(pa_context* c, int success, void* data)
if (success != 0) {
if ((backend->context_ != nullptr) &&
pa_context_get_state(backend->context_) == PA_CONTEXT_READY) {
pa_context_get_source_info_by_index(backend->context_, backend->source_idx_, sourceInfoCb,
data);
if (auto* o = pa_context_get_source_info_by_index(backend->context_, backend->source_idx_,
sourceInfoCb, data))
pa_operation_unref(o);
}
} else {
spdlog::debug("Source volume modification failed");
@@ -317,8 +325,8 @@ void AudioBackend::serverInfoCb(pa_context* context, const pa_server_info* i, vo
backend->default_sink_name = i->default_sink_name ? i->default_sink_name : "";
backend->default_source_name_ = i->default_source_name ? i->default_source_name : "";
pa_context_get_sink_info_list(context, sinkInfoCb, data);
pa_context_get_source_info_list(context, sourceInfoCb, data);
if (auto* o = pa_context_get_sink_info_list(context, sinkInfoCb, data)) pa_operation_unref(o);
if (auto* o = pa_context_get_source_info_list(context, sourceInfoCb, data)) pa_operation_unref(o);
}
uint16_t AudioBackend::getVolume(PulseaudioTarget target) const {
@@ -368,10 +376,13 @@ void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t m
// Apply the volume change
pa_threaded_mainloop_lock(mainloop_);
if (is_source) {
pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume, sourceVolumeModifyCb,
this);
if (auto* o = pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume,
sourceVolumeModifyCb, this))
pa_operation_unref(o);
} else {
pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this);
if (auto* o = pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume,
volumeModifyCb, this))
pa_operation_unref(o);
}
pa_threaded_mainloop_unlock(mainloop_);
}
@@ -412,10 +423,13 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma
// No need to continue with volume change if we had to create a new structure
pa_threaded_mainloop_lock(mainloop_);
if (is_source) {
pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume, sourceVolumeModifyCb,
this);
if (auto* o = pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume,
sourceVolumeModifyCb, this))
pa_operation_unref(o);
} else {
pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this);
if (auto* o = pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume,
volumeModifyCb, this))
pa_operation_unref(o);
}
pa_threaded_mainloop_unlock(mainloop_);
return;
@@ -458,10 +472,13 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma
// Apply the volume change
pa_threaded_mainloop_lock(mainloop_);
if (is_source) {
pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume, sourceVolumeModifyCb,
this);
if (auto* o = pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume,
sourceVolumeModifyCb, this))
pa_operation_unref(o);
} else {
pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this);
if (auto* o = pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume,
volumeModifyCb, this))
pa_operation_unref(o);
}
pa_threaded_mainloop_unlock(mainloop_);
}
@@ -478,8 +495,9 @@ void AudioBackend::toggleSinkMute() {
if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return;
muted_ = !muted_;
pa_threaded_mainloop_lock(mainloop_);
pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast<int>(muted_), nullptr,
nullptr);
if (auto* o = pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast<int>(muted_),
nullptr, nullptr))
pa_operation_unref(o);
pa_threaded_mainloop_unlock(mainloop_);
}
@@ -487,8 +505,9 @@ void AudioBackend::toggleSinkMute(bool mute) {
if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return;
muted_ = mute;
pa_threaded_mainloop_lock(mainloop_);
pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast<int>(muted_), nullptr,
nullptr);
if (auto* o = pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast<int>(muted_),
nullptr, nullptr))
pa_operation_unref(o);
pa_threaded_mainloop_unlock(mainloop_);
}
@@ -496,8 +515,9 @@ void AudioBackend::toggleSourceMute() {
if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return;
source_muted_ = !source_muted_;
pa_threaded_mainloop_lock(mainloop_);
pa_context_set_source_mute_by_index(context_, source_idx_, static_cast<int>(source_muted_),
nullptr, nullptr);
if (auto* o = pa_context_set_source_mute_by_index(
context_, source_idx_, static_cast<int>(source_muted_), nullptr, nullptr))
pa_operation_unref(o);
pa_threaded_mainloop_unlock(mainloop_);
}
@@ -505,8 +525,9 @@ void AudioBackend::toggleSourceMute(bool mute) {
if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return;
source_muted_ = mute;
pa_threaded_mainloop_lock(mainloop_);
pa_context_set_source_mute_by_index(context_, source_idx_, static_cast<int>(source_muted_),
nullptr, nullptr);
if (auto* o = pa_context_set_source_mute_by_index(
context_, source_idx_, static_cast<int>(source_muted_), nullptr, nullptr))
pa_operation_unref(o);
pa_threaded_mainloop_unlock(mainloop_);
}