77#include < utility>
88
99#include < QFile>
10+ #include < QFileDevice>
11+ #include < QHash>
1012#include < QProcess>
13+ #include < QRegularExpression>
1114#include < QSocketNotifier>
1215#include < QTextStream>
16+ #include < Qt>
17+ #include < QtTypes>
1318#include < fcntl.h>
1419#include < qalgorithms.h>
1520#include < qlist.h>
1924#include < qtimer.h>
2025#include < qtmetamacros.h>
2126#ifdef __FreeBSD__
27+ #include < sys/event.h>
2228#include < sys/ioccom.h>
29+ #include < sys/queue.h>
2330#include < sys/socket.h>
2431#include < sys/sysctl.h>
2532#include < sys/types.h>
@@ -341,12 +348,30 @@ OSS::OSS(QObject* parent): QObject(parent), mRescanTimer(new QTimer(this)) {
341348 });
342349
343350 this ->connectToDevd ();
351+ this ->initializeJackStates ();
352+ // clang-format off
353+ #ifdef __FreeBSD__
354+ this ->setupJackDetection ();
355+ #endif
356+ // clang-format on
344357 } else {
345358 qCWarning (logOSS) << " OSS sound system is not available" ;
346359 }
347360}
348361
349362OSS::~OSS () {
363+ if (this ->mKqueueNotifier ) {
364+ this ->mKqueueNotifier ->setEnabled (false );
365+ }
366+
367+ if (this ->mKqueue >= 0 ) {
368+ close (this ->mKqueue );
369+ }
370+
371+ if (this ->mLogFileDescriptor >= 0 ) {
372+ close (this ->mLogFileDescriptor );
373+ }
374+
350375 if (this ->mDevdSocket >= 0 ) {
351376 close (this ->mDevdSocket );
352377 }
@@ -369,6 +394,7 @@ bool OSS::isAvailable() const { return this->mAvailable; }
369394void OSS::connectToDevd () {
370395 // clang-format off
371396 #ifdef __FreeBSD__
397+ // clang-format on
372398 // Try seqpacket first
373399 const char * pipePaths[] = {
374400 " /var/run/devd.seqpacket.pipe" ,
@@ -415,6 +441,7 @@ void OSS::connectToDevd() {
415441 }
416442
417443 qCWarning (logOSS) << " Failed to connect to a devd pipe—device monitoring disabled" ;
444+ // clang-format off
418445 #else
419446 qCInfo (logOSS) << " devd monitoring only available on FreeBSD" ;
420447 #endif
@@ -473,6 +500,265 @@ void OSS::handleDevdEvent() {
473500 }
474501}
475502
503+ #ifdef __FreeBSD__
504+ void OSS::handleKqueueEvent () {
505+ struct kevent event;
506+ struct timespec timeout = {.tv_sec = 0 , .tv_nsec = 0 };
507+
508+ auto nev = kevent (this ->mKqueue , nullptr , 0 , &event, 1 , &timeout);
509+
510+ if (nev > 0 && (event.fflags & (NOTE_WRITE | NOTE_EXTEND))) {
511+ this ->readNewLogLines ();
512+ }
513+ }
514+
515+ void OSS::setupJackDetection () {
516+ this ->mLogFileDescriptor = open (" /var/log/messages" , O_RDONLY);
517+ if (this ->mLogFileDescriptor < 0 ) {
518+ qCWarning (logOSS) << " Failed to open /var/log/messages for jack detection" ;
519+ return ;
520+ }
521+
522+ // Seek to end to only catch new messages
523+ this ->mLogFilePosition = lseek (this ->mLogFileDescriptor , 0 , SEEK_END); // NOLINT
524+
525+ this ->mKqueue = kqueue (); // NOLINT
526+ if (this ->mKqueue < 0 ) {
527+ qCWarning (logOSS) << " Failed to create kqueue for jack detection" ;
528+ close (this ->mLogFileDescriptor );
529+ this ->mLogFileDescriptor = -1 ;
530+ return ;
531+ }
532+
533+ struct kevent change;
534+ EV_SET (
535+ &change,
536+ this ->mLogFileDescriptor ,
537+ EVFILT_VNODE,
538+ EV_ADD | EV_ENABLE | EV_CLEAR,
539+ NOTE_WRITE | NOTE_EXTEND,
540+ 0 ,
541+ nullptr
542+ );
543+
544+ if (kevent (this ->mKqueue , &change, 1 , nullptr , 0 , nullptr ) < 0 ) {
545+ qCWarning (logOSS) << " Failed to register kqueue event:" << qt_error_string (errno);
546+ close (this ->mKqueue );
547+ close (this ->mLogFileDescriptor );
548+
549+ this ->mKqueue = -1 ;
550+ this ->mLogFileDescriptor = -1 ;
551+ return ;
552+ }
553+
554+ this ->mKqueueNotifier = new QSocketNotifier (this ->mKqueue , QSocketNotifier::Read, this );
555+ connect (this ->mKqueueNotifier , &QSocketNotifier::activated, this , &OSS::handleKqueueEvent);
556+ this ->mKqueueNotifier ->setEnabled (true );
557+
558+ qCInfo (logOSS) << " Jack detection monitoring enabled" ;
559+ }
560+ #endif
561+
562+ void OSS::identifyJackType (int nid, bool connected) {
563+ // Read sysctl to identify what this nid is
564+ const QString sysctlPath = QString (" dev.hdaa.0.nid%1" ).arg (nid);
565+
566+ QProcess process;
567+ process.start (" sysctl" , QStringList () << " -n" << sysctlPath);
568+ process.waitForFinished (100 );
569+
570+ const QString output = QString::fromUtf8 (process.readAllStandardOutput ()).trimmed ();
571+
572+ if (output.contains (" Headphones" , Qt::CaseInsensitive)) {
573+ qCInfo (logOSS) << " Headphones" << (connected ? " connected" : " disconnected" );
574+ emit this ->headphonesChanged (connected);
575+ } else if (output.contains (" Mic" , Qt::CaseInsensitive)) {
576+ qCInfo (logOSS) << " Microphone" << (connected ? " connected" : " disconnected" );
577+ emit this ->microphoneChanged (connected);
578+ }
579+ }
580+
581+ void OSS::syncJackProperties () {
582+ for (auto it = this ->mJackStates .constBegin (); it != this ->mJackStates .constEnd (); ++it) {
583+ const int nid = it.key ();
584+ const bool connected = it.value ();
585+
586+ // Identify what type of jack this is and update properties
587+ const QString sysctlPath = QString (" dev.hdaa.0.nid%1" ).arg (nid);
588+
589+ QProcess process;
590+ process.start (" sysctl" , QStringList () << " -n" << sysctlPath);
591+ process.waitForFinished (100 );
592+
593+ const QString output = QString::fromUtf8 (process.readAllStandardOutput ()).trimmed ();
594+
595+ if (output.contains (" Headphones" , Qt::CaseInsensitive)) {
596+ if (this ->mHeadphonesConnected != connected) {
597+ this ->mHeadphonesConnected = connected;
598+ qCInfo (logOSS) << " Initial headphone state:" << (connected ? " connected" : " disconnected" );
599+ emit this ->headphonesChanged (connected);
600+ }
601+ }
602+ }
603+ }
604+
605+ void OSS::initializeJackStates () {
606+ QProcess sysctl;
607+ sysctl.start (" sysctl" , QStringList () << " -a" );
608+ if (!sysctl.waitForFinished (500 )) {
609+ qCWarning (logOSS) << " sysctl timeout" ;
610+ return ;
611+ }
612+
613+ const QString output = QString::fromUtf8 (sysctl.readAllStandardOutput ());
614+ const QStringList lines = output.split (' \n ' );
615+
616+ for (const QString& line: lines) {
617+ // Look for jack pins
618+ if (line.contains (" dev.hdaa." ) && line.contains (" : pin:" ) && line.contains (" Jack" )) {
619+ const QRegularExpression regex (R"( dev\.hdaa\.\d+\.nid(\d+):\s+pin:\s+(.+))" );
620+ auto match = regex.match (line);
621+
622+ if (match.hasMatch ()) {
623+ const int nid = match.captured (1 ).toInt ();
624+ const QString pinDesc = match.captured (2 );
625+
626+ // Skip disabled or internal (Fixed/None) pins
627+ if (pinDesc.contains (" DISABLED" ) || pinDesc.contains (" (None)" )
628+ || pinDesc.contains (" (Fixed)" ))
629+ {
630+ continue ;
631+ }
632+
633+ qCDebug (logOSS) << " Found jack: nid=" << nid << " desc=" << pinDesc;
634+
635+ // Initialize as disconnected
636+ this ->mJackStates [nid] = false ;
637+
638+ if (pinDesc.contains (" Headphones" , Qt::CaseInsensitive)) {
639+ this ->mHeadphonesConnected = false ;
640+ qCInfo (logOSS) << " Found headphone jack at nid" << nid;
641+ }
642+ }
643+ }
644+ }
645+
646+ qCDebug (logOSS) << " Initialized" << this ->mJackStates .size () << " jacks" ;
647+
648+ this ->readRecentLogMessages ();
649+ this ->syncJackProperties ();
650+ }
651+
652+ void OSS::readNewLogLines () {
653+ QFile logFile;
654+ if (!logFile.open (
655+ this ->mLogFileDescriptor ,
656+ QIODevice::ReadOnly | QIODevice::Text,
657+ QFileDevice::DontCloseHandle
658+ ))
659+ {
660+ return ;
661+ }
662+
663+ logFile.seek (this ->mLogFilePosition );
664+
665+ QTextStream in (&logFile);
666+ while (!in.atEnd ()) {
667+ const QString line = in.readLine ();
668+ this ->parseLogLine (line);
669+ }
670+
671+ // Update position for the next read
672+ this ->mLogFilePosition = logFile.pos ();
673+ }
674+
675+ void OSS::parseLogLine (const QString& line) {
676+ if (!line.contains (" Pin sense:" )) {
677+ return ;
678+ }
679+
680+ // Extract nid number
681+ const QRegularExpression nidRegex (R"( nid=(\d+))" );
682+ auto nidMatch = nidRegex.match (line);
683+
684+ if (!nidMatch.hasMatch ()) {
685+ return ;
686+ }
687+
688+ const int nid = nidMatch.captured (1 ).toInt ();
689+
690+ bool connected = false ;
691+ if (line.contains (" (connected)" )) {
692+ connected = true ;
693+ } else if (line.contains (" (disconnected)" )) {
694+ connected = false ;
695+ } else {
696+ return ; // Unknown state
697+ }
698+
699+ if (this ->mJackStates .value (nid, !connected) != connected) {
700+ this ->mJackStates [nid] = connected;
701+
702+ qCInfo (logOSS) << " Jack state changed: nid=" << nid
703+ << (connected ? " connected" : " disconnected" );
704+
705+ emit this ->jackStateChanged (nid, connected);
706+ this ->identifyJackType (nid, connected);
707+ }
708+ }
709+
710+ void OSS::readRecentLogMessages () {
711+ QFile logFile (" /var/log/messages" );
712+ if (!logFile.open (QIODevice::ReadOnly | QIODevice::Text)) {
713+ qCWarning (logOSS) << " Cannot read /var/log/messages for initial state" ;
714+ return ;
715+ }
716+
717+ // Read last 8KB for recent events
718+ const qint64 size = logFile.size ();
719+ if (size > 8192 ) {
720+ logFile.seek (size - 8192 );
721+ }
722+
723+ QTextStream in (&logFile);
724+ const QString content = in.readAll ();
725+ logFile.close ();
726+
727+ const QStringList lines = content.split (' \n ' );
728+ QHash<int , bool > lastLoggedState;
729+
730+ for (const QString& line: lines) {
731+ if (!line.contains (" Pin sense:" )) {
732+ continue ;
733+ }
734+
735+ const QRegularExpression nidRegex (R"( nid=(\d+))" );
736+ auto nidMatch = nidRegex.match (line);
737+
738+ if (!nidMatch.hasMatch ()) {
739+ continue ;
740+ }
741+
742+ const int nid = nidMatch.captured (1 ).toInt ();
743+ const bool connected = line.contains (" (connected)" );
744+
745+ lastLoggedState[nid] = connected;
746+ }
747+
748+ for (auto it = lastLoggedState.constBegin (); it != lastLoggedState.constEnd (); ++it) {
749+ const int nid = it.key ();
750+ const bool connected = it.value ();
751+
752+ if (this ->mJackStates .contains (nid)) {
753+ this ->mJackStates [nid] = connected;
754+ }
755+ }
756+
757+ qCDebug (logOSS) << " After reading log, jack states:" << this ->mJackStates ;
758+ }
759+
760+ bool OSS::headphonesConnected () const { return this ->mHeadphonesConnected ; }
761+
476762void OSS::scanDevices () {
477763 if (this ->mDefaultDevice != nullptr ) {
478764 this ->mDefaultDevice = nullptr ;
0 commit comments