diff --git a/CMakeLists.txt b/CMakeLists.txt index cde8d62..6c6a8a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,11 @@ if (WAYLAND) list(APPEND QT_FPDEPS WaylandClient) endif() +if (DBUS) + list(APPEND QT_DEPS Qt6::DBus) + list(APPEND QT_FPDEPS DBus) +endif() + find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS}) qt_standard_project_setup(REQUIRES 6.6) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 247abf2..090fbc2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,6 +5,10 @@ install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) add_subdirectory(core) add_subdirectory(io) +if (DBUS) + add_subdirectory(dbus) +endif() + if (WAYLAND) add_subdirectory(wayland) endif () diff --git a/src/dbus/CMakeLists.txt b/src/dbus/CMakeLists.txt new file mode 100644 index 0000000..1a0d23e --- /dev/null +++ b/src/dbus/CMakeLists.txt @@ -0,0 +1,22 @@ +set_source_files_properties(org.freedesktop.DBus.Properties.xml PROPERTIES + CLASSNAME DBusPropertiesInterface + #INCLUDE dbus_properties_types.hpp +) + +qt_add_dbus_interface(DBUS_INTERFACES + org.freedesktop.DBus.Properties.xml + dbus_properties +) + +qt_add_library(quickshell-dbus STATIC + dbusutil.cpp + ${DBUS_INTERFACES} +) + +# dbus headers +target_include_directories(quickshell-dbus PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +target_link_libraries(quickshell-dbus PRIVATE ${QT_DEPS}) + +qs_pch(quickshell-dbus) +#qs_pch(quickshell-dbusplugin) diff --git a/src/dbus/dbusutil.cpp b/src/dbus/dbusutil.cpp new file mode 100644 index 0000000..a80d622 --- /dev/null +++ b/src/dbus/dbusutil.cpp @@ -0,0 +1,284 @@ +#include "dbusutil.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dbus_properties.h" + +Q_LOGGING_CATEGORY(logDbus, "quickshell.dbus", QtWarningMsg); + +namespace qs::dbus { + +QDBusError demarshallVariant(const QVariant& variant, const QMetaType& type, void* slot) { + const char* expectedSignature = "v"; + + if (type.id() != QMetaType::QVariant) { + expectedSignature = QDBusMetaType::typeToSignature(type); + if (expectedSignature == nullptr) { + qFatal() << "failed to demarshall unregistered dbus meta-type" << type << "with" << variant; + } + } + + if (variant.metaType() == type) { + if (type.id() == QMetaType::QVariant) { + *reinterpret_cast(slot) = variant; // NOLINT + } else { + type.destruct(slot); + type.construct(slot, variant.constData()); + } + } else if (variant.metaType() == QMetaType::fromType()) { + auto arg = qvariant_cast(variant); + auto signature = arg.currentSignature(); + + if (signature == expectedSignature) { + if (!QDBusMetaType::demarshall(arg, type, slot)) { + QString error; + QDebug(&error) << "failed to deserialize dbus value" << variant << "into" << type; + return QDBusError(QDBusError::InvalidArgs, error); + } + } + } else { + QString error; + QDebug(&error) << "failed to deserialize variant" << variant + << "which is not a primitive type or a dbus argument (what?)"; + return QDBusError(QDBusError::InvalidArgs, error); + } + + return QDBusError(); +} + +void asyncReadPropertyInternal( + const QMetaType& type, + QDBusAbstractInterface& interface, + const QString& property, + std::function)> callback // NOLINT +) { + if (type.id() != QMetaType::QVariant) { + const char* expectedSignature = QDBusMetaType::typeToSignature(type); + if (expectedSignature == nullptr) { + qFatal() << "qs::dbus::asyncReadPropertyInternal called with unregistered dbus meta-type" + << type; + } + } + + auto callMessage = QDBusMessage::createMethodCall( + interface.service(), + interface.path(), + "org.freedesktop.DBus.Properties", + "Get" + ); + + callMessage << interface.interface() << property; + auto pendingCall = interface.connection().asyncCall(callMessage); + + auto* call = new QDBusPendingCallWatcher(pendingCall, &interface); + + auto responseCallback = [type, callback](QDBusPendingCallWatcher* call) { + QDBusPendingReply reply = *call; + + callback([&](void* slot) { + if (reply.isError()) { + return reply.error(); + } else { + return demarshallVariant(reply.value().variant(), type, slot); + } + }); + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, &interface, responseCallback); +} + +void AbstractDBusProperty::tryUpdate(const QVariant& variant) { + auto error = this->read(variant); + if (error.isValid()) { + qCWarning(logDbus).noquote() << "Error demarshalling property update for" << this->toString(); + qCWarning(logDbus) << error; + } else { + qCDebug(logDbus).noquote() << "Updated property" << this->toString() << "to" + << this->valueString(); + } +} + +void AbstractDBusProperty::update() { + if (this->group == nullptr) { + qFatal(logDbus) << "Tried to update dbus property" << this->name + << "which is not attached to a group"; + } else { + const QString propStr = this->toString(); + + if (this->group->interface == nullptr) { + qFatal(logDbus).noquote() << "Tried to update property" << propStr + << "of a disconnected interface"; + } + + qCDebug(logDbus).noquote() << "Updating property" << propStr; + + auto pendingCall = + this->group->propertyInterface->Get(this->group->interface->interface(), this->name); + + auto* call = new QDBusPendingCallWatcher(pendingCall, this); + + auto responseCallback = [this, propStr](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logDbus).noquote() << "Error updating property" << propStr; + qCWarning(logDbus) << reply.error(); + } else { + this->tryUpdate(reply.value().variant()); + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); + } +} + +QString AbstractDBusProperty::toString() const { + const QString group = this->group == nullptr ? "{ NO GROUP }" : this->group->toString(); + return group + ':' + this->name; +} + +DBusPropertyGroup::DBusPropertyGroup(QVector properties, QObject* parent) + : QObject(parent) + , properties(std::move(properties)) {} + +void DBusPropertyGroup::setInterface(QDBusAbstractInterface* interface) { + if (this->interface != nullptr) { + delete this->propertyInterface; + this->propertyInterface = nullptr; + } + + if (interface != nullptr) { + this->interface = interface; + + this->propertyInterface = new DBusPropertiesInterface( + interface->service(), + interface->path(), + interface->connection(), + this + ); + + QObject::connect( + this->propertyInterface, + &DBusPropertiesInterface::PropertiesChanged, + this, + &DBusPropertyGroup::onPropertiesChanged + ); + } +} + +void DBusPropertyGroup::attachProperty(AbstractDBusProperty* property) { + this->properties.append(property); + property->group = this; +} + +void DBusPropertyGroup::updateAllDirect() { + qCDebug(logDbus).noquote() << "Updating all properties of" << this->toString() + << "via individual queries"; + + if (this->interface == nullptr) { + qFatal() << "Attempted to update properties of disconnected property group"; + } + + for (auto* property: this->properties) { + property->update(); + } +} + +void DBusPropertyGroup::updateAllViaGetAll() { + qCDebug(logDbus).noquote() << "Updating all properties of" << this->toString() << "via GetAll"; + + if (this->interface == nullptr) { + qFatal() << "Attempted to update properties of disconnected property group"; + } + + auto pendingCall = this->propertyInterface->GetAll(this->interface->interface()); + auto* call = new QDBusPendingCallWatcher(pendingCall, this); + + auto responseCallback = [this](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logDbus).noquote() + << "Error updating properties of" << this->toString() << "via GetAll"; + qCWarning(logDbus) << reply.error(); + } else { + qCDebug(logDbus).noquote() << "Received GetAll property set for" << this->toString(); + this->updatePropertySet(reply.value()); + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties) { + for (const auto [name, value]: properties.asKeyValueRange()) { + auto prop = std::find_if( + this->properties.begin(), + this->properties.end(), + [&name](AbstractDBusProperty* prop) { return prop->name == name; } + ); + + if (prop == this->properties.end()) { + qCDebug(logDbus) << "Ignoring untracked property update" << name << "for" << this; + } else { + (*prop)->tryUpdate(value); + } + } +} + +QString DBusPropertyGroup::toString() const { + if (this->interface == nullptr) { + return "{ DISCONNECTED }"; + } else { + return this->interface->service() + this->interface->path() + "/" + + this->interface->interface(); + } +} + +void DBusPropertyGroup::onPropertiesChanged( + const QString& interfaceName, + const QVariantMap& changedProperties, + const QStringList& invalidatedProperties +) { + if (interfaceName != this->interface->interface()) return; + qCDebug(logDbus) << "Received property change set and invalidations for" << this->toString(); + + for (const auto& name: invalidatedProperties) { + auto prop = std::find_if( + this->properties.begin(), + this->properties.end(), + [&name](AbstractDBusProperty* prop) { return prop->name == name; } + ); + + if (prop == this->properties.end()) { + qCDebug(logDbus) << "Ignoring untracked property invalidation" << name << "for" << this; + } else { + (*prop)->update(); + } + } + + this->updatePropertySet(changedProperties); +} + +} // namespace qs::dbus diff --git a/src/dbus/dbusutil.hpp b/src/dbus/dbusutil.hpp new file mode 100644 index 0000000..1c5f3f0 --- /dev/null +++ b/src/dbus/dbusutil.hpp @@ -0,0 +1,189 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class DBusPropertiesInterface; + +Q_DECLARE_LOGGING_CATEGORY(logDbus); + +namespace qs::dbus { + +QDBusError demarshallVariant(const QVariant& variant, const QMetaType& type, void* slot); + +template +class DBusResult { +public: + explicit DBusResult() = default; + explicit DBusResult(T value): value(std::move(value)) {} + explicit DBusResult(QDBusError error): error(std::move(error)) {} + explicit DBusResult(T value, QDBusError error) + : value(std::move(value)) + , error(std::move(error)) {} + + bool isValid() { return !this->error.isValid(); } + + T value; + QDBusError error; +}; + +template +DBusResult demarshallVariant(const QVariant& variant) { + T value; + auto error = demarshallVariant(variant, QMetaType::fromType(), &value); + return DBusResult(value, error); +} + +void asyncReadPropertyInternal( + const QMetaType& type, + QDBusAbstractInterface& interface, + const QString& property, + std::function)> callback +); + +template +void asyncReadProperty( + QDBusAbstractInterface& interface, + const QString& property, + std::function callback +) { + asyncReadPropertyInternal( + QMetaType::fromType(), + interface, + property, + [callback](std::function internalCallback) { // NOLINT + T slot; + auto error = internalCallback(static_cast(&slot)); + callback(slot, error); + } + ); +} + +class DBusPropertyGroup; + +class AbstractDBusProperty: public QObject { + Q_OBJECT; + +public: + explicit AbstractDBusProperty(QString name, const QMetaType& type, QObject* parent = nullptr) + : QObject(parent) + , name(std::move(name)) + , type(type) {} + + [[nodiscard]] QString toString() const; + [[nodiscard]] virtual QString valueString() = 0; + +public slots: + void update(); + +signals: + void changed(); + +protected: + virtual QDBusError read(const QVariant& variant) = 0; + +private: + void tryUpdate(const QVariant& variant); + + DBusPropertyGroup* group = nullptr; + + QString name; + QMetaType type; + + friend class DBusPropertyGroup; +}; + +class DBusPropertyGroup: public QObject { + Q_OBJECT; + +public: + explicit DBusPropertyGroup( + QVector properties = QVector(), + QObject* parent = nullptr + ); + + void setInterface(QDBusAbstractInterface* interface); + void attachProperty(AbstractDBusProperty* property); + void updateAllDirect(); + void updateAllViaGetAll(); + [[nodiscard]] QString toString() const; + +private slots: + void onPropertiesChanged( + const QString& interfaceName, + const QVariantMap& changedProperties, + const QStringList& invalidatedProperties + ); + +private: + void updatePropertySet(const QVariantMap& properties); + + DBusPropertiesInterface* propertyInterface = nullptr; + QDBusAbstractInterface* interface = nullptr; + QVector properties; + + friend class AbstractDBusProperty; +}; + +template +class DBusProperty: public AbstractDBusProperty { +public: + explicit DBusProperty(QString name, QObject* parent = nullptr, T value = T()) + : AbstractDBusProperty(std::move(name), QMetaType::fromType(), parent) + , value(std::move(value)) {} + + explicit DBusProperty( + DBusPropertyGroup& group, + QString name, + QObject* parent = nullptr, + T value = T() + ) + : DBusProperty(std::move(name), parent, std::move(value)) { + group.attachProperty(this); + } + + [[nodiscard]] QString valueString() override { + QString str; + QDebug(&str) << this->value; + return str; + } + + [[nodiscard]] T get() const { return this->value; } + + void set(T value) { + this->value = std::move(value); + emit this->changed(); + } + +protected: + QDBusError read(const QVariant& variant) override { + auto result = demarshallVariant(variant); + + if (result.isValid()) { + this->set(std::move(result.value)); + } + + return result.error; + } + +private: + T value; + + friend class DBusPropertyGroup; +}; + +} // namespace qs::dbus diff --git a/src/dbus/org.freedesktop.DBus.Properties.xml b/src/dbus/org.freedesktop.DBus.Properties.xml new file mode 100644 index 0000000..021123a --- /dev/null +++ b/src/dbus/org.freedesktop.DBus.Properties.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +