fix(wireplumber): reconnect when PipeWire/WirePlumber restarts

Previously the wireplumber module connected to PipeWire once in its
constructor and had no handling for the connection being lost. When
PipeWire or the wireplumber service restarted (or crashed), the module
went stale/blank and never recovered until Waybar itself was restarted.

Connect to the WpCore "disconnected" signal and, on disconnect, schedule
a bounded main-loop retry (Glib::signal_timeout) that tears down the now
invalid core/object-manager/mixer-api references and rebuilds the whole
connection from scratch, re-running the async API and object-manager
setup. Connection setup/teardown is factored into setupConnection() and
teardownConnection() so startup and reconnect share one code path.

The reconnect timer is cancelled in the destructor and the existing
isModuleAlive() registry guard still protects in-flight async callbacks,
so teardown during a pending reconnect stays safe.

Fixes #2882.
This commit is contained in:
Alex
2026-07-04 13:41:29 +02:00
parent d9f2a437d0
commit 0b88b4ef0c
2 changed files with 91 additions and 9 deletions
+8
View File
@@ -17,6 +17,11 @@ class Wireplumber : public ALabel {
auto update() -> void override;
private:
bool setupConnection();
void teardownConnection();
void scheduleReconnect();
bool onReconnectTimeout();
static void onCoreDisconnected(waybar::modules::Wireplumber* self);
void asyncLoadRequiredApiModules();
void prepare(waybar::modules::Wireplumber* self);
void activatePlugins();
@@ -66,6 +71,9 @@ class Wireplumber : public ALabel {
bool only_physical_;
bool resolved_physical_;
std::string form_factor_;
// Timer used to retry connecting to PipeWire after it goes away; disconnected in the destructor
// so a pending attempt can't outlive the module. See #2882.
sigc::connection reconnect_timer_;
};
} // namespace waybar::modules
+83 -9
View File
@@ -10,6 +10,11 @@ bool isValidNodeId(uint32_t id) { return id > 0 && id < G_MAXUINT32; }
std::list<waybar::modules::Wireplumber*> waybar::modules::Wireplumber::modules;
// Interval between reconnect attempts after PipeWire/WirePlumber goes away. Fixed (rather than
// growing) so the module recovers promptly whenever the service comes back, while still being
// a bounded, main-loop-friendly poll rather than a busy loop.
static constexpr unsigned kReconnectIntervalMs = 2000;
// Async load/activation callbacks (onDefaultNodesApiLoaded, onMixerApiLoaded, onPluginActivated)
// are handed a raw `self` pointer with no GCancellable, and WirePlumber has no way to withdraw an
// in-flight callback. If the module is destroyed before such a callback fires (e.g. an output/bar
@@ -46,21 +51,37 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val
waybar::modules::Wireplumber::modules.push_back(this);
wp_init(WP_INIT_PIPEWIRE);
wp_core_ = wp_core_new(nullptr, nullptr, nullptr);
apis_ = g_ptr_array_new_with_free_func(g_object_unref);
om_ = wp_object_manager_new();
type_ = g_strdup(config_["node-type"].isString() ? config_["node-type"].asString().c_str()
: "Audio/Sink");
only_physical_ = config_["only-physical"].isBool() ? config_["only-physical"].asBool() : false;
if (!setupConnection()) {
spdlog::error("[{}]: Could not connect to PipeWire: '{}'", name_, type_);
throw std::runtime_error("Could not connect to PipeWire\n");
}
}
// Creates a fresh WpCore/object manager, connects to PipeWire and kicks off async API loading.
// Used both at startup and when reconnecting after a PipeWire/WirePlumber restart, so all
// connection-scoped state is (re)built here. Returns false if the connection could not be
// initiated. See https://github.com/Alexays/Waybar/issues/2882.
bool waybar::modules::Wireplumber::setupConnection() {
wp_core_ = wp_core_new(nullptr, nullptr, nullptr);
apis_ = g_ptr_array_new_with_free_func(g_object_unref);
om_ = wp_object_manager_new();
pending_plugins_ = 0;
prepare(this);
// Recover when PipeWire/WirePlumber goes away (service restart, crash). The "disconnected"
// signal fires on the GTK main loop; from there we schedule a reconnect attempt.
g_signal_connect_swapped(wp_core_, "disconnected", (GCallback)onCoreDisconnected, this);
spdlog::debug("[{}]: connecting to pipewire: '{}'...", name_, type_);
if (wp_core_connect(wp_core_) == 0) {
spdlog::error("[{}]: Could not connect to PipeWire: '{}'", name_, type_);
throw std::runtime_error("Could not connect to PipeWire\n");
return false;
}
spdlog::debug("[{}]: {} connected!", name_, type_);
@@ -68,10 +89,13 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val
g_signal_connect_swapped(om_, "installed", (GCallback)onObjectManagerInstalled, this);
asyncLoadRequiredApiModules();
return true;
}
waybar::modules::Wireplumber::~Wireplumber() {
waybar::modules::Wireplumber::modules.remove(this);
// Disconnects signal handlers and releases all connection-scoped WirePlumber objects. Safe to call
// when already partially/fully torn down (every pointer is null-checked and cleared), so it doubles
// as the reconnect reset and the destructor's cleanup.
void waybar::modules::Wireplumber::teardownConnection() {
if (mixer_api_ != nullptr) {
g_signal_handlers_disconnect_by_data(mixer_api_, this);
}
@@ -81,12 +105,62 @@ waybar::modules::Wireplumber::~Wireplumber() {
if (om_ != nullptr) {
g_signal_handlers_disconnect_by_data(om_, this);
}
wp_core_disconnect(wp_core_);
if (wp_core_ != nullptr) {
g_signal_handlers_disconnect_by_data(wp_core_, this);
wp_core_disconnect(wp_core_);
}
g_clear_pointer(&apis_, g_ptr_array_unref);
g_clear_object(&om_);
g_clear_object(&wp_core_);
g_clear_object(&mixer_api_);
g_clear_object(&def_nodes_api_);
g_clear_object(&wp_core_);
// onObjectManagerInstalled re-populates these via out-params (which don't free the previous
// value), so clear them here to avoid leaking the old strings across a reconnect.
g_clear_pointer(&default_node_name_, g_free);
g_clear_pointer(&default_source_name_, g_free);
pending_plugins_ = 0;
}
// "disconnected" signal handler on wp_core_. Runs during the core's own signal emission, so it must
// not tear down the core here; it only schedules a reconnect, which performs the teardown/rebuild
// once control has returned to the main loop.
void waybar::modules::Wireplumber::onCoreDisconnected(waybar::modules::Wireplumber* self) {
if (!isModuleAlive(self)) {
return;
}
spdlog::warn("[{}]: PipeWire connection lost; will attempt to reconnect", self->name_);
self->scheduleReconnect();
}
void waybar::modules::Wireplumber::scheduleReconnect() {
if (reconnect_timer_.connected()) {
return; // a reconnect attempt is already pending
}
reconnect_timer_ = Glib::signal_timeout().connect(
sigc::mem_fun(*this, &Wireplumber::onReconnectTimeout), kReconnectIntervalMs);
}
// Runs on the GTK main loop. Rebuilds the connection from scratch; returns true to keep retrying at
// the fixed interval until PipeWire is back, or false to stop once reconnected (a future
// "disconnected" signal will re-arm the timer if needed).
bool waybar::modules::Wireplumber::onReconnectTimeout() {
teardownConnection();
spdlog::info("[{}]: attempting to reconnect to PipeWire...", name_);
if (setupConnection()) {
spdlog::info("[{}]: reconnected to PipeWire", name_);
return false;
}
teardownConnection();
spdlog::debug("[{}]: reconnect failed; retrying in {} ms", name_, kReconnectIntervalMs);
return true;
}
waybar::modules::Wireplumber::~Wireplumber() {
// Remove from the live-module registry first so any in-flight async callback bails out (#3974),
// then cancel a pending reconnect so onReconnectTimeout can't fire on a half-destroyed module.
waybar::modules::Wireplumber::modules.remove(this);
reconnect_timer_.disconnect();
teardownConnection();
g_free(default_node_name_);
g_free(default_source_name_);
g_free(type_);