Commit 75333fc1 authored by David Edmundson's avatar David Edmundson

Add a new daemon for stats monitoring

Summary:
ksysguardd, whilst good, has a few problems

The code is a bit archaic, it relies on a polling API, which is overhead
for infrequently changed values or where setting up a monitor has a big
overhead.

It also moves the problem of translations into the daemon, allowing for
better extensibility without requiring client side changes.

The daemon is based around a typical OO model. Plugins have lists of
objects, those objects have properties using common Qt patterns. A
property also has various metadata.

For full compatibility ksgrd is wrapped and the plan is to land with the
bridge, then slowly land patches that use the new API natively.

An nvidia plugin is also added to show the API being used in another
format.

This is all consumed by the new API posted in D28141

Test Plan:
Unit test
Used with the new library to create a new suite of applet (upcoming patch)
Used in a ported ksysguard

Reviewers: #plasma, mart, ngraham, ahiemstra

Reviewed By: #plasma, mart, ngraham, ahiemstra

Subscribers: ahiemstra, ivan, mart, zzag, plasma-devel

Tags: #plasma

Differential Revision: https://phabricator.kde.org/D28333
parent 8e2603a6
......@@ -24,6 +24,7 @@ include(FeatureSummary)
find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS
Core
Widgets
Test
)
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS
Config
......@@ -83,6 +84,9 @@ add_subdirectory( example )
add_subdirectory( ksysguardd )
add_subdirectory(kstats)
add_subdirectory(libkstats)
add_subdirectory( plugins )
# add clang-format target for all our real source files
......
set(SOURCES
client.cpp
ksysguarddaemon.cpp
)
set_source_files_properties("ksysguard_iface.xml"
PROPERTIES INCLUDE "../ksysguard-backend/types.h" )
qt5_add_dbus_adaptor(SOURCES "ksysguard_iface.xml" ksysguarddaemon.h KSysGuardDaemon)
add_library(kstats_core STATIC ${SOURCES})
target_link_libraries(kstats_core PUBLIC Qt5::Core Qt5::DBus KF5::CoreAddons PW5::SysGuardBackend )
add_executable(kstats main.cpp)
target_link_libraries(kstats kstats_core)
install(TARGETS kstats DESTINATION ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
kdbusaddons_generate_dbus_service_file(kstats org.kde.kstats ${KDE_INSTALL_FULL_BINDIR})
add_subdirectory(test)
add_subdirectory(autotests)
include(ECMAddTests)
set(SOURCES
main.cpp
)
set_source_files_properties("${CMAKE_CURRENT_SOURCE_DIR}/../ksysguard_iface.xml"
PROPERTIES INCLUDE "../../libkstats/types.h" )
qt5_add_dbus_interface(SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/../ksysguard_iface.xml" kstatsiface)
ecm_add_test(
${SOURCES}
TEST_NAME kstatstest
LINK_LIBRARIES Qt5::Test kstats_core
)
/********************************************************************
Copyright 2020 David Edmundson <davidedmundson@kde.org>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/
#include <QtTest>
#include "../ksysguarddaemon.h"
#include <SensorContainer.h>
#include <SensorObject.h>
#include <SensorPlugin.h>
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusContext>
#include <QDBusMessage>
#include <QDBusMetaType>
#include "kstatsiface.h"
#include <QDebug>
class TestPlugin : public SensorPlugin
{
public:
TestPlugin(QObject *parent)
: SensorPlugin(parent, {})
{
m_testContainer = new SensorContainer("testContainer", "Test Container", this);
m_testObject = new SensorObject("testObject", "Test Object", m_testContainer);
m_property1 = new SensorProperty("property1", m_testObject);
m_property1->setMin(0);
m_property1->setMax(100);
m_property1->setShortName("Some Sensor 1");
m_property1->setName("Some Sensor Name 1");
m_property2 = new SensorProperty("property2", m_testObject);
}
QString providerName() const override
{
return "testPlugin";
}
void update() override
{
m_updateCount++;
}
SensorContainer *m_testContainer;
SensorObject *m_testObject;
SensorProperty *m_property1;
SensorProperty *m_property2;
int m_updateCount = 0;
};
class KStatsTest : public KSysGuardDaemon
{
Q_OBJECT
public:
KStatsTest();
protected:
void loadProviders() override;
private Q_SLOTS:
void init();
void findById();
void update();
void subscription();
void changes();
void dbusApi();
private:
TestPlugin *m_testPlugin = nullptr;
};
KStatsTest::KStatsTest()
{
qDBusRegisterMetaType<SensorData>();
qDBusRegisterMetaType<SensorInfo>();
qDBusRegisterMetaType<SensorDataList>();
qDBusRegisterMetaType<QHash<QString, SensorInfo>>();
qDBusRegisterMetaType<QStringList>();
}
void KStatsTest::loadProviders()
{
m_testPlugin = new TestPlugin(this);
registerProvider(m_testPlugin);
}
void KStatsTest::init()
{
KSysGuardDaemon::init();
}
void KStatsTest::findById()
{
QVERIFY(findSensor("testContainer/testObject/property1"));
QVERIFY(findSensor("testContainer/testObject/property2"));
QVERIFY(!findSensor("testContainer/asdfasdfasfs/property1"));
}
void KStatsTest::update()
{
QCOMPARE(m_testPlugin->m_updateCount, 0);
sendFrame();
QCOMPARE(m_testPlugin->m_updateCount, 1);
}
void KStatsTest::subscription()
{
QSignalSpy property1Subscribed(m_testPlugin->m_property1, &SensorProperty::subscribedChanged);
QSignalSpy property2Subscribed(m_testPlugin->m_property2, &SensorProperty::subscribedChanged);
QSignalSpy objectSubscribed(m_testPlugin->m_testObject, &SensorObject::subscribedChanged);
m_testPlugin->m_property1->subscribe();
QCOMPARE(property1Subscribed.count(), 1);
QCOMPARE(objectSubscribed.count(), 1);
m_testPlugin->m_property1->subscribe();
QCOMPARE(property1Subscribed.count(), 1);
QCOMPARE(objectSubscribed.count(), 1);
m_testPlugin->m_property2->subscribe();
QCOMPARE(objectSubscribed.count(), 1);
m_testPlugin->m_property1->unsubscribe();
QCOMPARE(property1Subscribed.count(), 1);
m_testPlugin->m_property1->unsubscribe();
QCOMPARE(property1Subscribed.count(), 2);
}
void KStatsTest::changes()
{
QSignalSpy property1Changed(m_testPlugin->m_property1, &SensorProperty::valueChanged);
m_testPlugin->m_property1->setValue(14);
QCOMPARE(property1Changed.count(), 1);
QCOMPARE(m_testPlugin->m_property1->value(), QVariant(14));
}
void KStatsTest::dbusApi()
{
OrgKdeKSysGuardDaemonInterface iface("org.kde.kstats",
"/",
QDBusConnection::sessionBus(),
this);
// list all objects
auto pendingSensors = iface.allSensors();
pendingSensors.waitForFinished();
auto sensors = pendingSensors.value();
QVERIFY(sensors.count() == 2);
// test metadata
QCOMPARE(sensors["testContainer/testObject/property1"].name, "Some Sensor Name 1");
// query value
m_testPlugin->m_property1->setValue(100);
auto pendingValues = iface.sensorData({ "testContainer/testObject/property1" });
pendingValues.waitForFinished();
QCOMPARE(pendingValues.value().first().sensorProperty, "testContainer/testObject/property1");
QCOMPARE(pendingValues.value().first().payload.toInt(), 100);
// change updates
QSignalSpy changesSpy(&iface, &OrgKdeKSysGuardDaemonInterface::newSensorData);
iface.subscribe({ "testContainer/testObject/property1" });
sendFrame();
// a frame with no changes, does nothing
QVERIFY(!changesSpy.wait(20));
m_testPlugin->m_property1->setValue(101);
// an update does nothing till it gets a frame, in order to batch
QVERIFY(!changesSpy.wait(20));
sendFrame();
QVERIFY(changesSpy.wait(20));
QCOMPARE(changesSpy.first().first().value<SensorDataList>().first().sensorProperty, "testContainer/testObject/property1");
QCOMPARE(changesSpy.first().first().value<SensorDataList>().first().payload, QVariant(101));
// we're not subscribed to property 2 so if that updates we should not get anything
m_testPlugin->m_property2->setValue(102);
sendFrame();
QVERIFY(!changesSpy.wait(20));
}
QTEST_GUILESS_MAIN(KStatsTest)
#include "main.moc"
/*
Copyright (c) 2019 David Edmundson <davidedmundson@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "client.h"
#include <QDBusConnection>
#include <QDBusMessage>
#include <QTimer>
#include <algorithm>
#include "ksysguarddaemon.h"
#include "SensorPlugin.h"
#include "SensorObject.h"
#include "SensorProperty.h"
Client::Client(KSysGuardDaemon *parent, const QString &serviceName)
: QObject(parent)
, m_serviceName(serviceName)
, m_daemon(parent)
{
connect(m_daemon, &KSysGuardDaemon::sensorRemoved, this, [this](const QString &sensor) {
m_subscribedSensors.remove(sensor);
});
}
Client::~Client()
{
for (auto sensor : qAsConst(m_subscribedSensors)) {
sensor->unsubscribe();
}
}
void Client::subscribeSensors(const QStringList &sensorPaths)
{
SensorDataList entries;
for (const QString &sensorPath : sensorPaths) {
if (auto sensor = m_daemon->findSensor(sensorPath)) {
m_connections.insert(sensor, connect(sensor, &SensorProperty::valueChanged, this, [this, sensor]() {
const QVariant value = sensor->value();
if (!value.isValid()) {
return;
}
m_pendingUpdates << SensorData(sensor->path(), value);
}));
m_connections.insert(sensor, connect(sensor, &SensorProperty::sensorInfoChanged, this, [this, sensor]() {
m_pendingMetaDataChanges[sensor->path()] = sensor->info();
}));
sensor->subscribe();
m_subscribedSensors.insert(sensorPath, sensor);
}
}
}
void Client::unsubscribeSensors(const QStringList &sensorPaths)
{
for (const QString &sensorPath : sensorPaths) {
if (auto sensor = m_subscribedSensors.take(sensorPath)) {
disconnect(m_connections.take(sensor));
disconnect(m_connections.take(sensor));
sensor->unsubscribe();
}
}
}
void Client::sendFrame()
{
sendMetaDataChanged(m_pendingMetaDataChanges);
sendValues(m_pendingUpdates);
m_pendingUpdates.clear();
m_pendingMetaDataChanges.clear();
}
void Client::sendValues(const SensorDataList &entries)
{
if (entries.isEmpty()) {
return;
}
auto msg = QDBusMessage::createTargetedSignal(m_serviceName, "/", "org.kde.KSysGuardDaemon", "newSensorData");
msg.setArguments({QVariant::fromValue(entries)});
QDBusConnection::sessionBus().send(msg);
}
void Client::sendMetaDataChanged(const SensorInfoMap &sensors)
{
if (sensors.isEmpty()) {
return;
}
auto msg = QDBusMessage::createTargetedSignal(m_serviceName, "/", "org.kde.KSysGuardDaemon", "sensorMetaDataChanged");
msg.setArguments({QVariant::fromValue(sensors)});
QDBusConnection::sessionBus().send(msg);
}
/*
Copyright (c) 2019 David Edmundson <davidedmundson@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#pragma once
#include "types.h"
#include <QObject>
class SensorProperty;
class KSysGuardDaemon;
/**
* This class represents an individual connection to the daemon
*/
class Client : public QObject
{
Q_OBJECT
public:
Client(KSysGuardDaemon *parent, const QString &serviceName);
~Client() override;
void subscribeSensors(const QStringList &sensorIds);
void unsubscribeSensors(const QStringList &sensorIds);
void sendFrame();
private:
void sendValues(const SensorDataList &updates);
void sendMetaDataChanged(const SensorInfoMap &sensors);
const QString m_serviceName;
KSysGuardDaemon *m_daemon;
QHash<QString, SensorProperty *> m_subscribedSensors;
QMultiHash<SensorProperty *, QMetaObject::Connection> m_connections;
SensorDataList m_pendingUpdates;
SensorInfoMap m_pendingMetaDataChanges;
};
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.kde.KSysGuardDaemon">
<signal name="sensorAdded">
<arg name="sensorId" type="s" direction="out"/>
</signal>
<signal name="sensorRemoved">
<arg name="sensorId" type="s" direction="out"/>
</signal>
<signal name="newSensorData">
<arg name="sensorData" type="a(sv)" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="SensorDataList"/>
</signal>
<signal name="sensorMetaDataChanged">
<arg name="metaData" type="a{s(sssuuddi)}" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QHash&lt;QString,SensorInfo&gt;"/>
</signal>
<method name="allSensors">
<arg name="sensorInfos" type="a{s(sssuuddi)}" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QHash&lt;QString,SensorInfo&gt;"/>
</method>
<method name="sensors">
<arg name="sensorsIds" type="as" direction="in"/>
<arg name ="sensorInfos" type="a{s(sssuuddi)}" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QHash&lt;QString,SensorInfo&gt;"/>
</method>
<method name="subscribe">
<arg name="sensorIds" type="as" direction="in"/>
</method>
<method name="unsubscribe">
<arg name="sensorIds" type="as" direction="in"/>
</method>
<method name="sensorData">
<arg name="sensorIds" type="as" direction="in"/>
<arg name="sensorData" type="a(sv)" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="SensorDataList"/>
</method>
</interface>
</node>
/*
Copyright (c) 2019 David Edmundson <davidedmundson@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "ksysguarddaemon.h"
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusMetaType>
#include <QDBusServiceWatcher>
#include <QTimer>
#include "SensorPlugin.h"
#include "SensorObject.h"
#include "SensorContainer.h"
#include "SensorProperty.h"
#include <KPluginLoader>
#include <KPluginMetaData>
#include <KPluginFactory>
#include "ksysguard_ifaceadaptor.h"
#include "client.h"
KSysGuardDaemon::KSysGuardDaemon()
: m_serviceWatcher(new QDBusServiceWatcher(this))
{
qDBusRegisterMetaType<SensorData>();
qDBusRegisterMetaType<SensorInfo>();
qRegisterMetaType<SensorDataList>("SDL");
qDBusRegisterMetaType<SensorDataList>();
qDBusRegisterMetaType<SensorInfoMap>();
qDBusRegisterMetaType<QStringList>();
new KSysGuardDaemonAdaptor(this);
m_serviceWatcher->setConnection(QDBusConnection::sessionBus());
connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &KSysGuardDaemon::onServiceDisconnected);
auto timer = new QTimer(this);
timer->setInterval(2000);
connect(timer, &QTimer::timeout, this, &KSysGuardDaemon::sendFrame);
timer->start();
}
void KSysGuardDaemon::init()
{
loadProviders();
QDBusConnection::sessionBus().registerObject("/", this, QDBusConnection::ExportAdaptors);
QDBusConnection::sessionBus().registerService("org.kde.kstats");
}
void KSysGuardDaemon::loadProviders()
{
//instantiate all plugins
QPluginLoader loader;
KPluginLoader::forEachPlugin(QStringLiteral("ksysguard"), [&loader, this](const QString &pluginPath) {
loader.setFileName(pluginPath);
QObject* obj = loader.instance();
auto factory = qobject_cast<KPluginFactory*>(obj);
if (!factory) {
qWarning() << "Failed to load ksysguard factory";
return;
}
SensorPlugin *provider = factory->create<SensorPlugin>(this);
if (!provider) {
return;
}
registerProvider(provider);
});
if (m_providers.isEmpty()) {
qWarning() << "No plugins found";
}
}
void KSysGuardDaemon::registerProvider(SensorPlugin *provider) {
m_providers.append(provider);
const auto containers = provider->containers();
for (auto container : containers) {
m_containers[container->id()] = container;
connect(container, &SensorContainer::objectAdded, this, [this](SensorObject *obj) {
for (auto sensor: obj->sensors()) {
emit sensorAdded(sensor->path());
}
});
connect(container, &SensorContainer::objectRemoved, this, [this](SensorObject *obj) {
for (auto sensor: obj->sensors()) {
emit sensorRemoved(sensor->path());
}
});
}
}
SensorInfoMap KSysGuardDaemon::allSensors() const
{
SensorInfoMap infoMap;
for (auto c : qAsConst(m_containers)) {
const auto objects = c->objects();
for(auto object : objects) {
const auto sensors = object->sensors();
for (auto sensor : sensors) {
infoMap[sensor->path()] = sensor->info();
}
}
}
return infoMap;
}
SensorInfoMap KSysGuardDaemon::sensors(const QStringList &sensorPaths) const
{
SensorInfoMap si;
for (const QString &path : sensorPaths) {
if (auto sensor = findSensor(path)) {
si[path] = sensor->info();
}
}
return si;
}
void KSysGuardDaemon::subscribe(const QStringList &sensorIds)
{
const QString sender = QDBusContext::message().service();
m_serviceWatcher->addWatchedService(sender);
Client *client = m_clients.value(sender);
if (!client) {
client = new Client(this, sender);
m_clients[sender] = client;
}
client->subscribeSensors(sensorIds);
}
void KSysGuardDaemon::unsubscribe(const QStringList &sensorIds)
{
const QString sender = QDBusContext::message().service();
Client *client = m_clients.value(sender);
if (!client) {
return;
}
client->unsubscribeSensors(sensorIds);
}
SensorDataList KSysGuardDaemon::sensorData(const QStringList &sensorIds)
{
SensorDataList sensorData;
for (const QString &sensorId: sensorIds) {
if (SensorProperty *sensorProperty = findSensor(sensorId)) {
const QVariant value = sensorProperty->value();
if (value.isValid()) {
sensorData << SensorData(sensorId, value);
}
}
}
return sensorData;
}
SensorProperty *KSysGuardDaemon::findSensor(const QString &path) const
{
int subsystemIndex = path.indexOf('/');
int propertyIndex = path.lastIndexOf('/');
const QString subsystem = path.left(subsystemIndex);
const QString object = path.mid(subsystemIndex + 1, propertyIndex - (subsystemIndex + 1));
const QString property = path.mid(propertyIndex + 1);
auto c = m_containers.value(subsystem);
if (!c) {
return nullptr;
}
auto o = c->object(object);
if (!o) {
return nullptr;
}
return o->sensor(property);
}
void KSysGuardDaemon::onServiceDisconnected(const QString &service)
{
delete m_clients.take(service);
}
void KSysGuardDaemon::sendFrame()
{
for (auto provider : qAsConst(m_providers)) {
provider->update();
}
for (auto client: qAsConst(m_clients)) {
client->sendFrame();
}
}
/*
Copyright (c) 2019 David Edmundson <davidedmundson@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.