File indexing completed on 2024-05-19 05:30:19

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