Skip to content

Commit 5fa045d

Browse files
committed
service/oss: use devd for device monitoring
1 parent 68891ca commit 5fa045d

2 files changed

Lines changed: 158 additions & 16 deletions

File tree

src/services/oss/oss.cpp

Lines changed: 149 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,40 @@
11
#include "oss.hpp"
22
#include <algorithm>
3+
#include <array>
34
#include <cerrno>
45
#include <cstdlib>
56
#include <cstring>
67
#include <utility>
78

89
#include <QFile>
910
#include <QProcess>
11+
#include <QSocketNotifier>
1012
#include <QTextStream>
1113
#include <fcntl.h>
1214
#include <qalgorithms.h>
1315
#include <qlist.h>
1416
#include <qlogging.h>
17+
#include <qloggingcategory.h>
1518
#include <qobject.h>
1619
#include <qtimer.h>
1720
#include <qtmetamacros.h>
1821
#ifdef __FreeBSD__
1922
#include <sys/ioccom.h>
23+
#include <sys/socket.h>
2024
#include <sys/sysctl.h>
2125
#include <sys/types.h>
26+
#include <sys/un.h>
2227
#endif
2328
#include <sys/ioctl.h>
2429
#include <sys/soundcard.h>
2530
#include <unistd.h>
2631

