1 /*
2     SPDX-FileCopyrightText: 2020 David Redondo <kde@david-redondo.de>
3     SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
4 
5     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
6 */
7 
8 #include "disks.h"
9 
10 #if defined(Q_OS_FREEBSD) && !defined(__DragonFly__)
11 #include <devstat.h>
12 #include <libgeom.h>
13 #endif
14 
15 #include <QCoreApplication>
16 #include <QUrl>
17 
18 #include <KIO/FileSystemFreeSpaceJob>
19 #include <KLocalizedString>
20 #include <KPluginFactory>
21 #include <Solid/Block>
22 #include <Solid/Device>
23 #include <Solid/DeviceNotifier>
24 #include <Solid/Predicate>
25 #include <Solid/StorageAccess>
26 #include <Solid/StorageDrive>
27 #include <Solid/StorageVolume>
28 
29 #include <systemstats/AggregateSensor.h>
30 #include <systemstats/SensorContainer.h>
31 #include <systemstats/SensorObject.h>
32 
33 class VolumeObject : public KSysGuard::SensorObject {
34 public:
35     VolumeObject(const Solid::Device &device, KSysGuard::SensorContainer *parent);
36     void update();
37     void setBytes(quint64 read, quint64 written, qint64 elapsedTime);
38 
39     const QString udi;
40     const QString mountPoint;
41 private:
42     static QString idHelper(const Solid::Device &device);
43 
44     KSysGuard::SensorProperty *m_name = nullptr;
45     KSysGuard::SensorProperty *m_total = nullptr;
46     KSysGuard::SensorProperty *m_used = nullptr;
47     KSysGuard::SensorProperty *m_free = nullptr;
48     KSysGuard::SensorProperty *m_readRate = nullptr;
49     KSysGuard::SensorProperty *m_writeRate = nullptr;
50     quint64 m_bytesRead;
51     quint64 m_bytesWritten;
52 };
53 
idHelper(const Solid::Device & device)54 QString VolumeObject::idHelper(const Solid::Device &device)
55 {
56     auto volume = device.as<Solid::StorageVolume>();
57     return volume->uuid().isEmpty() ? volume->label() : volume->uuid();
58 }
59 
60 
VolumeObject(const Solid::Device & device,KSysGuard::SensorContainer * parent)61 VolumeObject::VolumeObject(const Solid::Device &device, KSysGuard::SensorContainer* parent)
62     : SensorObject(idHelper(device), device.displayName(),  parent)
63     , udi(device.udi())
64     , mountPoint(device.as<Solid::StorageAccess>()->filePath())
65 {
66     auto volume = device.as<Solid::StorageVolume>();
67 
68     m_name = new KSysGuard::SensorProperty("name", i18nc("@title", "Name"), device.displayName(), this);
69     m_name->setShortName(i18nc("@title", "Name"));
70     m_name->setVariantType(QVariant::String);
71 
72     m_total = new KSysGuard::SensorProperty("total", i18nc("@title", "Total Space"), volume->size(), this);
73     m_total->setPrefix(name());
74     m_total->setShortName(i18nc("@title Short for 'Total Space'", "Total"));
75     m_total->setUnit(KSysGuard::UnitByte);
76     m_total->setVariantType(QVariant::ULongLong);
77 
78     m_used = new KSysGuard::SensorProperty("used", i18nc("@title", "Used Space"), this);
79     m_used->setPrefix(name());
80     m_used->setShortName(i18nc("@title Short for 'Used Space'", "Used"));
81     m_used->setUnit(KSysGuard::UnitByte);
82     m_used->setVariantType(QVariant::ULongLong);
83     m_used->setMax(volume->size());
84 
85     m_free = new KSysGuard::SensorProperty("free", i18nc("@title", "Free Space"), this);
86     m_free->setPrefix(name());
87     m_free->setShortName(i18nc("@title Short for 'Free Space'", "Free"));
88     m_free->setUnit(KSysGuard::UnitByte);
89     m_free->setVariantType(QVariant::ULongLong);
90     m_free->setMax(volume->size());
91 
92     m_readRate = new KSysGuard::SensorProperty("read", i18nc("@title", "Read Rate"), this);
93     m_readRate->setPrefix(name());
94     m_readRate->setShortName(i18nc("@title Short for 'Read Rate'", "Read"));
95     m_readRate->setUnit(KSysGuard::UnitByteRate);
96     m_readRate->setVariantType(QVariant::Double);
97 
98     m_writeRate = new KSysGuard::SensorProperty("write", i18nc("@title", "Write Rate"), this);
99     m_writeRate->setPrefix(name());
100     m_writeRate->setShortName(i18nc("@title Short for 'Write Rate'", "Write"));
101     m_writeRate->setUnit(KSysGuard::UnitByteRate);
102     m_writeRate->setVariantType(QVariant::Double);
103 
104     auto usedPercent = new KSysGuard::PercentageSensor(this, "usedPercent", i18nc("@title", "Percentage Used"));
105     usedPercent->setPrefix(name());
106     usedPercent->setBaseSensor(m_used);
107 
108     auto freePercent = new KSysGuard::PercentageSensor(this, "freePercent", i18nc("@title", "Percentage Free"));
109     freePercent->setPrefix(name());
110     freePercent->setBaseSensor(m_free);
111 }
112 
update()113 void VolumeObject::update()
114 {
115     auto job = KIO::fileSystemFreeSpace(QUrl::fromLocalFile(mountPoint));
116     connect(job, &KIO::FileSystemFreeSpaceJob::result, this, [this] (KJob *job, KIO::filesize_t size, KIO::filesize_t available) {
117         if (!job->error()) {
118             m_total->setValue(size);
119             m_free->setValue(available);
120             m_free->setMax(size);
121             m_used->setValue(size - available);
122             m_used->setMax(size);
123         }
124     });
125 }
126 
setBytes(quint64 read,quint64 written,qint64 elapsed)127 void VolumeObject::setBytes(quint64 read, quint64 written, qint64 elapsed)
128 {
129     if (elapsed != 0) {
130         double seconds = elapsed / 1000.0;
131         m_readRate->setValue((read - m_bytesRead) / seconds);
132         m_writeRate->setValue((written - m_bytesWritten) / seconds);
133     }
134     m_bytesRead = read;
135     m_bytesWritten = written;
136 }
137 
DisksPlugin(QObject * parent,const QVariantList & args)138 DisksPlugin::DisksPlugin(QObject *parent, const QVariantList &args)
139     : SensorPlugin(parent, args)
140 {
141     auto container = new KSysGuard::SensorContainer("disk", i18n("Disks"), this);
142     auto storageAccesses = Solid::Device::listFromType(Solid::DeviceInterface::StorageAccess);
143     for (const auto &storageAccess : storageAccesses) {
144        addDevice(storageAccess);
145     }
146     connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, [this] (const QString &udi) {
147             addDevice(Solid::Device(udi));
148     });
149     connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved, this, [this, container] (const QString &udi) {
150         Solid::Device device(udi);
151         if (device.isDeviceInterface(Solid::DeviceInterface::StorageAccess)) {
152             auto it = std::find_if(m_volumesByDevice.begin(), m_volumesByDevice.end(), [&udi] (VolumeObject *volume) {
153                 return volume->udi == udi;
154             });
155             if (it != m_volumesByDevice.end()) {
156                 container->removeObject(*it);
157                 m_volumesByDevice.erase(it);
158             }
159         }
160     });
161     addAggregateSensors();
162 #if defined(Q_OS_FREEBSD) && !defined(__DragonFly__)
163     geom_stats_open();
164 #endif
165 }
166 
~DisksPlugin()167 DisksPlugin::~DisksPlugin()
168 {
169 #if defined(Q_OS_FREEBSD) && !defined(__DragonFly__)
170     geom_stats_close();
171 #endif
172 }
173 
addDevice(const Solid::Device & device)174 void DisksPlugin::addDevice(const Solid::Device& device)
175 {
176     auto container = containers()[0];
177     const auto volume = device.as<Solid::StorageVolume>();
178     auto access = device.as<Solid::StorageAccess>();
179     if (!access || !volume || volume->isIgnored()) {
180         return;
181     }
182     Solid::Device drive = device;
183     // Only exlude volumes if we know that they are for sure not on a hard disk
184     while (drive.isValid()) {
185         if (drive.is<Solid::StorageDrive>()) {
186             auto type = drive.as<Solid::StorageDrive>()->driveType();
187             if (type == Solid::StorageDrive::HardDisk) {
188                 break;
189             } else {
190                 return;
191             }
192         }
193         drive = drive.parent();
194     }
195 
196     if (access->filePath() != QString()) {
197         createAccessibleVolumeObject(device);
198     }
199     connect(access, &Solid::StorageAccess::accessibilityChanged, this, [this, container] (bool accessible, const QString &udi) {
200         if (accessible) {
201             Solid::Device device(udi);
202             createAccessibleVolumeObject(device);
203         } else {
204             auto it = std::find_if(m_volumesByDevice.begin(), m_volumesByDevice.end(), [&udi] (VolumeObject *disk) {
205                 return disk->udi == udi;
206             });
207             if (it != m_volumesByDevice.end()) {
208                 container->removeObject(*it);
209                 m_volumesByDevice.erase(it);
210             }
211         }
212     });
213 }
214 
createAccessibleVolumeObject(const Solid::Device & device)215 void DisksPlugin::createAccessibleVolumeObject(const Solid::Device &device)
216 {
217     auto block = device.as<Solid::Block>();
218     auto access = device.as<Solid::StorageAccess>();
219     Q_ASSERT(access->isAccessible());
220     const QString  mountPoint = access->filePath();
221     const bool hasMountPoint = std::any_of(m_volumesByDevice.cbegin(), m_volumesByDevice.cend(), [mountPoint] (const VolumeObject* volume) {
222         return volume->mountPoint == mountPoint;
223     });
224     if (hasMountPoint) {
225         return;
226     }
227     m_volumesByDevice.insert(block->device(), new VolumeObject(device,  containers()[0]));
228 }
229 
addAggregateSensors()230 void DisksPlugin::addAggregateSensors()
231 {
232     auto container = containers()[0];
233     auto allDisks = new KSysGuard::SensorObject("all", i18nc("@title", "All Disks"), container);
234 
235     auto total = new KSysGuard::AggregateSensor(allDisks, "total", i18nc("@title", "Total Space"));
236     total->setShortName(i18nc("@title Short for 'Total Space'", "Total"));
237     total->setUnit(KSysGuard::UnitByte);
238     total->setVariantType(QVariant::ULongLong);
239     total->setMatchSensors(QRegularExpression("^(?!all).*$"), "total");
240 
241     auto free = new KSysGuard::AggregateSensor(allDisks, "free", i18nc("@title", "Free Space"));
242     free->setShortName(i18nc("@title Short for 'Free Space'", "Free"));
243     free->setUnit(KSysGuard::UnitByte);
244     free->setVariantType(QVariant::ULongLong);
245     free->setMax(total->value().toULongLong());
246     free->setMatchSensors(QRegularExpression("^(?!all).*$"), "free");
247 
248     auto used = new KSysGuard::AggregateSensor(allDisks, "used", i18nc("@title", "Used Space"));
249     used->setShortName(i18nc("@title Short for 'Used Space'", "Used"));
250     used->setUnit(KSysGuard::UnitByte);
251     used->setVariantType(QVariant::ULongLong);
252     used->setMax(total->value().toULongLong());
253     used->setMatchSensors(QRegularExpression("^(?!all).*$"), "used");
254 
255     auto readRate = new KSysGuard::AggregateSensor(allDisks, "read", i18nc("@title", "Read Rate"));
256     readRate->setShortName(i18nc("@title Short for 'Read Rate'", "Read"));
257     readRate->setUnit(KSysGuard::UnitByteRate);
258     readRate->setVariantType(QVariant::Double);
259     readRate->setMatchSensors(QRegularExpression("^(?!all).*$"), "read");
260 
261     auto writeRate = new KSysGuard::AggregateSensor(allDisks, "write", i18nc("@title", "Write Rate"));
262     writeRate->setShortName(i18nc("@title Short for 'Write Rate'", "Write"));
263     writeRate->setUnit(KSysGuard::UnitByteRate);
264     writeRate->setVariantType(QVariant::Double);
265     writeRate->setMatchSensors(QRegularExpression("^(?!all).*$"), "write");
266 
267     auto freePercent = new KSysGuard::PercentageSensor(allDisks, "freePercent", i18nc("@title", "Percentage Free"));
268     freePercent->setShortName(i18nc("@title, Short for `Percentage Free", "Free"));
269     freePercent->setBaseSensor(free);
270 
271     auto usedPercent = new KSysGuard::PercentageSensor(allDisks, "usedPercent", i18nc("@title", "Percentage Used"));
272     usedPercent->setShortName(i18nc("@title, Short for `Percentage Used", "Used"));
273     usedPercent->setBaseSensor(used);
274 
275     connect(total, &KSysGuard::SensorProperty::valueChanged, this, [total, free, used] () {
276         free->setMax(total->value().toULongLong());
277         used->setMax(total->value().toULongLong());
278     });
279 }
280 
update()281 void DisksPlugin::update()
282 {
283     bool anySubscribed = false;
284     for (auto volume : m_volumesByDevice) {
285         if (volume->isSubscribed()) {
286             anySubscribed = true;
287             volume->update();
288         }
289     }
290 
291     if (!anySubscribed) {
292         return;
293     }
294 
295     qint64 elapsed = 0;
296     if (m_elapsedTimer.isValid()) {
297         elapsed = m_elapsedTimer.restart();
298     } else {
299         m_elapsedTimer.start();
300     }
301 #if defined Q_OS_LINUX
302     QFile diskstats("/proc/diskstats");
303     if (!diskstats.exists()) {
304         return;
305     }
306     diskstats.open(QIODevice::ReadOnly | QIODevice::Text);
307     /* procfs-diskstats (See https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats)
308     The /proc/diskstats file displays the I/O statistics
309     of block devices. Each line contains the following 14
310     fields:
311     - major number
312     - minor mumber
313     - device name
314     - reads completed successfully
315     - reads merged
316     - sectors read
317     - time spent reading (ms)
318     - writes completed
319     - writes merged
320     - sectors written
321     [...]
322     */
323     for (QByteArray line = diskstats.readLine(); !line.isNull(); line = diskstats.readLine()) {
324         QList<QByteArray> fields = line.simplified().split(' ');
325         const QString device = QStringLiteral("/dev/%1").arg(QString::fromLatin1(fields[2]));
326         if (m_volumesByDevice.contains(device)) {
327             // A sector as reported in diskstats is 512 Bytes, see https://stackoverflow.com/a/38136179
328             m_volumesByDevice[device]->setBytes(fields[5].toULongLong() * 512, fields[9].toULongLong() * 512, elapsed);
329         }
330     }
331 #elif defined Q_OS_FREEBSD && !defined(__DragonFly__)
332     std::unique_ptr<void, decltype(&geom_stats_snapshot_free)> stats(geom_stats_snapshot_get(), geom_stats_snapshot_free);
333     gmesh mesh;
334     geom_gettree(&mesh);
335     while (devstat *dstat = geom_stats_snapshot_next(stats.get())) {
336         gident *id = geom_lookupid(&mesh, dstat->id);
337         if (id && id->lg_what == gident::ISPROVIDER) {
338             auto provider = static_cast<gprovider*>(id->lg_ptr);
339             const QString device = QStringLiteral("/dev/%1").arg(QString::fromLatin1(provider->lg_name));
340             if (m_volumesByDevice.contains(device)) {
341                 uint64_t bytesRead, bytesWritten;
342                 devstat_compute_statistics(dstat, nullptr, 0, DSM_TOTAL_BYTES_READ, &bytesRead, DSM_TOTAL_BYTES_WRITE, &bytesWritten, DSM_NONE);
343                 m_volumesByDevice[device]->setBytes(bytesRead, bytesWritten, elapsed);
344             }
345         }
346     }
347     geom_deletetree(&mesh);
348 #elif defined(__DragonFly__)
349     return; // not implemented
350 #endif
351 }
352 
353 K_PLUGIN_CLASS_WITH_JSON(DisksPlugin, "metadata.json")
354 #include "disks.moc"
355