Skip to content

Commit 3f73b0a

Browse files
committed
services/pipewire: add reconnect support
1 parent 8d19beb commit 3f73b0a

10 files changed

Lines changed: 274 additions & 29 deletions

File tree

changelog/next.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ set shell id.
1818
- Added the ability to override Quickshell.cacheDir with a custom path.
1919
- Added minimized, maximized, and fullscreen properties to FloatingWindow.
2020
- Added the ability to handle move and resize events to FloatingWindow.
21+
- Pipewire service now reconnects if pipewire dies or a protocol error occurs.
2122

2223
## Other Changes
2324

src/services/pipewire/connection.cpp

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,135 @@
11
#include "connection.hpp"
22

3+
#include <qdir.h>
4+
#include <qfilesystemwatcher.h>
5+
#include <qlogging.h>
6+
#include <qloggingcategory.h>
7+
#include <qnamespace.h>
38
#include <qobject.h>
9+
#include <qtenvironmentvariables.h>
10+
#include <unistd.h>
11+
12+
#include "../../core/logcat.hpp"
13+
#include "core.hpp"
414

515
namespace qs::service::pipewire {
616

17+
namespace {
18+
QS_LOGGING_CATEGORY(logConnection, "quickshell.service.pipewire.connection", QtWarningMsg);
19+
}
20+
721
PwConnection::PwConnection(QObject* parent): QObject(parent) {
22+
this->runtimeDir = PwConnection::resolveRuntimeDir();
23+
24+
QObject::connect(&this->core, &PwCore::fatalError, this, &PwConnection::queueFatalError);
25+
26+
if (!this->tryConnect(false)
27+
&& qEnvironmentVariableIntegerValue("QS_PIPEWIRE_IMMEDIATE_RECONNECT") == 1)
28+
{
29+
this->beginReconnect();
30+
}
31+
}
32+
33+
QString PwConnection::resolveRuntimeDir() {
34+
auto runtimeDir = qEnvironmentVariable("PIPEWIRE_RUNTIME_DIR");
35+
if (runtimeDir.isEmpty()) {
36+
runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR");
37+
}
38+
39+
if (runtimeDir.isEmpty()) {
40+
runtimeDir = QString("/run/user/%1").arg(getuid());
41+
}
42+
43+
return runtimeDir;
44+
}
45+
46+
void PwConnection::beginReconnect() {
847
if (this->core.isValid()) {
9-
this->registry.init(this->core);
48+
this->stopSocketWatcher();
49+
return;
50+
}
51+
52+
if (!qEnvironmentVariableIsEmpty("PIPEWIRE_REMOTE")) return;
53+
54+
if (this->runtimeDir.isEmpty()) {
55+
qCWarning(
56+
logConnection
57+
) << "Cannot watch runtime dir for pipewire reconnects: runtime dir is empty.";
58+
return;
1059
}
60+
61+
this->startSocketWatcher();
62+
this->tryConnect(true);
63+
}
64+
65+
bool PwConnection::tryConnect(bool retry) {
66+
if (this->core.isValid()) return true;
67+
68+
qCDebug(logConnection) << "Attempting reconnect...";
69+
if (!this->core.start(retry)) {
70+
return false;
71+
}
72+
73+
qCInfo(logConnection) << "Connection established";
74+
this->stopSocketWatcher();
75+
76+
this->registry.init(this->core);
77+
return true;
78+
}
79+
80+
void PwConnection::startSocketWatcher() {
81+
if (this->socketWatcher != nullptr) return;
82+
if (!qEnvironmentVariableIsEmpty("PIPEWIRE_REMOTE")) return;
83+
84+
auto dir = QDir(this->runtimeDir);
85+
if (!dir.exists()) {
86+
qCWarning(logConnection) << "Cannot wait for a new pipewire socket, runtime dir does not exist:"
87+
<< this->runtimeDir;
88+
return;
89+
}
90+
91+
this->socketWatcher = new QFileSystemWatcher(this);
92+
this->socketWatcher->addPath(this->runtimeDir);
93+
94+
QObject::connect(
95+
this->socketWatcher,
96+
&QFileSystemWatcher::directoryChanged,
97+
this,
98+
&PwConnection::onRuntimeDirChanged
99+
);
100+
}
101+
102+
void PwConnection::stopSocketWatcher() {
103+
if (this->socketWatcher == nullptr) return;
104+
105+
this->socketWatcher->deleteLater();
106+
this->socketWatcher = nullptr;
107+
}
108+
109+
void PwConnection::queueFatalError() {
110+
if (this->fatalErrorQueued) return;
111+
112+
this->fatalErrorQueued = true;
113+
QMetaObject::invokeMethod(this, &PwConnection::onFatalError, Qt::QueuedConnection);
114+
}
115+
116+
void PwConnection::onFatalError() {
117+
this->fatalErrorQueued = false;
118+
119+
this->defaults.reset();
120+
this->registry.reset();
121+
this->core.shutdown();
122+
123+
this->beginReconnect();
124+
}
125+
126+
void PwConnection::onRuntimeDirChanged(const QString& /*path*/) {
127+
if (this->core.isValid()) {
128+
this->stopSocketWatcher();
129+
return;
130+
}
131+
132+
this->tryConnect(true);
11133
}
12134

13135
PwConnection* PwConnection::instance() {

src/services/pipewire/connection.hpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
#pragma once
22

3+
#include <qstring.h>
4+
35
#include "core.hpp"
46
#include "defaults.hpp"
57
#include "registry.hpp"
68

9+
class QFileSystemWatcher;
10+
711
namespace qs::service::pipewire {
812

913
class PwConnection: public QObject {
@@ -18,6 +22,23 @@ class PwConnection: public QObject {
1822
static PwConnection* instance();
1923

2024
private:
25+
static QString resolveRuntimeDir();
26+
27+
void beginReconnect();
28+
bool tryConnect(bool retry);
29+
void startSocketWatcher();
30+
void stopSocketWatcher();
31+
32+
private slots:
33+
void queueFatalError();
34+
void onFatalError();
35+
void onRuntimeDirChanged(const QString& path);
36+
37+
private:
38+
QString runtimeDir;
39+
QFileSystemWatcher* socketWatcher = nullptr;
40+
bool fatalErrorQueued = false;
41+
2142
// init/destroy order is important. do not rearrange.
2243
PwCore core;
2344
};

src/services/pipewire/core.cpp

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const pw_core_events PwCore::EVENTS = {
2727
.info = nullptr,
2828
.done = &PwCore::onSync,
2929
.ping = nullptr,
30-
.error = nullptr,
30+
.error = &PwCore::onError,
3131
.remove_id = nullptr,
3232
.bound_id = nullptr,
3333
.add_mem = nullptr,
@@ -36,26 +36,46 @@ const pw_core_events PwCore::EVENTS = {
3636
};
3737

3838
PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read) {
39-
qCInfo(logLoop) << "Creating pipewire event loop.";
4039
pw_init(nullptr, nullptr);
40+
}
41+
42+
bool PwCore::start(bool retry) {
43+
if (this->core != nullptr) return true;
44+
45+
qCInfo(logLoop) << "Creating pipewire event loop.";
4146

4247
this->loop = pw_loop_new(nullptr);
4348
if (this->loop == nullptr) {
44-
qCCritical(logLoop) << "Failed to create pipewire event loop.";
45-
return;
49+
if (retry) {
50+
qCInfo(logLoop) << "Failed to create pipewire event loop.";
51+
} else {
52+
qCCritical(logLoop) << "Failed to create pipewire event loop.";
53+
}
54+
this->shutdown();
55+
return false;
4656
}
4757

4858
this->context = pw_context_new(this->loop, nullptr, 0);
4959
if (this->context == nullptr) {
50-
qCCritical(logLoop) << "Failed to create pipewire context.";
51-
return;
60+
if (retry) {
61+
qCInfo(logLoop) << "Failed to create pipewire context.";
62+
} else {
63+
qCCritical(logLoop) << "Failed to create pipewire context.";
64+
}
65+
this->shutdown();
66+
return false;
5267
}
5368

5469
qCInfo(logLoop) << "Connecting to pipewire server.";
5570
this->core = pw_context_connect(this->context, nullptr, 0);
5671
if (this->core == nullptr) {
57-
qCCritical(logLoop) << "Failed to connect pipewire context. Errno:" << errno;
58-
return;
72+
if (retry) {
73+
qCInfo(logLoop) << "Failed to connect pipewire context. Errno:" << errno;
74+
} else {
75+
qCCritical(logLoop) << "Failed to connect pipewire context. Errno:" << errno;
76+
}
77+
this->shutdown();
78+
return false;
5979
}
6080

6181
pw_core_add_listener(this->core, &this->listener.hook, &PwCore::EVENTS, this);
@@ -66,22 +86,34 @@ PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read
6686
this->notifier.setSocket(fd);
6787
QObject::connect(&this->notifier, &QSocketNotifier::activated, this, &PwCore::poll);
6888
this->notifier.setEnabled(true);
69-
}
7089

71-
PwCore::~PwCore() {
72-
qCInfo(logLoop) << "Destroying PwCore.";
90+
return true;
91+
}
7392

74-
if (this->loop != nullptr) {
75-
if (this->context != nullptr) {
76-
if (this->core != nullptr) {
77-
pw_core_disconnect(this->core);
78-
}
93+
void PwCore::shutdown() {
94+
if (this->core != nullptr) {
95+
this->listener.remove();
96+
pw_core_disconnect(this->core);
97+
this->core = nullptr;
98+
}
7999

80-
pw_context_destroy(this->context);
81-
}
100+
if (this->context != nullptr) {
101+
pw_context_destroy(this->context);
102+
this->context = nullptr;
103+
}
82104

105+
if (this->loop != nullptr) {
83106
pw_loop_destroy(this->loop);
107+
this->loop = nullptr;
84108
}
109+
110+
this->notifier.setEnabled(false);
111+
QObject::disconnect(&this->notifier, nullptr, this, nullptr);
112+
}
113+
114+
PwCore::~PwCore() {
115+
qCInfo(logLoop) << "Destroying PwCore.";
116+
this->shutdown();
85117
}
86118

87119
bool PwCore::isValid() const {
@@ -90,6 +122,7 @@ bool PwCore::isValid() const {
90122
}
91123

92124
void PwCore::poll() {
125+
if (this->loop == nullptr) return;
93126
qCDebug(logLoop) << "Pipewire event loop received new events, iterating.";
94127
// Spin pw event loop.
95128
pw_loop_iterate(this->loop, 0);
@@ -107,6 +140,18 @@ void PwCore::onSync(void* data, quint32 id, qint32 seq) {
107140
emit self->synced(id, seq);
108141
}
109142

143+
void PwCore::onError(void* data, quint32 id, qint32 /*seq*/, qint32 res, const char* message) {
144+
auto* self = static_cast<PwCore*>(data);
145+
146+
if (message != nullptr) {
147+
qCWarning(logLoop) << "Fatal pipewire error on object" << id << "with code" << res << message;
148+
} else {
149+
qCWarning(logLoop) << "Fatal pipewire error on object" << id << "with code" << res;
150+
}
151+
152+
emit self->fatalError();
153+
}
154+
110155
SpaHook::SpaHook() { // NOLINT
111156
spa_zero(this->hook);
112157
}

src/services/pipewire/core.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ class PwCore: public QObject {
3030
~PwCore() override;
3131
Q_DISABLE_COPY_MOVE(PwCore);
3232

33+
bool start(bool retry);
34+
void shutdown();
35+
3336
[[nodiscard]] bool isValid() const;
3437
[[nodiscard]] qint32 sync(quint32 id) const;
3538

@@ -40,6 +43,7 @@ class PwCore: public QObject {
4043
signals:
4144
void polled();
4245
void synced(quint32 id, qint32 seq);
46+
void fatalError();
4347

4448
private slots:
4549
void poll();
@@ -48,6 +52,7 @@ private slots:
4852
static const pw_core_events EVENTS;
4953

5054
static void onSync(void* data, quint32 id, qint32 seq);
55+
static void onError(void* data, quint32 id, qint32 seq, qint32 res, const char* message);
5156

5257
QSocketNotifier notifier;
5358
SpaHook listener;

src/services/pipewire/defaults.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,22 @@ PwDefaultTracker::PwDefaultTracker(PwRegistry* registry): registry(registry) {
3131
QObject::connect(registry, &PwRegistry::nodeAdded, this, &PwDefaultTracker::onNodeAdded);
3232
}
3333

34+
void PwDefaultTracker::reset() {
35+
if (auto* meta = this->defaultsMetadata.object()) {
36+
QObject::disconnect(meta, nullptr, this, nullptr);
37+
}
38+
39+
this->defaultsMetadata.setObject(nullptr);
40+
this->setDefaultSink(nullptr);
41+
this->setDefaultSinkName(QString());
42+
this->setDefaultSource(nullptr);
43+
this->setDefaultSourceName(QString());
44+
this->setDefaultConfiguredSink(nullptr);
45+
this->setDefaultConfiguredSinkName(QString());
46+
this->setDefaultConfiguredSource(nullptr);
47+
this->setDefaultConfiguredSourceName(QString());
48+
}
49+
3450
void PwDefaultTracker::onMetadataAdded(PwMetadata* metadata) {
3551
if (metadata->name() == "default") {
3652
qCDebug(logDefaults) << "Got new defaults metadata object" << metadata;

src/services/pipewire/defaults.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class PwDefaultTracker: public QObject {
1212

1313
public:
1414
explicit PwDefaultTracker(PwRegistry* registry);
15+
void reset();
1516

1617
[[nodiscard]] PwNode* defaultSink() const;
1718
[[nodiscard]] PwNode* defaultSource() const;

0 commit comments

Comments
 (0)