32+
#include "../../core/logcat.hpp"
33+
34+
namespace {
35+
QS_LOGGING_CATEGORY(logPolkit, "quickshell.service.oss", QtWarningMsg);
36+
}
37+
2738
namespace qs::service::oss {
2839

2940
// OSSMixerControl Implementation
@@ -86,7 +97,7 @@ bool OSSMixerControl::updateValue() {
8697
auto fd = open(mixerPath.toUtf8().constData(), O_RDONLY);
8798

8899
if (fd < 0) {
89-
qWarning() << "Failed to open mixer device:" << mixerPath;
100+
qCWarning(logPolkit) << "Failed to open mixer device:" << mixerPath;
90101
return false;
91102
}
92103

@@ -126,7 +137,7 @@ bool OSSMixerControl::writeValue() {
126137
auto fd = open(mixerPath.toUtf8().constData(), O_WRONLY);
127138

128139
if (fd < 0) {
129-
qWarning() << "Failed to open mixer device for writing:" << mixerPath;
140+
qCWarning(logPolkit) << "Failed to open mixer device for writing:" << mixerPath;
130141
return false;
131142
}
132143

@@ -136,7 +147,7 @@ bool OSSMixerControl::writeValue() {
136147

137148
if (ioctl(fd, MIXER_WRITE(this->mMixerId), &value) < 0) { // NOLINT
138149
close(fd);
139-
qWarning() << "Failed to write mixer value for control:" << this->mName;
150+
qCWarning(logPolkit) << "Failed to write mixer value for control:" << this->mName;
140151
return false;
141152
}
142153

@@ -164,8 +175,8 @@ bool OSSSoundDevice::initialize() {
164175
auto fd = open(mixerPath.toUtf8().constData(), O_RDONLY);
165176

166177
if (fd < 0) {
167-
qWarning() << "OSS: Failed to open mixer device:" << mixerPath
168-
<< "error:" << qt_error_string(errno);
178+
qCWarning(logPolkit) << "Failed to open mixer device:" << mixerPath
179+
<< "error:" << qt_error_string(errno);
169180
return false;
170181
}
171182

@@ -174,7 +185,7 @@ bool OSSSoundDevice::initialize() {
174185
if (ioctl(fd, SOUND_MIXER_READ_DEVMASK, &devmask) >= 0) { // NOLINT
175186
this->loadControls();
176187
} else {
177-
qWarning() << "OSS: Failed to read devmask for" << mixerPath;
188+
qCWarning(logPolkit) << "Failed to read devmask for" << mixerPath;
178189
close(fd);
179190
return false;
180191
}
@@ -212,7 +223,7 @@ bool OSSSoundDevice::initialize() {
212223

213224
sndstat.close();
214225
} else {
215-
qWarning() << "OSS: Could not open /dev/sndstat";
226+
qCWarning(logPolkit) << "Could not open /dev/sndstat";
216227
}
217228

218229
close(fd);
@@ -287,17 +298,30 @@ QList<QObject*> OSSSoundDevice::controls() const {
287298
}
288299

289300
// OSS Implementation
290-
OSS::OSS(QObject* parent): QObject(parent), mPollTimer(new QTimer(this)) {
301+
OSS::OSS(QObject* parent): QObject(parent), mRescanTimer(new QTimer(this)) {
291302
if (QFile::exists("/dev/sndstat")) {
292303
this->mAvailable = true;
293304

294305
this->scanDevices();
295306
this->updateDefault();
296307

297-
connect(this->mPollTimer, &QTimer::timeout, this, &OSS::refresh);
298-
this->mPollTimer->start(5000);
308+
this->mRescanTimer->setSingleShot(true);
309+
this->mRescanTimer->setInterval(100); // debounce
310+
311+
connect(this->mRescanTimer, &QTimer::timeout, this, [this]() {
312+
this->scanDevices();
313+
this->updateDefault();
314+
});
315+
316+
this->connectToDevd();
299317
} else {
300-
qWarning() << "OSS sound system is not available";
318+
qCWarning(logPolkit) << "OSS sound system is not available";
319+
}
320+
}
321+
322+
OSS::~OSS() {
323+
if (this->mDevdSocket >= 0) {
324+
close(this->mDevdSocket);
301325
}
302326
}
303327

@@ -315,6 +339,113 @@ QList<QObject*> OSS::devices() const {
315339
OSSSoundDevice* OSS::defaultDevice() const { return this->mDefaultDevice; }
316340
bool OSS::isAvailable() const { return this->mAvailable; }
317341

342+
void OSS::connectToDevd() {
343+
// clang-format off
344+
#ifdef __FreeBSD__
345+
// Try seqpacket first
346+
const char* pipePaths[] = {
347+
"/var/run/devd.seqpacket.pipe",
348+
"/var/run/devd.pipe",
349+
};
350+
351+
for (const auto* path: pipePaths) {
352+
if (!QFile::exists(path)) {
353+
qCInfo(logPolkit) << "devd pipe" << path << "does not exist, trying next";
354+
continue;
355+
}
356+
357+
this->mDevdSocket = socket(PF_UNIX, SOCK_SEQPACKET, 0);
358+
if (this->mDevdSocket < 0) {
359+
this->mDevdSocket = socket(PF_UNIX, SOCK_STREAM, 0);
360+
}
361+
362+
if (this->mDevdSocket < 0) {
363+
qCWarning(logPolkit) << "Failed to create socket for devd:" << qt_error_string(errno);
364+
continue;
365+
}
366+
367+
struct sockaddr_un addr = {};
368+
addr.sun_family = AF_UNIX;
369+
strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
370+
371+
if (::connect(this->mDevdSocket, (struct sockaddr*) &addr, SUN_LEN(&addr)) == 0) {
372+
int flags = fcntl(this->mDevdSocket, F_GETFL, 0);
373+
fcntl(this->mDevdSocket, F_SETFL, flags | O_NONBLOCK);
374+
375+
this->mDevdNotifier = new QSocketNotifier(this->mDevdSocket, QSocketNotifier::Read, this);
376+
connect(this->mDevdNotifier, &QSocketNotifier::activated, this, &OSS::handleDevdEvent);
377+
378+
this->mDevdNotifier->setEnabled(true);
379+
qCInfo(logPolkit) << "Successfully connected to devd at" << path;
380+
381+
return;
382+
} else {
383+
qCWarning(logPolkit) << "Failed to connect to" << path << ":" << qt_error_string(errno);
384+
}
385+
386+
close(this->mDevdSocket);
387+
this->mDevdSocket = -1;
388+
}
389+
390+
qCWarning(logPolkit) << "Failed to connect to a devd pipe—device monitoring disabled";
391+
#else
392+
qCInfo(logPolkit) << "devd monitoring only available on FreeBSD";
393+
#endif
394+
// clang-format on
395+
(void) this;
396+
}
397+
398+
void OSS::handleDevdEvent() {
399+
std::array<char, 4096> buffer {};
400+
const ssize_t n = read(this->mDevdSocket, buffer.data(), buffer.size() - 1);
401+
402+
if (n <= 0) {
403+
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
404+
qCWarning(logPolkit) << "devd socket read error:" << qt_error_string(errno);
405+
}
406+
return;
407+
}
408+
409+
buffer[n] = '\0';
410+
411+
const std::array<const char*, 4> audioKeywords = {"dsp", "mixer", "pcm", "snd"};
412+
bool isAudioRelated = false;
413+
414+
for (const auto* keyword: audioKeywords) {
415+
if (strstr(buffer.data(), keyword) != nullptr) {
416+
isAudioRelated = true;
417+
break;
418+
}
419+
}
420+
421+
if (!isAudioRelated) {
422+
return; // Skip non-audio events
423+
}
424+
425+
const QString event = QString::fromUtf8(buffer.data());
426+
427+
bool needsRescan = false;
428+
429+
if (event.contains("system=DEVFS")
430+
&& (event.contains("cdev=dsp") || event.contains("cdev=mixer") || event.contains("cdev=snd")
431+
|| event.contains("cdev=pcm")))
432+
{
433+
434+
qCInfo(logPolkit) << "Audio device event detected, scheduling rescan";
435+
needsRescan = true;
436+
}
437+
438+
if (event.contains("hw.snd.default_unit")) {
439+
qCInfo(logPolkit) << "Default device changed, scheduling update";
440+
needsRescan = true;
441+
}
442+
443+
// Prevent multiple rapid rescans when devices are changing quickly
444+
if (needsRescan && !this->mRescanTimer->isActive()) {
445+
this->mRescanTimer->start();
446+
}
447+
}
448+
318449
void OSS::scanDevices() {
319450
if (this->mDefaultDevice != nullptr) {
320451
this->mDefaultDevice = nullptr;
@@ -332,8 +463,9 @@ void OSS::scanDevices() {
332463

333464
if (device->initialize()) {
334465
this->mDevices.append(device);
466+
qCInfo(logPolkit) << "Device added:" << device;
335467
} else {
336-
qWarning() << "OSS: Failed to initialize device:" << mixerPath;
468+
qCWarning(logPolkit) << "Failed to initialize device:" << device;
337469
delete device;
338470
}
339471
}
@@ -385,7 +517,7 @@ void OSS::updateDefault() {
385517

386518
sndstat.close();
387519
} else {
388-
qWarning() << "OSS: Failed to open /dev/sndstat for reading default device";
520+
qCWarning(logPolkit) << "Failed to read default device via /dev/sndstat";
389521
}
390522

391523
if (!newDefault && !this->mDevices.isEmpty()) {
@@ -406,20 +538,22 @@ bool OSS::setDefaultDevice(int deviceId) {
406538
#ifdef __FreeBSD__
407539
if (sysctlbyname("hw.snd.default_unit", nullptr, nullptr, &deviceId, size) == 0) {
408540
this->updateDefault();
541+
qCInfo(logPolkit) << "Set default device to" << deviceId;
409542
return true;
410543
}
411544
#else
412545
(void)this;
413546
(void)size;
414547

415548
if (!this->mAvailable) {
416-
qWarning() << "OSS is not available on this system";
549+
qCWarning(logPolkit) << "OSS is not available on this system";
417550
return false;
418551
}
419552
#endif
420553
// clang-format on
421554

422-
qWarning() << "Failed to set default device to" << deviceId << "error:" << qt_error_string(errno);
555+
qCWarning(logPolkit) << "Failed to set default device to" << deviceId
556+
<< "error:" << qt_error_string(errno);
423557

424558
return false;
425559
}

src/services/oss/oss.hpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include <QObject>
66
#include <QQmlEngine>
7+
#include <QSocketNotifier>
78
#include <QString>
89
#include <QTimer>
910
#include <QVector>
@@ -130,10 +131,12 @@ class OSSSoundDevice: public QObject {
130131

131132
///! OSS sound system integration (FreeBSD)
132133
/// Provides access to OSS sound devices and mixer controls.
134+
/// Uses devd for real-time device change notifications.
133135
///
134136
/// See @@OSSSoundDevice and @@OSSMixerControl for controlling individual devices.
135137
class OSS: public QObject {
136138
Q_OBJECT;
139+
Q_DISABLE_COPY_MOVE(OSS);
137140
/// All available sound devices
138141
Q_PROPERTY(QList<QObject*> devices READ devices NOTIFY devicesChanged);
139142
/// The current system default device
@@ -145,6 +148,7 @@ class OSS: public QObject {
145148

146149
public:
147150
explicit OSS(QObject* parent = nullptr);
151+
~OSS() override;
148152

149153
[[nodiscard]] QList<QObject*> devices() const;
150154
[[nodiscard]] OSSSoundDevice* defaultDevice() const;
@@ -162,11 +166,15 @@ class OSS: public QObject {
162166
private:
163167
void scanDevices();
164168
void updateDefault();
169+
void connectToDevd();
170+
void handleDevdEvent();
165171

166172
QVector<OSSSoundDevice*> mDevices;
167173
OSSSoundDevice* mDefaultDevice = nullptr;
168174
bool mAvailable = false;
169-
QTimer* mPollTimer;
175+
QTimer* mRescanTimer = nullptr;
176+
int mDevdSocket = -1;
177+
QSocketNotifier* mDevdNotifier = nullptr;
170178
};
171179

172180
} // namespace qs::service::oss

0 commit comments

Comments
 (0)