File indexing completed on 2024-05-12 17:00:14

0001 /*
0002     SPDX-FileCopyrightText: 2020 David Redondo <kde@david-redondo.de>
0003     SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 */
0007 
0008 #include "disks.h"
0009 
0010 #ifdef Q_OS_FREEBSD
0011 #include <devstat.h>
0012 #include <libgeom.h>
0013 #endif
0014 
0015 #include <QUrl>
0016 
0017 #include <KIO/FileSystemFreeSpaceJob>
0018 #include <KLocalizedString>
0019 #include <KPluginFactory>
0020 #include <Solid/Block>
0021 #include <Solid/Device>
0022 #include <Solid/DeviceNotifier>
0023 #include <Solid/Predicate>
0024 #include <Solid/StorageAccess>
0025 #include <Solid/StorageDrive>
0026 #include <Solid/StorageVolume>
0027 
0028 #include <systemstats/AggregateSensor.h>
0029 #include <systemstats/SensorContainer.h>
0030 #include <systemstats/SensorObject.h>
0031 
0032 class VolumeObject : public KSysGuard::SensorObject {
0033 public:
0034     VolumeObject(const Solid::Device &device, KSysGuard::SensorContainer *parent);
0035     void update();
0036     void setBytes(quint64 read, quint64 written, qint64 elapsedTime);
0037 
0038     const QString udi;
0039     const QString mountPoint;
0040 private:
0041     static QString idHelper(const Solid::Device &device);
0042 
0043     KSysGuard::SensorProperty *m_name = nullptr;
0044     KSysGuard::SensorProperty *m_total = nullptr;
0045     KSysGuard::SensorProperty *m_used = nullptr;
0046     KSysGuard::SensorProperty *m_free = nullptr;
0047     KSysGuard::SensorProperty *m_readRate = nullptr;
0048     KSysGuard::SensorProperty *m_writeRate = nullptr;
0049     quint64 m_bytesRead = 0;
0050     quint64 m_bytesWritten = 0;
0051 };
0052 
0053 QString VolumeObject::idHelper(const Solid::Device &device)
0054 {
0055     auto volume = device.as<Solid::StorageVolume>();
0056     return volume->uuid().isEmpty() ? volume->label() : volume->uuid();
0057 }
0058 
0059 
0060 VolumeObject::VolumeObject(const Solid::Device &device, KSysGuard::SensorContainer* parent)
0061     : SensorObject(idHelper(device), device.displayName(),  parent)
0062     , udi(device.udi())
0063     , mountPoint(device.as<Solid::StorageAccess>()->filePath())
0064 {
0065     auto volume = device.as<Solid::StorageVolume>();
0066 
0067     m_name = new KSysGuard::SensorProperty("name", i18nc("@title", "Name"), device.displayName(), this);
0068     m_name->setShortName(i18nc("@title", "Name"));
0069     m_name->setVariantType(QVariant::String);
0070 
0071     m_total = new KSysGuard::SensorProperty("total", i18nc("@title", "Total Space"), volume->size(), this);
0072     m_total->setPrefix(name());
0073     m_total->setShortName(i18nc("@title Short for 'Total Space'", "Total"));
0074     m_total->setUnit(KSysGuard::UnitByte);
0075     m_total->setVariantType(QVariant::ULongLong);
0076 
0077     m_used = new KSysGuard::SensorProperty("used", i18nc("@title", "Used Space"), this);
0078     m_used->setPrefix(name());
0079     m_used->setShortName(i18nc("@title Short for 'Used Space'", "Used"));
0080     m_used->setUnit(KSysGuard::UnitByte);
0081     m_used->setVariantType(QVariant::ULongLong);
0082     m_used->setMax(volume->size());
0083 
0084     m_free = new KSysGuard::SensorProperty("free", i18nc("@title", "Free Space"), this);
0085     m_free->setPrefix(name());
0086     m_free->setShortName(i18nc("@title Short for 'Free Space'", "Free"));
0087     m_free->setUnit(KSysGuard::UnitByte);
0088     m_free->setVariantType(QVariant::ULongLong);
0089     m_free->setMax(volume->size());
0090 
0091     m_readRate = new KSysGuard::SensorProperty("read", i18nc("@title", "Read Rate"), 0, this);
0092     m_readRate->setPrefix(name());
0093     m_readRate->setShortName(i18nc("@title Short for 'Read Rate'", "Read"));
0094     m_readRate->setUnit(KSysGuard::UnitByteRate);
0095     m_readRate->setVariantType(QVariant::Double);
0096 
0097     m_writeRate = new KSysGuard::SensorProperty("write", i18nc("@title", "Write Rate"), 0, this);
0098     m_writeRate->setPrefix(name());
0099     m_writeRate->setShortName(i18nc("@title Short for 'Write Rate'", "Write"));
0100     m_writeRate->setUnit(KSysGuard::UnitByteRate);
0101     m_writeRate->setVariantType(QVariant::Double);
0102 
0103     auto usedPercent = new KSysGuard::PercentageSensor(this, "usedPercent", i18nc("@title", "Percentage Used"));
0104     usedPercent->setPrefix(name());
0105     usedPercent->setBaseSensor(m_used);
0106 
0107     auto freePercent = new KSysGuard::PercentageSensor(this, "freePercent", i18nc("@title", "Percentage Free"));
0108     freePercent->setPrefix(name());
0109     freePercent->setBaseSensor(m_free);
0110 }
0111 
0112 void VolumeObject::update()
0113 {
0114     auto job = KIO::fileSystemFreeSpace(QUrl::fromLocalFile(mountPoint));
0115     connect(job, &KIO::FileSystemFreeSpaceJob::result, this, [this] (KJob *job, KIO::filesize_t size, KIO::filesize_t available) {
0116         if (!job->error()) {
0117             m_total->setValue(size);
0118             m_free->setValue(available);
0119             m_free->setMax(size);
0120             m_used->setValue(size - available);
0121             m_used->setMax(size);
0122         }
0123     });
0124 }
0125 
0126 void VolumeObject::setBytes(quint64 read, quint64 written, qint64 elapsed)
0127 {
0128     if (elapsed != 0) {
0129         double seconds = elapsed / 1000.0;
0130         m_readRate->setValue((read - m_bytesRead) / seconds);
0131         m_writeRate->setValue((written - m_bytesWritten) / seconds);
0132     }
0133     m_bytesRead = read;
0134     m_bytesWritten = written;
0135 }
0136 
0137 DisksPlugin::DisksPlugin(QObject *parent, const QVariantList &args)
0138     : SensorPlugin(parent, args)
0139 {
0140     auto container = new KSysGuard::SensorContainer("disk", i18n("Disks"), this);
0141     auto storageAccesses = Solid::Device::listFromType(Solid::DeviceInterface::StorageAccess);
0142     for (const auto &storageAccess : storageAccesses) {
0143        addDevice(storageAccess);
0144     }
0145     connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, [this] (const QString &udi) {
0146             addDevice(Solid::Device(udi));
0147     });
0148     connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved, this, [this, container] (const QString &udi) {
0149         Solid::Device device(udi);
0150         if (device.isDeviceInterface(Solid::DeviceInterface::StorageAccess)) {
0151             auto it = std::find_if(m_volumesByDevice.begin(), m_volumesByDevice.end(), [&udi] (VolumeObject *volume) {
0152                 return volume->udi == udi;
0153             });
0154             if (it != m_volumesByDevice.end()) {
0155                 container->removeObject(*it);
0156                 m_volumesByDevice.erase(it);
0157             }
0158         }
0159     });
0160     addAggregateSensors();
0161 #ifdef Q_OS_FREEBSD
0162     geom_stats_open();
0163 #endif
0164 }
0165 
0166 DisksPlugin::~DisksPlugin()
0167 {
0168 #ifdef Q_OS_FREEBSD
0169     geom_stats_close();
0170 #endif
0171 }
0172 
0173 void DisksPlugin::addDevice(const Solid::Device& device)
0174 {
0175     auto container = containers()[0];
0176     const auto volume = device.as<Solid::StorageVolume>();
0177     auto access = device.as<Solid::StorageAccess>();
0178     if (!access || !volume || volume->isIgnored()) {
0179         return;
0180     }
0181     Solid::Device drive = device;
0182     // Only exlude volumes if we know that they are for sure not on a hard disk
0183     while (drive.isValid()) {
0184         if (drive.is<Solid::StorageDrive>()) {
0185             auto type = drive.as<Solid::StorageDrive>()->driveType();
0186             if (type == Solid::StorageDrive::HardDisk) {
0187                 break;
0188             } else {
0189                 return;
0190             }
0191         }
0192         drive = drive.parent();
0193     }
0194 
0195     if (access->filePath() != QString()) {
0196         createAccessibleVolumeObject(device);
0197     }
0198     connect(access, &Solid::StorageAccess::accessibilityChanged, this, [this, container] (bool accessible, const QString &udi) {
0199         if (accessible) {
0200             Solid::Device device(udi);
0201             createAccessibleVolumeObject(device);
0202         } else {
0203             auto it = std::find_if(m_volumesByDevice.begin(), m_volumesByDevice.end(), [&udi] (VolumeObject *disk) {
0204                 return disk->udi == udi;
0205             });
0206             if (it != m_volumesByDevice.end()) {
0207                 container->removeObject(*it);
0208                 m_volumesByDevice.erase(it);
0209             }
0210         }
0211     });
0212 }
0213 
0214 void DisksPlugin::createAccessibleVolumeObject(const Solid::Device &device)
0215 {
0216     auto block = device.as<Solid::Block>();
0217     auto access = device.as<Solid::StorageAccess>();
0218     Q_ASSERT(access->isAccessible());
0219     const QString  mountPoint = access->filePath();
0220     const bool hasMountPoint = std::any_of(m_volumesByDevice.cbegin(), m_volumesByDevice.cend(), [mountPoint] (const VolumeObject* volume) {
0221         return volume->mountPoint == mountPoint;
0222     });
0223     if (hasMountPoint) {
0224         return;
0225     }
0226     m_volumesByDevice.insert(block->device(), new VolumeObject(device,  containers()[0]));
0227 }
0228 
0229 void DisksPlugin::addAggregateSensors()
0230 {
0231     auto container = containers()[0];
0232     auto allDisks = new KSysGuard::SensorObject("all", i18nc("@title", "All Disks"), container);
0233 
0234     auto total = new KSysGuard::AggregateSensor(allDisks, "total", i18nc("@title", "Total Space"));
0235     total->setShortName(i18nc("@title Short for 'Total Space'", "Total"));
0236     total->setUnit(KSysGuard::UnitByte);
0237     total->setVariantType(QVariant::ULongLong);
0238     total->setMatchSensors(QRegularExpression("^(?!all).*$"), "total");
0239 
0240     auto free = new KSysGuard::AggregateSensor(allDisks, "free", i18nc("@title", "Free Space"));
0241     free->setShortName(i18nc("@title Short for 'Free Space'", "Free"));
0242     free->setUnit(KSysGuard::UnitByte);
0243     free->setVariantType(QVariant::ULongLong);
0244     free->setMax(total->value().toULongLong());
0245     free->setMatchSensors(QRegularExpression("^(?!all).*$"), "free");
0246 
0247     auto used = new KSysGuard::AggregateSensor(allDisks, "used", i18nc("@title", "Used Space"));
0248     used->setShortName(i18nc("@title Short for 'Used Space'", "Used"));
0249     used->setUnit(KSysGuard::UnitByte);
0250     used->setVariantType(QVariant::ULongLong);
0251     used->setMax(total->value().toULongLong());
0252     used->setMatchSensors(QRegularExpression("^(?!all).*$"), "used");
0253 
0254     auto readRate = new KSysGuard::AggregateSensor(allDisks, "read", i18nc("@title", "Read Rate"), 0);
0255     readRate->setShortName(i18nc("@title Short for 'Read Rate'", "Read"));
0256     readRate->setUnit(KSysGuard::UnitByteRate);
0257     readRate->setVariantType(QVariant::Double);
0258     readRate->setMatchSensors(QRegularExpression("^(?!all).*$"), "read");
0259 
0260     auto writeRate = new KSysGuard::AggregateSensor(allDisks, "write", i18nc("@title", "Write Rate"), 0);
0261     writeRate->setShortName(i18nc("@title Short for 'Write Rate'", "Write"));
0262     writeRate->setUnit(KSysGuard::UnitByteRate);
0263     writeRate->setVariantType(QVariant::Double);
0264     writeRate->setMatchSensors(QRegularExpression("^(?!all).*$"), "write");
0265 
0266     auto freePercent = new KSysGuard::PercentageSensor(allDisks, "freePercent", i18nc("@title", "Percentage Free"));
0267     freePercent->setShortName(i18nc("@title, Short for `Percentage Free", "Free"));
0268     freePercent->setBaseSensor(free);
0269 
0270     auto usedPercent = new KSysGuard::PercentageSensor(allDisks, "usedPercent", i18nc("@title", "Percentage Used"));
0271     usedPercent->setShortName(i18nc("@title, Short for `Percentage Used", "Used"));
0272     usedPercent->setBaseSensor(used);
0273 
0274     connect(total, &KSysGuard::SensorProperty::valueChanged, this, [total, free, used] () {
0275         free->setMax(total->value().toULongLong());
0276         used->setMax(total->value().toULongLong());
0277     });
0278 }
0279 
0280 void DisksPlugin::update()
0281 {
0282     bool anySubscribed = false;
0283     for (auto volume : m_volumesByDevice) {
0284         if (volume->isSubscribed()) {
0285             anySubscribed = true;
0286             volume->update();
0287         }
0288     }
0289 
0290     if (!anySubscribed) {
0291         return;
0292     }
0293 
0294     qint64 elapsed = 0;
0295     if (m_elapsedTimer.isValid()) {
0296         elapsed = m_elapsedTimer.restart();
0297     } else {
0298         m_elapsedTimer.start();
0299     }
0300 #if defined Q_OS_LINUX
0301     QFile diskstats("/proc/diskstats");
0302     if (!diskstats.exists()) {
0303         return;
0304     }
0305     diskstats.open(QIODevice::ReadOnly | QIODevice::Text);
0306     /* procfs-diskstats (See https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats)
0307     The /proc/diskstats file displays the I/O statistics
0308     of block devices. Each line contains the following 14
0309     fields:
0310     - major number
0311     - minor mumber
0312     - device name
0313     - reads completed successfully
0314     - reads merged
0315     - sectors read
0316     - time spent reading (ms)
0317     - writes completed
0318     - writes merged
0319     - sectors written
0320     [...]
0321     */
0322     for (QByteArray line = diskstats.readLine(); !line.isNull(); line = diskstats.readLine()) {
0323         QList<QByteArray> fields = line.simplified().split(' ');
0324         const QString device = QStringLiteral("/dev/%1").arg(QString::fromLatin1(fields[2]));
0325         if (m_volumesByDevice.contains(device)) {
0326             // A sector as reported in diskstats is 512 Bytes, see https://stackoverflow.com/a/38136179
0327             m_volumesByDevice[device]->setBytes(fields[5].toULongLong() * 512, fields[9].toULongLong() * 512, elapsed);
0328         }
0329     }
0330 #elif defined Q_OS_FREEBSD
0331     std::unique_ptr<void, decltype(&geom_stats_snapshot_free)> stats(geom_stats_snapshot_get(), geom_stats_snapshot_free);
0332     gmesh mesh;
0333     geom_gettree(&mesh);
0334     while (devstat *dstat = geom_stats_snapshot_next(stats.get())) {
0335         gident *id = geom_lookupid(&mesh, dstat->id);
0336         if (id && id->lg_what == gident::ISPROVIDER) {
0337             auto provider = static_cast<gprovider*>(id->lg_ptr);
0338             const QString device = QStringLiteral("/dev/%1").arg(QString::fromLatin1(provider->lg_name));
0339             if (m_volumesByDevice.contains(device)) {
0340                 uint64_t bytesRead, bytesWritten;
0341                 devstat_compute_statistics(dstat, nullptr, 0, DSM_TOTAL_BYTES_READ, &bytesRead, DSM_TOTAL_BYTES_WRITE, &bytesWritten, DSM_NONE);
0342                 m_volumesByDevice[device]->setBytes(bytesRead, bytesWritten, elapsed);
0343             }
0344         }
0345     }
0346     geom_deletetree(&mesh);
0347 #endif
0348 }
0349 
0350 K_PLUGIN_CLASS_WITH_JSON(DisksPlugin, "metadata.json")
0351 #include "disks.moc"