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