diff --git a/CMakeLists.txt b/CMakeLists.txt index 459c6137..697e622e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,93 +1,97 @@ cmake_minimum_required(VERSION 3.0) project(ksysguard) set(PROJECT_VERSION "5.18.80") set(KSYSGUARD_VERSION 4.98.0) set(KSYSGUARD_STRING_VERSION "${KSYSGUARD_VERSION}") set(QT_MIN_VERSION "5.14.0") set(KF5_MIN_VERSION "5.58.0") find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) include(CheckIncludeFiles) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(KDEClangFormat) include(ECMAddTests) include(ECMInstallIcons) include(FeatureSummary) find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Widgets + Test ) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Config CoreAddons DBusAddons DocTools I18n IconThemes Init ItemViews KIO NewStuff Notifications WindowSystem ) find_package(KF5 REQUIRED COMPONENTS SysGuard) add_definitions(-DQT_NO_URL_CAST_FROM_STRING) add_definitions(-DQT_USE_QSTRINGBUILDER) #add_definitions(-DQT_NO_CAST_FROM_ASCII) #add_definitions(-DQT_NO_CAST_TO_ASCII) #add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x060000) if (EXISTS "${CMAKE_SOURCE_DIR}/.git") add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x060000) add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x054200) endif() find_package(Sensors) set_package_properties(Sensors PROPERTIES TYPE OPTIONAL PURPOSE "Allows to show sensor information") find_package(libpcap) set_package_properties( libpcap PROPERTIES TYPE RECOMMENDED PURPOSE "libpcap is used for per-application network usage." ) if (libpcap_FOUND) find_package(Libcap) set_package_properties(Libcap PROPERTIES TYPE OPTIONAL PURPOSE "Needed for setting capabilities of the per-application network plugin." ) endif() include_directories(${CMAKE_CURRENT_BINARY_DIR}) configure_file(config-workspace.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-workspace.h) add_subdirectory( gui ) add_subdirectory( doc ) add_subdirectory( pics ) 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 file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES *.cpp *.h) kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES}) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/kstats/CMakeLists.txt b/kstats/CMakeLists.txt new file mode 100644 index 00000000..5de2838c --- /dev/null +++ b/kstats/CMakeLists.txt @@ -0,0 +1,21 @@ +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) diff --git a/kstats/autotests/CMakeLists.txt b/kstats/autotests/CMakeLists.txt new file mode 100644 index 00000000..66dd25bd --- /dev/null +++ b/kstats/autotests/CMakeLists.txt @@ -0,0 +1,15 @@ +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 +) diff --git a/kstats/autotests/main.cpp b/kstats/autotests/main.cpp new file mode 100644 index 00000000..b808bee1 --- /dev/null +++ b/kstats/autotests/main.cpp @@ -0,0 +1,200 @@ +/******************************************************************** +Copyright 2020 David Edmundson + +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 . +*********************************************************************/ + +#include + +#include "../ksysguarddaemon.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "kstatsiface.h" +#include + +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(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType>(); + qDBusRegisterMetaType(); +} + +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().first().sensorProperty, "testContainer/testObject/property1"); + QCOMPARE(changesSpy.first().first().value().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" diff --git a/kstats/client.cpp b/kstats/client.cpp new file mode 100644 index 00000000..01467e24 --- /dev/null +++ b/kstats/client.cpp @@ -0,0 +1,111 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 +#include +#include + +#include + +#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); +} diff --git a/kstats/client.h b/kstats/client.h new file mode 100644 index 00000000..a063ba9d --- /dev/null +++ b/kstats/client.h @@ -0,0 +1,51 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 + +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 m_subscribedSensors; + QMultiHash m_connections; + SensorDataList m_pendingUpdates; + SensorInfoMap m_pendingMetaDataChanges; +}; diff --git a/kstats/ksysguard_iface.xml b/kstats/ksysguard_iface.xml new file mode 100644 index 00000000..238421c2 --- /dev/null +++ b/kstats/ksysguard_iface.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kstats/ksysguarddaemon.cpp b/kstats/ksysguarddaemon.cpp new file mode 100644 index 00000000..957c9fd4 --- /dev/null +++ b/kstats/ksysguarddaemon.cpp @@ -0,0 +1,210 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 +#include +#include +#include +#include + +#include + +#include "SensorPlugin.h" +#include "SensorObject.h" +#include "SensorContainer.h" +#include "SensorProperty.h" + +#include +#include +#include + +#include "ksysguard_ifaceadaptor.h" +#include "client.h" + +KSysGuardDaemon::KSysGuardDaemon() + : m_serviceWatcher(new QDBusServiceWatcher(this)) +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qRegisterMetaType("SDL"); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + 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(obj); + if (!factory) { + qWarning() << "Failed to load ksysguard factory"; + return; + } + SensorPlugin *provider = factory->create(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(); + } +} diff --git a/kstats/ksysguarddaemon.h b/kstats/ksysguarddaemon.h new file mode 100644 index 00000000..559bd10a --- /dev/null +++ b/kstats/ksysguarddaemon.h @@ -0,0 +1,75 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 +#include +#include + +class SensorPlugin; +class SensorContainer; +class SensorProperty; +class Entity; +class Client; +class QDBusServiceWatcher; + +/** + * The main central application + */ +class KSysGuardDaemon : public QObject, public QDBusContext +{ + Q_OBJECT + +public: + KSysGuardDaemon(); + void init(); + SensorProperty *findSensor(const QString &path) const; + +public Q_SLOTS: + // DBus + SensorInfoMap allSensors() const; + SensorInfoMap sensors(const QStringList &sensorsIds) const; + + void subscribe(const QStringList &sensorIds); + void unsubscribe(const QStringList &sensorIds); + + SensorDataList sensorData(const QStringList &sensorIds); + +Q_SIGNALS: + // DBus + void sensorAdded(const QString &sensorId); + void sensorRemoved(const QString &sensorId); + // not emitted directly as we use targetted signals via lower level API + void newSensorData(const SensorDataList &sensorData); + +protected: + // virtual for autotest to override and not load real plugins + virtual void loadProviders(); + + void sendFrame(); + void registerProvider(SensorPlugin *); + +private: + void onServiceDisconnected(const QString &service); + QVector m_providers; + QHash m_clients; + QHash m_containers; + QDBusServiceWatcher *m_serviceWatcher; +}; diff --git a/kstats/main.cpp b/kstats/main.cpp new file mode 100644 index 00000000..96288caf --- /dev/null +++ b/kstats/main.cpp @@ -0,0 +1,31 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 +#include + +#include "ksysguarddaemon.h" + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + KSysGuardDaemon d; + d.init(); + app.exec(); +} diff --git a/kstats/test/CMakeLists.txt b/kstats/test/CMakeLists.txt new file mode 100644 index 00000000..3d7d0f66 --- /dev/null +++ b/kstats/test/CMakeLists.txt @@ -0,0 +1,12 @@ +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) + +add_executable(kstatsviewer ${SOURCES}) +target_link_libraries(kstatsviewer Qt5::Core Qt5::DBus) + +install(TARGETS kstatsviewer DESTINATION ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/kstats/test/README.txt b/kstats/test/README.txt new file mode 100644 index 00000000..21abcbbe --- /dev/null +++ b/kstats/test/README.txt @@ -0,0 +1,3 @@ +This executable subscribes to a few sensors and prints out a rolling buffer of results + +It deliberately uses the native DBus API so we can test parts individually diff --git a/kstats/test/main.cpp b/kstats/test/main.cpp new file mode 100644 index 00000000..57cd223b --- /dev/null +++ b/kstats/test/main.cpp @@ -0,0 +1,231 @@ +/******************************************************************** +Copyright 2020 David Edmundson + +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 . +*********************************************************************/ + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "kstatsiface.h" + +QString unitToString(KSysGuard::utils::Unit unit); + +class SensorWatcher : public QCoreApplication +{ + Q_OBJECT + +public: + SensorWatcher(int &argc, char **argv); + ~SensorWatcher() = default; + + void subscribe(const QStringList &sensorNames); + void list(); + void showDetails(const QStringList &sensorNames); + + void setShowDetails(bool details); + +private: + void onNewSensorData(const SensorDataList &changes); + void onSensorMetaDataChanged(const SensorInfoMap &sensors); + void showSensorDetails(const SensorInfoMap &sensors); + OrgKdeKSysGuardDaemonInterface *m_iface; + bool m_showDetails = false; +}; + +int main(int argc, char **argv) +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType>(); + qDBusRegisterMetaType(); + + SensorWatcher app(argc, argv); + + QCommandLineParser parser; + auto listSensorsOption = QCommandLineOption(QStringLiteral("list"), QStringLiteral("List Available Sensors")); + parser.addOption(listSensorsOption); + + parser.addOption({ QStringLiteral("details"), QStringLiteral("Show detailed information about selected sensors") }); + + parser.addPositionalArgument(QStringLiteral("sensorNames"), QStringLiteral("List of sensors to monitor"), QStringLiteral("sensorId1 sensorId2 ...")); + parser.addHelpOption(); + parser.process(app); + + if (parser.isSet(listSensorsOption)) { + app.list(); + } else if (parser.positionalArguments().isEmpty()) { + qDebug() << "No sensors specified."; + parser.showHelp(-1); + } else { + app.setShowDetails(parser.isSet(QStringLiteral("details"))); + app.subscribe(parser.positionalArguments()); + app.exec(); + } +} + +SensorWatcher::SensorWatcher(int &argc, char **argv) + : QCoreApplication(argc, argv) + , m_iface(new OrgKdeKSysGuardDaemonInterface("org.kde.kstats", + "/", + QDBusConnection::sessionBus(), + this)) +{ + connect(m_iface, &OrgKdeKSysGuardDaemonInterface::newSensorData, this, &SensorWatcher::onNewSensorData); + connect(m_iface, &OrgKdeKSysGuardDaemonInterface::sensorMetaDataChanged, this, &SensorWatcher::onSensorMetaDataChanged); +} + +void SensorWatcher::subscribe(const QStringList &sensorNames) +{ + m_iface->subscribe(sensorNames); + + auto pendingInitialValues = m_iface->sensorData(sensorNames); + pendingInitialValues.waitForFinished(); + onNewSensorData(pendingInitialValues.value()); + + if (m_showDetails) { + auto pendingSensors = m_iface->sensors(sensorNames); + pendingSensors.waitForFinished(); + + auto sensors = pendingSensors.value(); + showSensorDetails(sensors); + } +} + +void SensorWatcher::onNewSensorData(const SensorDataList &changes) +{ + for (const auto &entry : changes) { + std::cout << qPrintable(entry.sensorProperty) << ' ' << qPrintable(entry.payload.toString()) << std::endl; + } +} + +void SensorWatcher::onSensorMetaDataChanged(const SensorInfoMap &sensors) +{ + if (!m_showDetails) { + return; + } + + std::cout << "Sensor metadata changed\n"; + showSensorDetails(sensors); +} + +void SensorWatcher::list() +{ + auto pendingSensors = m_iface->allSensors(); + pendingSensors.waitForFinished(); + auto sensors = pendingSensors.value(); + for (auto it = sensors.constBegin(); it != sensors.constEnd(); it++) { + std::cout << qPrintable(it.key()) << ' ' << qPrintable(it.value().name) << std::endl; + } +} + +void SensorWatcher::setShowDetails(bool details) +{ + m_showDetails = details; +} + +void SensorWatcher::showSensorDetails(const SensorInfoMap &sensors) +{ + for (auto it = sensors.constBegin(); it != sensors.constEnd(); ++it) { + auto info = it.value(); + std::cout << qPrintable(it.key()) << "\n"; + std::cout << " Name: " << qPrintable(info.name) << "\n"; + std::cout << " Short Name: " << qPrintable(info.shortName) << "\n"; + std::cout << " Description: " << qPrintable(info.description) << "\n"; + std::cout << " Unit: " << qPrintable(unitToString(info.unit)) << "\n"; + std::cout << " Minimum: " << info.min << "\n"; + std::cout << " Maximum: " << info.max << "\n"; + } +} + +QString unitToString(KSysGuard::utils::Unit unit) +{ + using namespace KSysGuard::utils; + + switch (unit) { + case UnitByte: + return QStringLiteral("B"); + case UnitKiloByte: + return QStringLiteral("KiB"); + case UnitMegaByte: + return QStringLiteral("MiB"); + case UnitGigaByte: + return QStringLiteral("GiB"); + case UnitTeraByte: + return QStringLiteral("TiB"); + case UnitPetaByte: + return QStringLiteral("PiB"); + case UnitByteRate: + return QStringLiteral("B/s"); + case UnitKiloByteRate: + return QStringLiteral("KiB/s"); + case UnitMegaByteRate: + return QStringLiteral("MiB/s"); + case UnitGigaByteRate: + return QStringLiteral("GiB/s"); + case UnitTeraByteRate: + return QStringLiteral("TiB/s"); + case UnitPetaByteRate: + return QStringLiteral("PiB/s"); + case UnitHertz: + return QStringLiteral("Hz"); + case UnitKiloHertz: + return QStringLiteral("kHz"); + case UnitMegaHertz: + return QStringLiteral("MHz"); + case UnitGigaHertz: + return QStringLiteral("GHz"); + case UnitTeraHertz: + return QStringLiteral("THz"); + case UnitPetaHertz: + return QStringLiteral("PHz"); + case UnitPercent: + return QStringLiteral("%"); + case UnitRpm: + return QStringLiteral("RPM"); + case UnitCelsius: + return QStringLiteral("°C"); + case UnitDecibelMilliWatts: + return QStringLiteral("dBm"); + case UnitSecond: + return QStringLiteral("s"); + case UnitVolt: + return QStringLiteral("V"); + case UnitWatt: + return QStringLiteral("W"); + case UnitRate: + return QStringLiteral("s⁻¹"); + case UnitInvalid: + return QStringLiteral("Invalid"); + case UnitNone: + return QStringLiteral("None"); + + default: + return QString(); + } +} + +#include "main.moc" diff --git a/libkstats/AggregateSensor.cpp b/libkstats/AggregateSensor.cpp new file mode 100644 index 00000000..fcfe0bc8 --- /dev/null +++ b/libkstats/AggregateSensor.cpp @@ -0,0 +1,244 @@ +/* + * Copyright 2019 Arjen Hiemstra + * + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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 . + */ + +#include "AggregateSensor.h" + +#include "SensorContainer.h" +#include + +// Add two QVariants together. +// +// This will try to add two QVariants together based on the type of first. This +// will try to convert first and second to a common type, add them, then convert +// back to the type of first. +// +// If any conversion fails or there is no way to add two types, first will be +// returned. +QVariant addVariants(const QVariant &first, const QVariant &second) +{ + auto result = QVariant {}; + + bool convertFirst = false; + bool convertSecond = false; + + auto type = first.type(); + switch (static_cast(type)) { + case QMetaType::Char: + case QMetaType::Short: + case QMetaType::Int: + case QMetaType::Long: + case QMetaType::LongLong: + result = first.toLongLong(&convertFirst) + second.toLongLong(&convertSecond); + break; + case QMetaType::UChar: + case QMetaType::UShort: + case QMetaType::UInt: + case QMetaType::ULong: + case QMetaType::ULongLong: + result = first.toULongLong(&convertFirst) + second.toULongLong(&convertSecond); + break; + case QMetaType::Float: + case QMetaType::Double: + result = first.toDouble(&convertFirst) + second.toDouble(&convertSecond); + break; + default: + return first; + } + + if (!convertFirst || !convertSecond) { + return first; + } + + if (!result.convert(type)) { + return first; + } + + return result; +} + +AggregateSensor::AggregateSensor(SensorObject *provider, const QString &id, const QString &name) + : SensorProperty(id, name, provider) + , m_subsystem(qobject_cast(provider->parent())) +{ + m_aggregateFunction = addVariants; + connect(m_subsystem, &SensorContainer::objectAdded, this, &AggregateSensor::updateSensors); + connect(m_subsystem, &SensorContainer::objectRemoved, this, &AggregateSensor::updateSensors); +} + +AggregateSensor::~AggregateSensor() +{ +} + +QRegularExpression AggregateSensor::matchSensors() const +{ + return m_matchObjects; +} + +QVariant AggregateSensor::value() const +{ + if (m_sensors.isEmpty()) { + return QVariant(); + } + + auto it = m_sensors.constBegin(); + QVariant result = it.value()->value(); + it++; + for (; it != m_sensors.constEnd(); it++) { + result = m_aggregateFunction(result, it.value()->value()); + } + return result; +} + +void AggregateSensor::subscribe() +{ + bool wasSubscribed = SensorProperty::isSubscribed(); + SensorProperty::subscribe(); + if (!wasSubscribed && isSubscribed()) { + for (auto sensor : qAsConst(m_sensors)) { + sensor->subscribe(); + } + } +} + +void AggregateSensor::unsubscribe() +{ + bool wasSubscribed = SensorProperty::isSubscribed(); + SensorProperty::unsubscribe(); + if (wasSubscribed && !isSubscribed()) { + for (auto sensor : qAsConst(m_sensors)) { + sensor->unsubscribe(); + } + } +} + +void AggregateSensor::setMatchSensors(const QRegularExpression &objectIds, const QString &propertyName) +{ + if (objectIds == m_matchObjects && propertyName == m_matchProperty) { + return; + } + + m_matchProperty = propertyName; + m_matchObjects = objectIds; + updateSensors(); +} + +std::function AggregateSensor::aggregateFunction() const +{ + return m_aggregateFunction; +} + +void AggregateSensor::setAggregateFunction(const std::function &newAggregateFunction) +{ + m_aggregateFunction = newAggregateFunction; +} + +void AggregateSensor::addSensor(SensorProperty *sensor) +{ + if (!sensor || sensor->path() == path() || m_sensors.contains(sensor->path())) { + return; + } + + if (isSubscribed()) { + sensor->subscribe(); + } + + connect(sensor, &SensorProperty::valueChanged, this, [this, sensor]() { + sensorDataChanged(sensor); + }); + m_sensors.insert(sensor->path(), sensor); +} + +void AggregateSensor::removeSensor(const QString &sensorPath) +{ + m_sensors.remove(sensorPath); +} + +void AggregateSensor::updateSensors() +{ + if (!m_matchObjects.isValid()) { + return; + } + for (auto obj : m_subsystem->objects()) { + if (m_matchObjects.match(obj->id()).hasMatch()) { + auto sensor = obj->sensor(m_matchProperty); + if (sensor) { + addSensor(sensor); + } + } + } + delayedEmitDataChanged(); +} + +void AggregateSensor::sensorDataChanged(SensorProperty *sensor) +{ + Q_UNUSED(sensor) + delayedEmitDataChanged(); +} + +void AggregateSensor::delayedEmitDataChanged() +{ + if (!m_dataChangeQueued) { + m_dataChangeQueued = true; + QTimer::singleShot(m_dataCompressionDuration, [this]() { + Q_EMIT valueChanged(); + m_dataChangeQueued = false; + }); + } +} + +PercentageSensor::PercentageSensor(SensorObject *provider, const QString &id, const QString &name) + : SensorProperty(id, name, provider) +{ + setUnit(KSysGuard::utils::UnitPercent); + setMax(100); +} + +PercentageSensor::~PercentageSensor() +{ +} + +void PercentageSensor::setBaseSensor(SensorProperty *property) +{ + m_sensor = property; + connect(property, &SensorProperty::valueChanged, this, &PercentageSensor::valueChanged); + connect(property, &SensorProperty::sensorInfoChanged, this, &PercentageSensor::valueChanged); +} + +QVariant PercentageSensor::value() const +{ + if (!m_sensor) { + return QVariant(); + } + QVariant value = m_sensor->value(); + if (!value.isValid()) { + return QVariant(); + } + return (value.toReal() / m_sensor->info().max) * 100.0; +} + +void PercentageSensor::subscribe() +{ + m_sensor->subscribe(); +} + +void PercentageSensor::unsubscribe() +{ + m_sensor->unsubscribe(); +} diff --git a/libkstats/AggregateSensor.h b/libkstats/AggregateSensor.h new file mode 100644 index 00000000..6b031e34 --- /dev/null +++ b/libkstats/AggregateSensor.h @@ -0,0 +1,89 @@ +/* + * Copyright 2019 Arjen Hiemstra + * + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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 . + */ + +#ifndef AGGREGATESENSOR_H +#define AGGREGATESENSOR_H + +#include + +#include +#include +#include + +#include "SensorObject.h" +#include "SensorPlugin.h" +#include "SensorProperty.h" + +/** + * @todo write docs + */ +class Q_DECL_EXPORT AggregateSensor : public SensorProperty +{ + Q_OBJECT + +public: + AggregateSensor(SensorObject *provider, const QString &id, const QString &name); + ~AggregateSensor(); + + QVariant value() const override; + void subscribe() override; + void unsubscribe() override; + + QRegularExpression matchSensors() const; + void setMatchSensors(const QRegularExpression &objectMatch, const QString &propertyId); + std::function aggregateFunction() const; + void setAggregateFunction(const std::function &function); + + void addSensor(SensorProperty *sensor); + void removeSensor(const QString &sensorPath); + +private: + void updateSensors(); + void sensorDataChanged(SensorProperty *sensor); + void delayedEmitDataChanged(); + + QRegularExpression m_matchObjects; + QString m_matchProperty; + QHash m_sensors; + bool m_dataChangeQueued = false; + int m_dataCompressionDuration = 100; + SensorContainer *m_subsystem; + + std::function m_aggregateFunction; +}; + +class Q_DECL_EXPORT PercentageSensor : public SensorProperty +{ + Q_OBJECT +public: + PercentageSensor(SensorObject *provider, const QString &id, const QString &name); + ~PercentageSensor() override; + + QVariant value() const override; + void subscribe() override; + void unsubscribe() override; + + void setBaseSensor(SensorProperty *sensor); + +private: + SensorProperty *m_sensor; +}; + +#endif // AGGREGATESENSOR_H diff --git a/libkstats/CMakeLists.txt b/libkstats/CMakeLists.txt new file mode 100644 index 00000000..44118e30 --- /dev/null +++ b/libkstats/CMakeLists.txt @@ -0,0 +1,20 @@ +set(ksgrdbackend_LIB_SRCS + AggregateSensor.cpp + SensorObject.cpp + SensorContainer.cpp + SensorPlugin.cpp + SensorProperty.cpp +) + +add_library(ksgrdbackend ${ksgrdbackend_LIB_SRCS}) +add_library(PW5::SysGuardBackend ALIAS ksgrdbackend) + +target_link_libraries(ksgrdbackend PUBLIC Qt5::Core Qt5::DBus) + +set_target_properties(ksgrdbackend PROPERTIES + EXPORT_NAME SysGuardBackend +) + +install(TARGETS ksgrdbackend EXPORT libKSysGuardbackendTargets ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) + +# Headers are currently not installed as we don't offer compatability yet diff --git a/libkstats/SensorContainer.cpp b/libkstats/SensorContainer.cpp new file mode 100644 index 00000000..e5c9b078 --- /dev/null +++ b/libkstats/SensorContainer.cpp @@ -0,0 +1,68 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 "SensorContainer.h" + +#include "SensorObject.h" +#include "SensorPlugin.h" + +SensorContainer::SensorContainer(const QString &id, const QString &name, SensorPlugin *parent) + : QObject(parent) + , m_id(id) + , m_name(name) +{ + parent->addContainer(this); +} + +SensorContainer::~SensorContainer() +{ +} + +QString SensorContainer::id() const +{ + return m_id; +} + +QString SensorContainer::name() const +{ + return m_name; +} + +QList SensorContainer::objects() +{ + return m_sensorObjects.values(); +} + +SensorObject *SensorContainer::object(const QString &id) const +{ + return m_sensorObjects.value(id); +} + +void SensorContainer::addSubObject(SensorObject *object) +{ + const QString id = object->id(); + Q_ASSERT(!m_sensorObjects.contains(id)); + m_sensorObjects[id] = object; + emit objectAdded(object); + + connect(object, &SensorObject::aboutToBeRemoved, this, [this, object]() { + m_sensorObjects.remove(object->id()); + emit objectRemoved(object); + }); +} diff --git a/libkstats/SensorContainer.h b/libkstats/SensorContainer.h new file mode 100644 index 00000000..f5823b1e --- /dev/null +++ b/libkstats/SensorContainer.h @@ -0,0 +1,73 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 + +#include "SensorPlugin.h" + +class SensorObject; + +/** + * Represents a collection of similar sensors. + * For example: a SensorContainer could represent all CPUs or represent all disks + */ +class Q_DECL_EXPORT SensorContainer : public QObject +{ + Q_OBJECT +public: + explicit SensorContainer(const QString &id, const QString &name, SensorPlugin *parent); + ~SensorContainer(); + + /** + * A computer readable ID of this group of sensors + */ + QString id() const; + /** + * A human readable name, used for selection + */ + QString name() const; + + QList objects(); + SensorObject *object(const QString &id) const; + +Q_SIGNALS: + /** + * Emitted when an object has been added + */ + void objectAdded(SensorObject *object); + /** + * Emitted after an object has been removed + * it may not be valid at this time + */ + void objectRemoved(SensorObject *object); + +private: + /** + * Called from sensorObject + * @internal + */ + void addSubObject(SensorObject *object); + + QString m_id; + QString m_name; + QHash m_sensorObjects; + friend class SensorObject; +}; diff --git a/libkstats/SensorObject.cpp b/libkstats/SensorObject.cpp new file mode 100644 index 00000000..decb304a --- /dev/null +++ b/libkstats/SensorObject.cpp @@ -0,0 +1,89 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 "SensorObject.h" + +#include "SensorContainer.h" +#include "SensorPlugin.h" + +SensorObject::SensorObject(const QString &id, SensorContainer *parent) + : SensorObject(id, QString(), parent) +{ +} + +SensorObject::SensorObject(const QString &id, const QString &name, SensorContainer *parent) + : QObject(parent) + , m_id(id) + , m_name(name) + , m_path(parent->id() + "/" + id) +{ + parent->addSubObject(this); +} + +SensorObject::~SensorObject() +{ +} + +QString SensorObject::id() const +{ + return m_id; +} + +QString SensorObject::name() const +{ + return m_name; +} + +QString SensorObject::path() const +{ + return m_path; +} + +QList SensorObject::sensors() const +{ + return m_sensors.values(); +} + +SensorProperty *SensorObject::sensor(const QString &sensorId) const +{ + return m_sensors.value(sensorId); +} + +void SensorObject::addProperty(SensorProperty *property) +{ + m_sensors[property->id()] = property; + + connect(property, &SensorProperty::subscribedChanged, this, [=]() { + uint count = std::count_if(m_sensors.constBegin(), m_sensors.constEnd(), [](const SensorProperty *prop) { + return prop->isSubscribed(); + }); + if (count == 1) { + emit subscribedChanged(true); + } else if (count == 0) { + emit subscribedChanged(false); + } + }); +} + +bool SensorObject::isSubscribed() const +{ + return std::any_of(m_sensors.constBegin(), m_sensors.constEnd(), [](const SensorProperty *prop) { + return prop->isSubscribed(); + }); +} diff --git a/libkstats/SensorObject.h b/libkstats/SensorObject.h new file mode 100644 index 00000000..feaa2a27 --- /dev/null +++ b/libkstats/SensorObject.h @@ -0,0 +1,69 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 + +#include "SensorPlugin.h" +#include "SensorProperty.h" + +class SensorContainer; +class SensorObject; + +/** + * Represents a physical or virtual object for example + * A CPU core, or a disk + */ +class Q_DECL_EXPORT SensorObject : public QObject +{ + Q_OBJECT +public: + explicit SensorObject(const QString &id, const QString &name, SensorContainer *parent); + explicit SensorObject(const QString &id, SensorContainer *parent); + ~SensorObject(); + + QString id() const; + QString name() const; + QString path() const; + + QList sensors() const; + SensorProperty *sensor(const QString &sensorId) const; + + void addProperty(SensorProperty *property); + + bool isSubscribed() const; +Q_SIGNALS: + /** + * Emitted when a client subscribes to one or more of the underlying properties of this object + */ + void subscribedChanged(bool); + + /** + * Emitted just before deletion + * The object is still valid at this point + */ + void aboutToBeRemoved(); + +private: + QString m_id; + QString m_name; + QString m_path; //or keep parent refernce? + QHash m_sensors; +}; diff --git a/libkstats/SensorPlugin.cpp b/libkstats/SensorPlugin.cpp new file mode 100644 index 00000000..e031e9f2 --- /dev/null +++ b/libkstats/SensorPlugin.cpp @@ -0,0 +1,45 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 "SensorPlugin.h" + +SensorPlugin::SensorPlugin(QObject *parent, const QVariantList &args) + : QObject(parent) +{ + Q_UNUSED(args) +} + +QList SensorPlugin::containers() const +{ + return m_containers; +} + +QString SensorPlugin::providerName() const +{ + return QString(); +} + +void SensorPlugin::update() +{ +} + +void SensorPlugin::addContainer(SensorContainer *container) +{ + m_containers << container; +} diff --git a/libkstats/SensorPlugin.h b/libkstats/SensorPlugin.h new file mode 100644 index 00000000..a5eb6e04 --- /dev/null +++ b/libkstats/SensorPlugin.h @@ -0,0 +1,69 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 +#include +#include +#include + +#include + +#include "types.h" + +class SensorPlugin; +class SensorContainer; + +/** + * Base class for plugins + */ +class Q_DECL_EXPORT SensorPlugin : public QObject +{ + Q_OBJECT +public: + SensorPlugin(QObject *parent, const QVariantList &args); + ~SensorPlugin() = default; + + /** + A list of all containers provided by this plugin + */ + QList containers() const; + + SensorContainer *container(const QString &id) const; + + /** + * @brief providerName + * @returns a non-user facing name of the plugin base + */ + virtual QString providerName() const; + + /** + * @brief + * A hook called before an update will be sent to the user + */ + virtual void update(); + + /** + * Registers an object as being available for stat retrieval. + */ + void addContainer(SensorContainer *container); + +private: + QList m_containers; +}; diff --git a/libkstats/SensorProperty.cpp b/libkstats/SensorProperty.cpp new file mode 100644 index 00000000..56a2a791 --- /dev/null +++ b/libkstats/SensorProperty.cpp @@ -0,0 +1,183 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 "SensorProperty.h" +#include "SensorObject.h" + +SensorProperty::SensorProperty(const QString &id, SensorObject *parent) + : SensorProperty(id, QString(), parent) +{ +} + +SensorProperty::SensorProperty(const QString &id, const QString &name, SensorObject *parent) + : SensorProperty(id, name, QVariant(), parent) +{ +} + +SensorProperty::SensorProperty(const QString &id, const QString &name, const QVariant &initalValue, SensorObject *parent) + : QObject(parent) + , m_id(id) +{ + m_path = parent->path() + "/" + m_id; + setName(name); + if (initalValue.isValid()) { + setValue(initalValue); + } + parent->addProperty(this); +} + +SensorProperty::~SensorProperty() +{ +} + +SensorInfo SensorProperty::info() const +{ + return m_info; +} + +QString SensorProperty::id() const +{ + return m_id; +} + +QString SensorProperty::path() const +{ + return m_path; +} + +void SensorProperty::setName(const QString &name) +{ + if (m_info.name == name) { + return; + } + + m_info.name = name; + emit sensorInfoChanged(); +} + +void SensorProperty::setShortName(const QString &name) +{ + if (m_info.shortName == name) { + return; + } + + m_info.shortName = name; + emit sensorInfoChanged(); +} + +void SensorProperty::setDescription(const QString &description) +{ + if (m_info.description == description) { + return; + } + + m_info.description = description; + emit sensorInfoChanged(); +} + +void SensorProperty::setMin(qreal min) +{ + if (qFuzzyCompare(m_info.min, min)) { + return; + } + + m_info.min = min; + emit sensorInfoChanged(); +} + +void SensorProperty::setMax(qreal max) +{ + if (qFuzzyCompare(m_info.max, max)) { + return; + } + + m_info.max = max; + emit sensorInfoChanged(); +} + +void SensorProperty::setMax(SensorProperty *other) +{ + setMax(other->value().toReal()); + if (isSubscribed()) { + other->subscribe(); + } + connect(this, &SensorProperty::subscribedChanged, this, [this, other](bool isSubscribed) { + if (isSubscribed) { + other->subscribe(); + setMax(other->value().toReal()); + } else { + other->unsubscribe(); + } + }); + connect(other, &SensorProperty::valueChanged, this, [this, other]() { + setMax(other->value().toReal()); + }); +} + +void SensorProperty::setUnit(KSysGuard::utils::Unit unit) +{ + if (m_info.unit == unit) { + return; + } + + m_info.unit = unit; + emit sensorInfoChanged(); +} + +void SensorProperty::setVariantType(QVariant::Type type) +{ + if (m_info.variantType == type) { + return; + } + + m_info.variantType = type; + emit sensorInfoChanged(); +} + +bool SensorProperty::isSubscribed() const +{ + return m_subscribers > 0; +} + +void SensorProperty::subscribe() +{ + m_subscribers++; + if (m_subscribers == 1) { + emit subscribedChanged(true); + } +} + +void SensorProperty::unsubscribe() +{ + m_subscribers--; + if (m_subscribers == 0) { + emit subscribedChanged(false); + } +} + +QVariant SensorProperty::value() const +{ + return m_value; +} + +void SensorProperty::setValue(const QVariant &value) +{ + m_value = value; + emit valueChanged(); +} diff --git a/libkstats/SensorProperty.h b/libkstats/SensorProperty.h new file mode 100644 index 00000000..fe871155 --- /dev/null +++ b/libkstats/SensorProperty.h @@ -0,0 +1,120 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 + +class SensorObject; + +/** + * Represents a given value source with attached metadata + * For example, current load for a given CPU core, or a disk capacity + */ +class Q_DECL_EXPORT SensorProperty : public QObject +{ + Q_OBJECT +public: + explicit SensorProperty(const QString &id, SensorObject *parent); + explicit SensorProperty(const QString &id, const QString &name, SensorObject *parent); + explicit SensorProperty(const QString &id, const QString &name, const QVariant &initalValue, SensorObject *parent); + + ~SensorProperty() override; + + SensorInfo info() const; + + /** + * A computer readable ID of the property + */ + QString id() const; + + /** + * A deduced path based on the concatenated ID of ourselves + parent IDs + */ + QString path() const; + /** + * A human reabable translated name of the property + */ + void setName(const QString &name); + void setShortName(const QString &name); + + void setDescription(const QString &description); + /** + * Sets a hint describing the minimum value this value can be. + * Values are not clipped, it is a hint for graphs. + * When not relevant, leave unset + */ + void setMin(qreal min); + /** + * Sets a hint describing the maximum value this value can be. + * Values are not clipped, it is a hint for graphs. + * When not relevant, leave unset + */ + void setMax(qreal max); + /** + * Shorthand for setting the maximum value to that of another property + * For example to mark the usedSpace of a disk to be the same as the disk capacity + */ + void setMax(SensorProperty *other); + void setUnit(KSysGuard::utils::Unit unit); + void setVariantType(QVariant::Type type); + + bool isSubscribed() const; + + /** + * Called when a client requests to get continual updates from this property. + */ + virtual void subscribe(); + /** + * Called when a client disconnects or no longer wants updates for this property. + */ + virtual void unsubscribe(); + /** + * Returns the last value set for this property + */ + virtual QVariant value() const; + /** + * Update the stored value for this property + */ + void setValue(const QVariant &value); + +Q_SIGNALS: + /** + * Emitted when the value changes + * Clients should emit this manually if they implement value() themselves + */ + void valueChanged(); + /** + * Emitted when the metadata of a sensor changes. + * min/max etc. + */ + void sensorInfoChanged(); + /** + * Emitted when we have our first subscription, or all subscriptions are gone + */ + void subscribedChanged(bool); + +private: + SensorInfo m_info; + QString m_id; + QString m_path; + QVariant m_value; + int m_subscribers = 0; +}; diff --git a/libkstats/types.h b/libkstats/types.h new file mode 100644 index 00000000..d62e7ca5 --- /dev/null +++ b/libkstats/types.h @@ -0,0 +1,177 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 +#include +#include +#include + +#include + +namespace KSysGuard +{ +namespace utils +{ + +/** + * This enum type is used to specify metric prefixes. + */ +enum MetricPrefix { + MetricPrefixAutoAdjust = -1, + MetricPrefixUnity = 0, + MetricPrefixKilo, + MetricPrefixMega, + MetricPrefixGiga, + MetricPrefixTera, + MetricPrefixPeta, + MetricPrefixLast = MetricPrefixPeta +}; + +/** + * This enum types is used to specify units. + */ +enum Unit { + UnitInvalid = -1, + UnitNone = 0, + + // Byte size units. + UnitByte = 100, + UnitKiloByte = MetricPrefixKilo + UnitByte, + UnitMegaByte = MetricPrefixMega + UnitByte, + UnitGigaByte = MetricPrefixGiga + UnitByte, + UnitTeraByte = MetricPrefixTera + UnitByte, + UnitPetaByte = MetricPrefixPeta + UnitByte, + + // Data rate units. + UnitByteRate = 200, + UnitKiloByteRate = MetricPrefixKilo + UnitByteRate, + UnitMegaByteRate = MetricPrefixMega + UnitByteRate, + UnitGigaByteRate = MetricPrefixGiga + UnitByteRate, + UnitTeraByteRate = MetricPrefixTera + UnitByteRate, + UnitPetaByteRate = MetricPrefixPeta + UnitByteRate, + + // Frequency. + UnitHertz = 300, + UnitKiloHertz = MetricPrefixKilo + UnitHertz, + UnitMegaHertz = MetricPrefixMega + UnitHertz, + UnitGigaHertz = MetricPrefixGiga + UnitHertz, + UnitTeraHertz = MetricPrefixTera + UnitHertz, + UnitPetaHertz = MetricPrefixPeta + UnitHertz, + + // Time units. + UnitBootTimestamp = 400, + UnitSecond, + UnitTime, + + // Misc units. + UnitCelsius = 500, + UnitDecibelMilliWatts, + UnitPercent, + UnitRate, + UnitRpm, + UnitVolt, + UnitWatt, +}; +} +} + +//Data that is static for the lifespan of the sensor +class SensorInfo +{ +public: + SensorInfo() = default; + QString name; //translated? + QString shortName; + QString description; // translated + QVariant::Type variantType = QVariant::Invalid; + KSysGuard::utils::Unit unit = KSysGuard::utils::UnitInvalid; //Both a format hint and implies data type (i.e double/string) + qreal min = 0; + qreal max = 0; +}; +Q_DECLARE_METATYPE(SensorInfo); +// this stuff could come from .desktop files (for the DBus case) or hardcoded (eg. for example nvidia-smi case) or come from current "ksysgrd monitors" + +class Q_DECL_EXPORT SensorData +{ +public: + SensorData() = default; + SensorData(const QString &_sensorProperty, const QVariant &_payload) + : sensorProperty(_sensorProperty) + , payload(_payload) + { + } + QString sensorProperty; + QVariant payload; +}; +Q_DECLARE_METATYPE(SensorData); + +typedef QHash SensorInfoMap; +typedef QVector SensorDataList; + +Q_DECLARE_METATYPE(SensorDataList); + +inline QDBusArgument &operator<<(QDBusArgument &argument, const SensorInfo &s) +{ + argument.beginStructure(); + argument << s.name; + argument << s.shortName; + argument << s.description; + argument << s.variantType; + argument << s.unit; + argument << s.min; + argument << s.max; + argument.endStructure(); + return argument; +} + +inline const QDBusArgument &operator>>(const QDBusArgument &argument, SensorInfo &s) +{ + argument.beginStructure(); + argument >> s.name; + argument >> s.shortName; + argument >> s.description; + uint32_t t; + argument >> t; + s.variantType = static_cast(t); + argument >> t; + s.unit = static_cast(t); + argument >> s.min; + argument >> s.max; + argument.endStructure(); + return argument; +} + +inline QDBusArgument &operator<<(QDBusArgument &argument, const SensorData &s) +{ + argument.beginStructure(); + argument << s.sensorProperty; + argument << QDBusVariant(s.payload); + argument.endStructure(); + return argument; +} + +inline const QDBusArgument &operator>>(const QDBusArgument &argument, SensorData &s) +{ + argument.beginStructure(); + argument >> s.sensorProperty; + argument >> s.payload; + argument.endStructure(); + return argument; +} diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 4d7c5854..88b36aa8 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -1,4 +1,7 @@ if (libpcap_FOUND) add_subdirectory(process/network) endif() - add_subdirectory(process/nvidia) +add_subdirectory(process/nvidia) + +add_subdirectory(global/nvidia) +add_subdirectory(global/ksgrd) diff --git a/plugins/global/ksgrd/CMakeLists.txt b/plugins/global/ksgrd/CMakeLists.txt new file mode 100644 index 00000000..1817c894 --- /dev/null +++ b/plugins/global/ksgrd/CMakeLists.txt @@ -0,0 +1,8 @@ +set(KSYSGUARD_KSGRD_PLUGIN_SOURCES + ksgrdiface.cpp +) + +add_library(ksysguard_ksgrd MODULE ${KSYSGUARD_KSGRD_PLUGIN_SOURCES}) +target_link_libraries(ksysguard_ksgrd Qt5::Core Qt5::DBus PW5::SysGuardBackend KF5::SysGuard KF5::CoreAddons KF5::I18n) + +install(TARGETS ksysguard_ksgrd DESTINATION ${KDE_INSTALL_PLUGINDIR}/ksysguard) diff --git a/plugins/global/ksgrd/ksgrdiface.cpp b/plugins/global/ksgrd/ksgrdiface.cpp new file mode 100644 index 00000000..a4cd1397 --- /dev/null +++ b/plugins/global/ksgrd/ksgrdiface.cpp @@ -0,0 +1,399 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 "ksgrdiface.h" + +#include "AggregateSensor.h" + +#include +#include +#include + +#include +#include + +// TODO instantiate multiple instances with args for which host to use + +KSGRDIface::KSGRDIface(QObject *parent, const QVariantList &args) + : SensorPlugin(parent, args) +{ + auto registerSubsystem = [this](const QString &id) { + m_subsystems[id] = new SensorContainer(id, id, this); // FIXME name resolve + }; + registerSubsystem("acpi"); + registerSubsystem("cpu"); + registerSubsystem("disk"); + registerSubsystem("lmsensors"); + registerSubsystem("mem"); + registerSubsystem("network"); + registerSubsystem("partitions"); + registerSubsystem("uptime"); + + KSGRD::SensorMgr = new KSGRD::SensorManager(this); + KSGRD::SensorMgr->engage(QStringLiteral("localhost"), QLatin1String(""), QStringLiteral("ksysguardd")); + connect(KSGRD::SensorMgr, &KSGRD::SensorManager::update, this, &KSGRDIface::updateMonitorsList); + updateMonitorsList(); + + // block for sensors to be loaded, but with a guard in case one fails to process + // a non issue when we port things to be in-process + + QEventLoop e; + auto t = new QTimer(&e); + t->setInterval(2000); + t->start(); + connect(t, &QTimer::timeout, &e, [&e, this]() { + e.quit(); + }); + connect(this, &KSGRDIface::sensorAdded, &e, [&e, this] { + if (m_sensors.count() == m_sensorIds.count()) { + e.quit(); + } + }); + e.exec(); + addAggregateSensors(); +} + +KSGRDIface::~KSGRDIface() +{ +} + +void KSGRDIface::subscribe(const QString &sensorPath) +{ + if (!m_subscribedSensors.contains(sensorPath)) { + m_subscribedSensors << sensorPath; + + const int index = m_sensorIds.indexOf(sensorPath); + + if (index != -1) { + m_waitingFor++; + KSGRD::SensorMgr->sendRequest(QStringLiteral("localhost"), sensorPath, (KSGRD::SensorClient *)this, index); + KSGRD::SensorMgr->sendRequest(QStringLiteral("localhost"), QStringLiteral("%1?").arg(sensorPath), (KSGRD::SensorClient *)this, -(index + 2)); + } + } +} + +void KSGRDIface::unsubscribe(const QString &sensorPath) +{ + m_subscribedSensors.removeAll(sensorPath); +} + +void KSGRDIface::updateMonitorsList() +{ + KSGRD::SensorMgr->sendRequest(QStringLiteral("localhost"), QStringLiteral("monitors"), (KSGRD::SensorClient *)this, -1); +} + +void KSGRDIface::onSensorMetaDataRetrieved(int id, const QList &answer) +{ + if (answer.isEmpty() || id > m_sensorIds.count()) { + qDebug() << "sensor info answer was empty, (" << answer.isEmpty() << ") or sensors does not exist to us "; + return; + } + + const QStringList newSensorInfo = QString::fromUtf8(answer[0]).split('\t'); + + if (newSensorInfo.count() < 4) { + qDebug() << "bad sensor info, only" << newSensorInfo.count() + << "entries, and we were expecting 4. Answer was " << answer; + return; + } + + const QString key = m_sensorIds.value(id); + + const QString &sensorName = newSensorInfo[0]; + const QString &min = newSensorInfo[1]; + const QString &max = newSensorInfo[2]; + const QString &unit = newSensorInfo[3]; + + int subsystemIndex = key.indexOf('/'); + int propertyIndex = key.lastIndexOf('/'); + + const QString subsystemId = key.left(subsystemIndex); + const QString objectId = key.mid(subsystemIndex + 1, propertyIndex - (subsystemIndex + 1)); + const QString propertyId = key.mid(propertyIndex + 1); + + auto subsystem = m_subsystems[subsystemId]; + if (Q_UNLIKELY(!subsystem)) { + qDebug() << "could not find subsystem" << subsystemId; + return; + } + auto sensorObject = subsystem->object(objectId); + if (!sensorObject) { + sensorObject = new SensorObject(objectId, objectId, subsystem); // FIXME i18n name for object id? + } + + auto sensor = m_sensors.value(key, nullptr); + if (!sensor) { + sensor = new SensorProperty(propertyId, sensorObject); + } + + sensor->setName(sensorName); + sensor->setShortName(shortNameFor(key)); + sensor->setMin(min.toDouble()); + sensor->setMax(max.toDouble()); + sensor->setUnit(unitFromString(unit)); + + if (m_sensors.contains(key)) { + return; + } + + auto type = m_pendingTypes.take(key); + if (type == QLatin1String("float")) { + sensor->setVariantType(QVariant::Double); + } else { + sensor->setVariantType(QVariant::Int); + } + connect(sensor, &SensorProperty::subscribedChanged, this, [this, sensor](bool subscribed) { + if (subscribed) { + subscribe(sensor->path()); + } else { + unsubscribe(sensor->path()); + } + }); + m_sensors[key] = sensor; + + emit sensorAdded(); +} + +void KSGRDIface::onSensorListRetrieved(const QList &answer) +{ + QSet sensors; + int count = 0; + + for (const QByteArray &sens : answer) { + const QString sensStr { QString::fromUtf8(sens) }; + const QVector newSensorInfo = sensStr.splitRef('\t'); + if (newSensorInfo.count() < 2) { + continue; + } + auto type = newSensorInfo.at(1); + if (type == QLatin1String("logfile")) { + continue; // logfile data type not currently supported + } + + const QString newSensor = newSensorInfo[0].toString(); + sensors.insert(newSensor); + m_sensorIds.append(newSensor); + + m_pendingTypes.insert(newSensor, type.toString()); + //we don't emit sensorAdded yet, instead wait for the meta info to be fetched + KSGRD::SensorMgr->sendRequest(QStringLiteral("localhost"), QStringLiteral("%1?").arg(newSensor), (KSGRD::SensorClient *)this, -(count + 2)); + ++count; + } + + // look for removed sensors + // FIXME? + foreach (const QString &sensor, m_sensorIds) { + if (!sensors.contains(sensor)) { + m_sensorIds.removeOne(sensor); + m_sensors.remove(sensor); + } + } +} + +void KSGRDIface::onSensorUpdated(int id, const QList &answer) +{ + m_waitingFor--; + + const QString sensorName = m_sensorIds.at(id); + if (sensorName.isEmpty()) { + return; + } + QString reply; + if (!answer.isEmpty()) { + reply = QString::fromUtf8(answer[0]); + } + auto sensor = m_sensors[sensorName]; + if (sensor) { + if (sensor->info().variantType == QVariant::Double) { + bool rc; + double value = reply.toDouble(&rc); + if (rc) { + sensor->setValue(value); + } + } else if (sensor->info().variantType == QVariant::Int) { + bool rc; + int value = reply.toInt(&rc); + if (rc) { + sensor->setValue(value); + } + } else { + sensor->setValue(reply); + } + } +} + +KSysGuard::utils::Unit KSGRDIface::unitFromString(const QString &unitString) const +{ + using namespace KSysGuard; + static const QHash map({ { "%", utils::UnitPercent }, + { "1/s", utils::UnitRate }, + { "°C", utils::UnitCelsius }, + { "dBm", utils::UnitDecibelMilliWatts }, + { "KB", utils::UnitKiloByte }, + { "KB/s", utils::UnitKiloByteRate }, + { "MHz", utils::UnitMegaHertz }, + { "port", utils::UnitNone }, + { "rpm", utils::UnitRpm }, + { "s", utils::UnitTime }, + { "V", utils::UnitVolt } }); + return map.value(unitString, utils::UnitNone); +} + +void KSGRDIface::update() +{ + for (int i = 0; i < m_subscribedSensors.count(); i++) { + auto sensorName = m_subscribedSensors.at(i); + + int index = m_sensorIds.indexOf(sensorName); + if (index < 0) { + return; + } + m_waitingFor++; + KSGRD::SensorMgr->sendRequest(QStringLiteral("localhost"), sensorName, (KSGRD::SensorClient *)this, index); + } +} + +void KSGRDIface::sensorLost(int) +{ + m_waitingFor--; +} + +void KSGRDIface::answerReceived(int id, const QList &answer) +{ + //this is the response to "sensorName?" + if (id < -1) { + onSensorMetaDataRetrieved(-id - 2, answer); + return; + } + + //response to "monitors" + if (id == -1) { + onSensorListRetrieved(answer); + return; + } + onSensorUpdated(id, answer); +} + +void KSGRDIface::addAggregateSensors() +{ + auto networkAll = new SensorObject("all", i18n("All"), m_subsystems["network"]); + + auto sensor = new AggregateSensor(networkAll, "receivedDataRate", i18n("Received Data Rate")); + sensor->setShortName(i18n("Down")); + sensor->setMatchSensors(QRegularExpression("[^/]*/receiver"), QStringLiteral("data")); + sensor->setDescription(i18n("The rate at which data is received on all interfaces.")); + sensor->setUnit(KSysGuard::utils::UnitKiloByteRate); + + sensor = new AggregateSensor(networkAll, "totalReceivedData", i18n("Total Received Data")); + sensor->setShortName(i18n("Total Down")); + sensor->setMatchSensors(QRegularExpression("[^/]*/receiver"), QStringLiteral("dataTotal")); + sensor->setDescription(i18n("The total amount of data received on all interfaces.")); + sensor->setUnit(KSysGuard::utils::UnitKiloByte); + + sensor = new AggregateSensor(networkAll, "sentDataRate", i18n("Sent Data Rate")); + sensor->setShortName(i18n("Up")); + sensor->setMatchSensors(QRegularExpression("[^/]*/transmitter"), QStringLiteral("data")); + sensor->setDescription(i18n("The rate at which data is sent on all interfaces.")); + sensor->setUnit(KSysGuard::utils::UnitKiloByteRate); + + sensor = new AggregateSensor(networkAll, "totalSentData", i18n("Total Sent Data")); + sensor->setShortName(i18n("Total Up")); + sensor->setMatchSensors(QRegularExpression("[^/]*/transmitter"), QStringLiteral("dataTotal")); + sensor->setDescription(i18n("The total amount of data sent on all interfaces.")); + sensor->setUnit(KSysGuard::utils::UnitKiloByte); + + auto diskAll = new SensorObject("all", i18n("all"), m_subsystems["disk"]); + sensor = new AggregateSensor(diskAll, "read", i18n("Disk Read Accesses")); + sensor->setShortName(i18n("Read")); + // TODO: This regex is not exhaustive as it doesn't consider things that aren't treated as sdX devices. + // However, we do not simply want to match disk/* as that would include duplicate devices. + sensor->setMatchSensors(QRegularExpression("^sd[a-z]+[0-9]+_[^/]*/Rate$"), QStringLiteral("rblk")); + sensor->setDescription(i18n("Read accesses across all disk devices")); + + sensor = new AggregateSensor(diskAll, "write", i18n("Disk Write Accesses")); + sensor->setShortName(i18n("Write")); + // TODO: See above. + sensor->setMatchSensors(QRegularExpression("^sd[a-z]+[0-9]+_[^/]*/Rate$"), QStringLiteral("wblk")); + sensor->setDescription(i18n("Write accesses across all disk devices")); + + auto memPhysical = m_subsystems["mem"]->object("physical"); + Q_ASSERT(memPhysical); + if (!memPhysical) { + return; + } + + PercentageSensor *appLevel = new PercentageSensor(memPhysical, "applicationlevel", i18n("Application Memory Percentage")); + appLevel->setShortName(i18n("Application")); + appLevel->setBaseSensor(memPhysical->sensor("application")); + appLevel->setDescription(i18n("Percentage of memory taken by applications.")); + + PercentageSensor *bufLevel = new PercentageSensor(memPhysical, "buflevel", i18n("Buffer Memory Percentage")); + bufLevel->setShortName(i18n("Buffer")); + bufLevel->setBaseSensor(memPhysical->sensor("buf")); + bufLevel->setDescription(i18n("Percentage of memory taken by the buffer.")); + + PercentageSensor *cacheLevel = new PercentageSensor(memPhysical, "cachelevel", i18n("Cache Memory Percentage")); + cacheLevel->setShortName(i18n("Cache")); + cacheLevel->setBaseSensor(memPhysical->sensor("cached")); + cacheLevel->setDescription(i18n("Percentage of memory taken by the cache.")); + + PercentageSensor *freeLevel = new PercentageSensor(memPhysical, "freelevel", i18n("Free Memory Percentage")); + freeLevel->setShortName(i18n("Cache")); + freeLevel->setBaseSensor(memPhysical->sensor("free")); + freeLevel->setDescription(i18n("Percentage of free memory.")); + + PercentageSensor *usedLevel = new PercentageSensor(memPhysical, "usedlevel", i18n("Used Memory Percentage")); + usedLevel->setShortName(i18n("Used")); + usedLevel->setBaseSensor(memPhysical->sensor("used")); + usedLevel->setDescription(i18n("Percentage of used memory.")); + + PercentageSensor *availableLevel = new PercentageSensor(memPhysical, "availablelevel", i18n("Available Memory Percentage")); + availableLevel->setShortName(i18n("Available")); + availableLevel->setBaseSensor(memPhysical->sensor("available")); + availableLevel->setDescription(i18n("Percentage of used memory.")); + + PercentageSensor *allocatedLevel = new PercentageSensor(memPhysical, "allocatedlevel", i18n("Allocated Memory Percentage")); + allocatedLevel->setShortName(i18n("Used")); + allocatedLevel->setBaseSensor(memPhysical->sensor("allocated")); + allocatedLevel->setDescription(i18n("Percentage of used memory.")); +} + +QString KSGRDIface::shortNameFor(const QString &key) +{ + // TODO: This is pretty ugly, but it is really hard to add this information to ksysguardd. + // So for now, we just map sensor ids to short names and return that. + + static QHash shortNames = { + { QStringLiteral("cpu/system/TotalLoad"), i18n("Usage") }, + { QStringLiteral("mem/physical/used"), i18n("Total Used") }, + { QStringLiteral("mem/physical/cached"), i18n("Cached") }, + { QStringLiteral("mem/physical/free"), i18n("Free") }, + { QStringLiteral("mem/physical/available"), i18n("Avalable") }, + { QStringLiteral("mem/physical/application"), i18n("Application") }, + { QStringLiteral("mem/physical/buf"), i18n("Buffer") }, + { QStringLiteral("cpu/system/processors"), i18n("Processors") }, + { QStringLiteral("cpu/system/cores"), i18n("Cores") }, + }; + + return shortNames.value(key, QString {}); +} + +K_PLUGIN_FACTORY(KSGRDPluginFactory, registerPlugin();) + +#include "ksgrdiface.moc" diff --git a/plugins/global/ksgrd/ksgrdiface.h b/plugins/global/ksgrd/ksgrdiface.h new file mode 100644 index 00000000..6a6d1e67 --- /dev/null +++ b/plugins/global/ksgrd/ksgrdiface.h @@ -0,0 +1,75 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 "SensorContainer.h" +#include "SensorObject.h" +#include "SensorPlugin.h" +#include "SensorProperty.h" + +#include +#include + +class AggregateSensor; + +class KSGRDIface : public SensorPlugin, public KSGRD::SensorClient +{ + Q_OBJECT + +public: + KSGRDIface(QObject *parent, const QVariantList &args); + ~KSGRDIface(); + + virtual QString providerName() const override + { + return QStringLiteral("ksgrd"); + } + + void update() override; + + // From KSGRD::SensorClient + void sensorLost(int sensor) override; + void answerReceived(int id, const QList &answer) override; + +Q_SIGNALS: + void sensorAdded(); + +private: + void updateMonitorsList(); + void onSensorMetaDataRetrieved(int id, const QList &answer); + void onSensorListRetrieved(const QList &answer); + void onSensorUpdated(int id, const QList &answer); + + void subscribe(const QString &sensorPath); + void unsubscribe(const QString &sensorPath); + + KSysGuard::utils::Unit unitFromString(const QString &unitString) const; + void addAggregateSensors(); + QString shortNameFor(const QString &key); + + //This qlist is just to have an index mapping because of KSGRD's old API + //Could be an index in SensorInfo subclass + QStringList m_sensorIds; + QStringList m_subscribedSensors; + + QHash m_subsystems; + QHash m_sensors; + QHash m_pendingTypes; + int m_waitingFor; +}; diff --git a/plugins/global/nvidia/CMakeLists.txt b/plugins/global/nvidia/CMakeLists.txt new file mode 100644 index 00000000..df5f544e --- /dev/null +++ b/plugins/global/nvidia/CMakeLists.txt @@ -0,0 +1,8 @@ +set(KSYSGUARD_NVIDIA_PLUGIN_SOURCES + nvidia.cpp +) + +add_library(ksysguard_plugin_nvidiaglobal MODULE ${KSYSGUARD_NVIDIA_PLUGIN_SOURCES}) +target_link_libraries(ksysguard_plugin_nvidiaglobal Qt5::Core Qt5::DBus PW5::SysGuardBackend KF5::CoreAddons KF5::I18n) + +install(TARGETS ksysguard_plugin_nvidiaglobal DESTINATION ${KDE_INSTALL_PLUGINDIR}/ksysguard) diff --git a/plugins/global/nvidia/nvidia.cpp b/plugins/global/nvidia/nvidia.cpp new file mode 100644 index 00000000..7c0d45c5 --- /dev/null +++ b/plugins/global/nvidia/nvidia.cpp @@ -0,0 +1,172 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 "nvidia.h" + +#include +#include +#include + +#include + +#include + +#include + +class GPU : public SensorObject +{ +public: + GPU(int index, SensorContainer *parent); + ~GPU() = default; + SensorProperty *powerProperty() const { return m_pwr; } + SensorProperty *tempProperty() const { return m_temp; } + SensorProperty *sharedMemory() const { return m_sm; } + SensorProperty *memory() const { return m_mem; } + SensorProperty *encoder() const { return m_enc; } + SensorProperty *decoder() const { return m_dec; } + SensorProperty *memoryClock() const { return m_memClock; } + SensorProperty *processorClock() const { return m_processorClock; } + +private: + SensorProperty *m_pwr; + SensorProperty *m_temp; + SensorProperty *m_sm; + SensorProperty *m_mem; + SensorProperty *m_enc; + SensorProperty *m_dec; + SensorProperty *m_memClock; + SensorProperty *m_processorClock; +}; + +GPU::GPU(int index, SensorContainer *parent) + : SensorObject(QStringLiteral("gpu%1").arg(index), i18n("GPU %1", index), parent) +{ + m_pwr = new SensorProperty(QStringLiteral("power"), this); + m_pwr->setName(i18n("Power")); + m_pwr->setUnit(KSysGuard::utils::UnitWatt); + m_pwr->setVariantType(QVariant::UInt); + + m_temp = new SensorProperty(QStringLiteral("temperature"), this); + m_temp->setName(i18n("Temperature")); + m_temp->setUnit(KSysGuard::utils::UnitCelsius); + m_temp->setVariantType(QVariant::Double); + + m_sm = new SensorProperty(QStringLiteral("sharedMemory"), this); + m_sm->setName(i18n("Shared memory")); + m_sm->setUnit(KSysGuard::utils::UnitPercent); + m_sm->setVariantType(QVariant::UInt); + + m_mem = new SensorProperty(QStringLiteral("memory"), this); + m_mem->setName(i18n("Memory")); + m_mem->setUnit(KSysGuard::utils::UnitPercent); + m_mem->setVariantType(QVariant::UInt); + + m_enc = new SensorProperty(QStringLiteral("encoderUsage"), this); + m_enc->setName(i18n("Encoder")); + m_enc->setUnit(KSysGuard::utils::UnitPercent); + m_enc->setVariantType(QVariant::UInt); + + m_dec = new SensorProperty(QStringLiteral("decoderUsage"), this); + m_dec->setName(i18n("Decoder")); + m_dec->setUnit(KSysGuard::utils::UnitPercent); + m_dec->setVariantType(QVariant::UInt); + + m_memClock = new SensorProperty(QStringLiteral("memoryClock"), this); + m_memClock->setName(i18n("Memory clock")); + m_memClock->setUnit(KSysGuard::utils::UnitMegaHertz); + m_memClock->setVariantType(QVariant::UInt); + + m_processorClock = new SensorProperty(QStringLiteral("processorClock"), this); + m_processorClock->setName(i18n("Processor clock")); + m_processorClock->setUnit(KSysGuard::utils::UnitMegaHertz); + m_processorClock->setVariantType(QVariant::UInt); +} + +NvidiaPlugin::NvidiaPlugin(QObject *parent, const QVariantList &args) + : SensorPlugin(parent, args) +{ + const auto sniExecutable = QStandardPaths::findExecutable("nvidia-smi"); + if (sniExecutable.isEmpty()) { + return; + } + + auto gpuSystem = new SensorContainer("nvidia", "Nvidia", this); + + //assuming just one GPU for now + auto gpu0 = new GPU(0, gpuSystem); + connect(gpu0, &SensorObject::subscribedChanged, this, &NvidiaPlugin::gpuSubscriptionChanged); + m_gpus[0] = gpu0; + + m_process = new QProcess(this); + m_process->setProgram(sniExecutable); + m_process->setArguments({ QStringLiteral("dmon") }); + + connect(m_process, &QProcess::readyReadStandardOutput, this, [=]() { + while (m_process->canReadLine()) { + const QString line = m_process->readLine(); + if (line.startsWith(QLatin1Char('#'))) { + continue; + } + const QVector parts = line.splitRef(QLatin1Char(' '), Qt::SkipEmptyParts); + + // format at time of writing is + // # gpu pwr gtemp mtemp sm mem enc dec mclk pclk + if (parts.count() != 10) { + continue; + } + bool ok; + int index = parts[0].toInt(&ok); + if (!ok) { + continue; + } + GPU *gpu = m_gpus.value(index); + if (!gpu) { + continue; + } + gpu->powerProperty()->setValue(parts[1].toUInt()); + gpu->tempProperty()->setValue(parts[2].toUInt()); + // I have no idea what parts[3] mtemp represents..skipping for now + gpu->sharedMemory()->setValue(parts[4].toUInt()); + gpu->memory()->setValue(parts[5].toUInt()); + gpu->encoder()->setValue(parts[6].toUInt()); + gpu->decoder()->setValue(parts[7].toUInt()); + gpu->memoryClock()->setValue(parts[8].toUInt()); + gpu->processorClock()->setValue(parts[9].toUInt()); + } + }); +} + +void NvidiaPlugin::gpuSubscriptionChanged(bool subscribed) +{ + if (subscribed) { + m_activeWatcherCount++; + if (m_activeWatcherCount == 1) { + m_process->start(); + } + } else { + m_activeWatcherCount--; + if (m_activeWatcherCount == 0) { + m_process->terminate(); + } + } +} + +K_PLUGIN_FACTORY(PluginFactory, registerPlugin();) + +#include "nvidia.moc" diff --git a/plugins/global/nvidia/nvidia.h b/plugins/global/nvidia/nvidia.h new file mode 100644 index 00000000..82d8e42a --- /dev/null +++ b/plugins/global/nvidia/nvidia.h @@ -0,0 +1,47 @@ +/* + Copyright (c) 2019 David Edmundson + + 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 "SensorObject.h" +#include "SensorPlugin.h" +#include "SensorProperty.h" + +#include +class QProcess; +class GPU; + +class NvidiaPlugin : public SensorPlugin +{ + Q_OBJECT +public: + NvidiaPlugin(QObject *parent, const QVariantList &args); + QString providerName() const override + { + return QStringLiteral("nvidia"); + } + + void init(); + +private: + void gpuSubscriptionChanged(bool subscribed); + + QProcess *m_process; + QHash m_gpus; + int m_activeWatcherCount = 0; +};