From 13fe9b0d98028361344b7422b1ebe238d1d29d02 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 6 Apr 2026 00:35:48 -0700 Subject: [PATCH 1/3] services/pipewire: avoid blanket disconnect for default nodes The same nodes can be both default and default configured nodes. When the default and default configured node are not changed in unison, a blanket disconnect will also disconnect the other's destroy handler, causing a crash if the other is accessed after the node is destroyed. --- changelog/next.md | 1 + src/services/pipewire/defaults.cpp | 50 +++++++++++++++++++++++++----- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index 86687eb..b430a27 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -70,6 +70,7 @@ set shell id. - Fixed JsonAdapter crashing and providing bad data on read when using JsonObject. - Fixed JsonAdapter sending unnecessary property changes for primitive values. - Fixed JsonAdapter serialization for lists. +- Fixed pipewire crashes after hotplugging devices and changing default outputs. ## Packaging Changes diff --git a/src/services/pipewire/defaults.cpp b/src/services/pipewire/defaults.cpp index 7a24a65..b9c8e35 100644 --- a/src/services/pipewire/defaults.cpp +++ b/src/services/pipewire/defaults.cpp @@ -214,13 +214,24 @@ void PwDefaultTracker::setDefaultSink(PwNode* node) { qCInfo(logDefaults) << "Default sink changed to" << node; if (this->mDefaultSink != nullptr) { - QObject::disconnect(this->mDefaultSink, nullptr, this, nullptr); + // Targeted disconnect is used because this can also be the default configured sink. + QObject::disconnect( + this->mDefaultSink, + &PwBindableObject::destroying, + this, + &PwDefaultTracker::onDefaultSinkDestroyed + ); } this->mDefaultSink = node; if (node != nullptr) { - QObject::connect(node, &QObject::destroyed, this, &PwDefaultTracker::onDefaultSinkDestroyed); + QObject::connect( + node, + &PwBindableObject::destroying, + this, + &PwDefaultTracker::onDefaultSinkDestroyed + ); } emit this->defaultSinkChanged(); @@ -244,13 +255,24 @@ void PwDefaultTracker::setDefaultSource(PwNode* node) { qCInfo(logDefaults) << "Default source changed to" << node; if (this->mDefaultSource != nullptr) { - QObject::disconnect(this->mDefaultSource, nullptr, this, nullptr); + // Targeted disconnect is used because this can also be the default configured source. + QObject::disconnect( + this->mDefaultSource, + &PwBindableObject::destroying, + this, + &PwDefaultTracker::onDefaultSourceDestroyed + ); } this->mDefaultSource = node; if (node != nullptr) { - QObject::connect(node, &QObject::destroyed, this, &PwDefaultTracker::onDefaultSourceDestroyed); + QObject::connect( + node, + &PwBindableObject::destroying, + this, + &PwDefaultTracker::onDefaultSourceDestroyed + ); } emit this->defaultSourceChanged(); @@ -274,7 +296,13 @@ void PwDefaultTracker::setDefaultConfiguredSink(PwNode* node) { qCInfo(logDefaults) << "Default configured sink changed to" << node; if (this->mDefaultConfiguredSink != nullptr) { - QObject::disconnect(this->mDefaultConfiguredSink, nullptr, this, nullptr); + // Targeted disconnect is used because this can also be the default sink. + QObject::disconnect( + this->mDefaultConfiguredSink, + &PwBindableObject::destroying, + this, + &PwDefaultTracker::onDefaultConfiguredSinkDestroyed + ); } this->mDefaultConfiguredSink = node; @@ -282,7 +310,7 @@ void PwDefaultTracker::setDefaultConfiguredSink(PwNode* node) { if (node != nullptr) { QObject::connect( node, - &QObject::destroyed, + &PwBindableObject::destroying, this, &PwDefaultTracker::onDefaultConfiguredSinkDestroyed ); @@ -309,7 +337,13 @@ void PwDefaultTracker::setDefaultConfiguredSource(PwNode* node) { qCInfo(logDefaults) << "Default configured source changed to" << node; if (this->mDefaultConfiguredSource != nullptr) { - QObject::disconnect(this->mDefaultConfiguredSource, nullptr, this, nullptr); + // Targeted disconnect is used because this can also be the default source. + QObject::disconnect( + this->mDefaultConfiguredSource, + &PwBindableObject::destroying, + this, + &PwDefaultTracker::onDefaultConfiguredSourceDestroyed + ); } this->mDefaultConfiguredSource = node; @@ -317,7 +351,7 @@ void PwDefaultTracker::setDefaultConfiguredSource(PwNode* node) { if (node != nullptr) { QObject::connect( node, - &QObject::destroyed, + &PwBindableObject::destroying, this, &PwDefaultTracker::onDefaultConfiguredSourceDestroyed ); From 5bf6a412b0b03ecc77aac08ad09bd3f52967f017 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 6 Apr 2026 00:43:02 -0700 Subject: [PATCH 2/3] core: correctly construct runtime path when XDG_RUNTIME_DIR missing Fixes a typo of % as $, which broke string substitution. --- src/core/paths.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 6555e54..d361e3d 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -64,7 +64,7 @@ QDir* QsPaths::baseRunDir() { if (this->baseRunState == DirState::Unknown) { auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR"); if (runtimeDir.isEmpty()) { - runtimeDir = QString("/run/user/$1").arg(getuid()); + runtimeDir = QString("/run/user/%1").arg(getuid()); qCInfo(logPaths) << "XDG_RUNTIME_DIR was not set, defaulting to" << runtimeDir; } From 7c5a6c4bd4be1f258aa47626cf5cde02215adad2 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 6 Apr 2026 00:45:26 -0700 Subject: [PATCH 3/3] core/log: crash if Quickshell's log filter is installed twice Crashes from recursion inside filterCategories through the old filter have been observed. Presumably this means the log filter is getting installed twice somehow. This should catch it. --- src/core/logging.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 415cf61..1b19fab 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -310,10 +310,15 @@ void LogManager::init( instance->rules->append(parser.rules()); } - qInstallMessageHandler(&LogManager::messageHandler); - instance->lastCategoryFilter = QLoggingCategory::installFilter(&LogManager::filterCategory); + if (instance->lastCategoryFilter == &LogManager::filterCategory) { + qCFatal(logLogging) << "Quickshell's log filter has been installed twice. This is a bug."; + instance->lastCategoryFilter = nullptr; + } + + qInstallMessageHandler(&LogManager::messageHandler); + qCDebug(logLogging) << "Creating offthread logger..."; auto* thread = new QThread(); instance->threadProxy.moveToThread(thread);