dbus: create property helper classes

Handles asynchronous property updates, part of the work for StatusNotifierItems.
This commit is contained in:
outfoxxed 2024-04-04 22:48:58 -07:00
parent 8529a2eb22
commit 8e530b6b77
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
6 changed files with 530 additions and 0 deletions

View file

@ -73,6 +73,11 @@ if (WAYLAND)
list(APPEND QT_FPDEPS WaylandClient) list(APPEND QT_FPDEPS WaylandClient)
endif() endif()
if (DBUS)
list(APPEND QT_DEPS Qt6::DBus)
list(APPEND QT_FPDEPS DBus)
endif()
find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS}) find_package(Qt6 REQUIRED COMPONENTS ${QT_FPDEPS})
qt_standard_project_setup(REQUIRES 6.6) qt_standard_project_setup(REQUIRES 6.6)

View file

@ -5,6 +5,10 @@ install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
add_subdirectory(core) add_subdirectory(core)
add_subdirectory(io) add_subdirectory(io)
if (DBUS)
add_subdirectory(dbus)
endif()
if (WAYLAND) if (WAYLAND)
add_subdirectory(wayland) add_subdirectory(wayland)
endif () endif ()

22
src/dbus/CMakeLists.txt Normal file
View file

@ -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)

284
src/dbus/dbusutil.cpp Normal file
View file

@ -0,0 +1,284 @@
#include "dbusutil.hpp"
#include <algorithm>
#include <utility>
#include <qcontainerfwd.h>
#include <qdbusabstractinterface.h>
#include <qdbusargument.h>
#include <qdbuserror.h>
#include <qdbusextratypes.h>
#include <qdbusmessage.h>
#include <qdbusmetatype.h>
#include <qdbuspendingcall.h>
#include <qdbuspendingreply.h>
#include <qdebug.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qmetatype.h>
#include <qobject.h>
#include <qpolygon.h>
#include <qvariant.h>
#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<QVariant*>(slot) = variant; // NOLINT
} else {
type.destruct(slot);
type.construct(slot, variant.constData());
}
} else if (variant.metaType() == QMetaType::fromType<QDBusArgument>()) {
auto arg = qvariant_cast<QDBusArgument>(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<void(std::function<QDBusError(void*)>)> 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<QDBusVariant> 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<QDBusVariant> 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<AbstractDBusProperty*> 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<QVariantMap> 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

189
src/dbus/dbusutil.hpp Normal file
View file

@ -0,0 +1,189 @@
#pragma once
#include <functional>
#include <utility>
#include <qcontainerfwd.h>
#include <qdbusabstractinterface.h>
#include <qdbuserror.h>
#include <qdbusextratypes.h>
#include <qdbuspendingcall.h>
#include <qdbusreply.h>
#include <qdbusservicewatcher.h>
#include <qdebug.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qvariant.h>
class DBusPropertiesInterface;
Q_DECLARE_LOGGING_CATEGORY(logDbus);
namespace qs::dbus {
QDBusError demarshallVariant(const QVariant& variant, const QMetaType& type, void* slot);
template <typename T>
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 <typename T>
DBusResult<T> demarshallVariant(const QVariant& variant) {
T value;
auto error = demarshallVariant(variant, QMetaType::fromType<T>(), &value);
return DBusResult<T>(value, error);
}
void asyncReadPropertyInternal(
const QMetaType& type,
QDBusAbstractInterface& interface,
const QString& property,
std::function<void(std::function<QDBusError(void*)>)> callback
);
template <typename T>
void asyncReadProperty(
QDBusAbstractInterface& interface,
const QString& property,
std::function<void(T, QDBusError)> callback
) {
asyncReadPropertyInternal(
QMetaType::fromType<T>(),
interface,
property,
[callback](std::function<QDBusError(void*)> internalCallback) { // NOLINT
T slot;
auto error = internalCallback(static_cast<void*>(&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<AbstractDBusProperty*> properties = QVector<AbstractDBusProperty*>(),
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<AbstractDBusProperty*> properties;
friend class AbstractDBusProperty;
};
template <typename T>
class DBusProperty: public AbstractDBusProperty {
public:
explicit DBusProperty(QString name, QObject* parent = nullptr, T value = T())
: AbstractDBusProperty(std::move(name), QMetaType::fromType<T>(), 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<T>(variant);
if (result.isValid()) {
this->set(std::move(result.value));
}
return result.error;
}
private:
T value;
friend class DBusPropertyGroup;
};
} // namespace qs::dbus

View file

@ -0,0 +1,26 @@
<node>
<interface name="org.freedesktop.DBus.Properties">
<method name="Get">
<arg name="interface_name" direction="in" type="s"/>
<arg name="property_name" direction="in" type="s"/>
<arg name="value" direction="out" type="v"/>
</method>
<method name="Set">
<arg name="interface_name" direction="in" type="s"/>
<arg name="property_name" direction="in" type="s"/>
<arg name="value" direction="in" type="v"/>
</method>
<method name="GetAll">
<arg name="interface_name" direction="in" type="s"/>
<arg name="props" direction="out" type="a{sv}"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
</method>
<signal name="PropertiesChanged">
<arg type="s" name="interface_name"/>
<arg type="a{sv}" name="changed_properties"/>
<arg type="as" name="invalidated_properties"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap" />
</signal>
</interface>
</node>