File indexing completed on 2024-04-28 05:31:35

0001 /*
0002     SPDX-FileCopyrightText: 2019 David Edmundson <davidedmundson@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "cgroup_data_model.h"
0008 
0009 #include "Formatter.h"
0010 #include "cgroup.h"
0011 #include "extended_process_list.h"
0012 #include "process_attribute.h"
0013 #include "process_data_model.h"
0014 
0015 #include <KLocalizedString>
0016 
0017 #include <QDebug>
0018 #include <QDir>
0019 #include <QMetaEnum>
0020 #include <QTimer>
0021 
0022 #include <algorithm>
0023 #include <filesystem>
0024 
0025 using namespace KSysGuard;
0026 
0027 class KSysGuard::CGroupDataModelPrivate
0028 {
0029 public:
0030     QList<KSysGuard::Process *> processesFor(CGroup *app);
0031 
0032     QSharedPointer<ExtendedProcesses> m_processes;
0033     QTimer *m_updateTimer;
0034     ProcessAttributeModel *m_attributeModel = nullptr;
0035     QHash<QString, KSysGuard::ProcessAttribute *> m_availableAttributes;
0036     QList<KSysGuard::ProcessAttribute *> m_enabledAttributes;
0037 
0038     bool m_available = false;
0039     QString m_root;
0040     QScopedPointer<CGroup> m_rootGroup;
0041 
0042     QList<CGroup *> m_cGroups; // an ordered list of unfiltered cgroups from our root
0043     QHash<QString, CGroup *> m_cgroupMap; // all known cgroups from our root
0044     QHash<QString, CGroup *> m_oldGroups;
0045     QHash<CGroup *, QList<Process *>> m_processMap; // cached mapping of cgroup to list of processes of that group
0046 };
0047 
0048 class GroupNameAttribute : public ProcessAttribute
0049 {
0050 public:
0051     GroupNameAttribute(QObject *parent)
0052         : KSysGuard::ProcessAttribute(QStringLiteral("menuId"), i18nc("@title", "Desktop ID"), parent)
0053     {
0054     }
0055     QVariant cgroupData(CGroup *app, const QList<KSysGuard::Process *> &processes) const override
0056     {
0057         Q_UNUSED(processes)
0058         return app->service()->menuId();
0059     }
0060 };
0061 
0062 class AppIconAttribute : public KSysGuard::ProcessAttribute
0063 {
0064 public:
0065     AppIconAttribute(QObject *parent)
0066         : KSysGuard::ProcessAttribute(QStringLiteral("iconName"), i18nc("@title", "Icon"), parent)
0067     {
0068     }
0069     QVariant cgroupData(CGroup *app, const QList<KSysGuard::Process *> &processes) const override
0070     {
0071         Q_UNUSED(processes)
0072         return app->service()->icon();
0073     }
0074 };
0075 
0076 class AppNameAttribute : public KSysGuard::ProcessAttribute
0077 {
0078 public:
0079     AppNameAttribute(QObject *parent)
0080         : KSysGuard::ProcessAttribute(QStringLiteral("appName"), i18nc("@title", "Name"), parent)
0081     {
0082     }
0083     QVariant cgroupData(CGroup *app, const QList<KSysGuard::Process *> &processes) const override
0084     {
0085         Q_UNUSED(processes)
0086         return app->service()->name();
0087     }
0088 };
0089 
0090 CGroupDataModel::CGroupDataModel(QObject *parent)
0091     : CGroupDataModel(QStringLiteral("/"), parent)
0092 {
0093 }
0094 
0095 CGroupDataModel::CGroupDataModel(const QString &root, QObject *parent)
0096     : QAbstractItemModel(parent)
0097     , d(new CGroupDataModelPrivate)
0098 {
0099     d->m_updateTimer = new QTimer(this);
0100     d->m_processes = ExtendedProcesses::instance();
0101 
0102     QList<ProcessAttribute *> attributes = d->m_processes->attributes();
0103     attributes.reserve(attributes.count() + 3);
0104     attributes.append(new GroupNameAttribute(this));
0105     attributes.append(new AppNameAttribute(this));
0106     attributes.append(new AppIconAttribute(this));
0107     for (auto attr : std::as_const(attributes)) {
0108         d->m_availableAttributes[attr->id()] = attr;
0109     }
0110 
0111     if (CGroup::cgroupSysBasePath().isEmpty()) {
0112         return;
0113     }
0114 
0115     connect(d->m_updateTimer, &QTimer::timeout, this, [this]() {
0116         update();
0117     });
0118     d->m_updateTimer->setInterval(2000);
0119     d->m_updateTimer->start();
0120 
0121     // updateAllProcesses will delete processes that no longer exist, a method that
0122     // can be called by any user of the shared Processes
0123     // so clear out our cache of cgroup -> process whenever anything gets removed
0124     connect(d->m_processes.data(), &Processes::beginRemoveProcess, this, [this]() {
0125         d->m_processMap.clear();
0126     });
0127 
0128     setRoot(root);
0129 }
0130 
0131 CGroupDataModel::~CGroupDataModel()
0132 {
0133 }
0134 
0135 int CGroupDataModel::rowCount(const QModelIndex &parent) const
0136 {
0137     if (parent.isValid()) {
0138         return 0;
0139     }
0140     return d->m_cGroups.count();
0141 }
0142 
0143 QModelIndex CGroupDataModel::index(int row, int column, const QModelIndex &parent) const
0144 {
0145     if (row < 0 || row >= d->m_cGroups.count()) {
0146         return QModelIndex();
0147     }
0148     if (parent.isValid()) {
0149         return QModelIndex();
0150     }
0151     return createIndex(row, column, d->m_cGroups.at(row));
0152 }
0153 
0154 QModelIndex CGroupDataModel::parent(const QModelIndex &child) const
0155 {
0156     Q_UNUSED(child)
0157     return QModelIndex();
0158 }
0159 
0160 int CGroupDataModel::columnCount(const QModelIndex &parent) const
0161 {
0162     if (parent.isValid()) {
0163         return 0;
0164     }
0165 
0166     return d->m_enabledAttributes.count();
0167 }
0168 
0169 QStringList CGroupDataModel::availableAttributes() const
0170 {
0171     return d->m_availableAttributes.keys();
0172 }
0173 
0174 QStringList CGroupDataModel::enabledAttributes() const
0175 {
0176     QStringList rc;
0177     rc.reserve(d->m_enabledAttributes.size());
0178     for (auto attr : std::as_const(d->m_enabledAttributes)) {
0179         rc << attr->id();
0180     }
0181     return rc;
0182 }
0183 
0184 void CGroupDataModel::setEnabledAttributes(const QStringList &enabledAttributes)
0185 {
0186     beginResetModel();
0187 
0188     QList<ProcessAttribute *> unusedAttributes = d->m_enabledAttributes;
0189     d->m_enabledAttributes.clear();
0190 
0191     for (auto attribute : enabledAttributes) {
0192         auto attr = d->m_availableAttributes.value(attribute, nullptr);
0193         if (!attr) {
0194             qWarning() << "Could not find attribute" << attribute;
0195             continue;
0196         }
0197         unusedAttributes.removeOne(attr);
0198         d->m_enabledAttributes << attr;
0199         int columnIndex = d->m_enabledAttributes.count() - 1;
0200 
0201         // reconnect as using the attribute in the lambda makes everything super fast
0202         disconnect(attr, &KSysGuard::ProcessAttribute::dataChanged, this, nullptr);
0203         connect(attr, &KSysGuard::ProcessAttribute::dataChanged, this, [this, columnIndex](KSysGuard::Process *process) {
0204             auto cgroup = d->m_cgroupMap.value(process->cGroup());
0205             if (!cgroup) {
0206                 return;
0207             }
0208             const QModelIndex index = getQModelIndex(cgroup, columnIndex);
0209             Q_EMIT dataChanged(index, index);
0210         });
0211     }
0212 
0213     for (auto unusedAttr : std::as_const(unusedAttributes)) {
0214         disconnect(unusedAttr, &KSysGuard::ProcessAttribute::dataChanged, this, nullptr);
0215     }
0216 
0217     endResetModel();
0218 
0219     Q_EMIT enabledAttributesChanged();
0220 }
0221 
0222 QModelIndex CGroupDataModel::getQModelIndex(CGroup *cgroup, int column) const
0223 {
0224     Q_ASSERT(cgroup);
0225     int row = d->m_cGroups.indexOf(cgroup);
0226     return index(row, column, QModelIndex());
0227 }
0228 
0229 QHash<int, QByteArray> CGroupDataModel::roleNames() const
0230 {
0231     QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
0232     QMetaEnum e = ProcessDataModel::staticMetaObject.enumerator(ProcessDataModel::staticMetaObject.indexOfEnumerator("AdditionalRoles"));
0233 
0234     for (int i = 0; i < e.keyCount(); ++i) {
0235         roles.insert(e.value(i), e.key(i));
0236     }
0237 
0238     return roles;
0239 }
0240 
0241 QVariant CGroupDataModel::data(const QModelIndex &index, int role) const
0242 {
0243     if (!checkIndex(index, CheckIndexOption::IndexIsValid)) {
0244         return QVariant();
0245     }
0246     int attr = index.column();
0247     auto attribute = d->m_enabledAttributes[attr];
0248     switch (role) {
0249     case Qt::DisplayRole:
0250     case ProcessDataModel::FormattedValue: {
0251         KSysGuard::CGroup *app = reinterpret_cast<KSysGuard::CGroup *>(index.internalPointer());
0252         const QVariant value = attribute->cgroupData(app, d->processesFor(app));
0253         return KSysGuard::Formatter::formatValue(value, attribute->unit());
0254     }
0255     case ProcessDataModel::Value: {
0256         KSysGuard::CGroup *app = reinterpret_cast<KSysGuard::CGroup *>(index.internalPointer());
0257         const QVariant value = attribute->cgroupData(app, d->processesFor(app));
0258         return value;
0259     }
0260     case ProcessDataModel::Attribute: {
0261         return attribute->id();
0262     }
0263     case ProcessDataModel::Minimum: {
0264         return attribute->min();
0265     }
0266     case ProcessDataModel::Maximum: {
0267         return attribute->max();
0268     }
0269     case ProcessDataModel::ShortName: {
0270         if (!attribute->shortName().isEmpty()) {
0271             return attribute->shortName();
0272         }
0273         return attribute->name();
0274     }
0275     case ProcessDataModel::Name: {
0276         return attribute->name();
0277     }
0278     case ProcessDataModel::Unit: {
0279         return attribute->unit();
0280     }
0281     case ProcessDataModel::PIDs: {
0282         const auto pids = static_cast<KSysGuard::CGroup *>(index.internalPointer())->pids();
0283         QVariantList result;
0284         std::transform(pids.begin(), pids.end(), std::back_inserter(result), [](pid_t pid) {
0285             return int(pid);
0286         });
0287         return result;
0288     }
0289     }
0290     return QVariant();
0291 }
0292 
0293 QVariant CGroupDataModel::headerData(int section, Qt::Orientation orientation, int role) const
0294 {
0295     if (orientation == Qt::Vertical) {
0296         return QVariant();
0297     }
0298 
0299     if (section < 0 || section >= columnCount()) {
0300         return QVariant();
0301     }
0302 
0303     auto attribute = d->m_enabledAttributes[section];
0304 
0305     switch (role) {
0306     case Qt::DisplayRole:
0307     case ProcessDataModel::ShortName: {
0308         if (!attribute->shortName().isEmpty()) {
0309             return attribute->shortName();
0310         }
0311         return attribute->name();
0312     }
0313     case ProcessDataModel::Name:
0314         return attribute->name();
0315     case ProcessDataModel::Value:
0316     case ProcessDataModel::Attribute: {
0317         return attribute->id();
0318     }
0319     case ProcessDataModel::Unit: {
0320         auto attribute = d->m_enabledAttributes[section];
0321         return attribute->unit();
0322     }
0323     case ProcessDataModel::Minimum: {
0324         return attribute->min();
0325     }
0326     case ProcessDataModel::Maximum: {
0327         return attribute->max();
0328     }
0329     default:
0330         break;
0331     }
0332 
0333     return QVariant();
0334 }
0335 
0336 ProcessAttributeModel *CGroupDataModel::attributesModel()
0337 {
0338     // lazy load
0339     if (!d->m_attributeModel) {
0340         d->m_attributeModel = new KSysGuard::ProcessAttributeModel(d->m_availableAttributes.values().toVector(), this);
0341     }
0342     return d->m_attributeModel;
0343 }
0344 
0345 bool CGroupDataModel::isEnabled() const
0346 {
0347     return d->m_updateTimer->isActive();
0348 }
0349 
0350 void CGroupDataModel::setEnabled(bool enabled)
0351 {
0352     if (enabled) {
0353         d->m_updateTimer->start();
0354         QMetaObject::invokeMethod(
0355             this,
0356             [this] {
0357                 update();
0358             },
0359             Qt::QueuedConnection);
0360     } else {
0361         d->m_updateTimer->stop();
0362     }
0363 }
0364 
0365 QString CGroupDataModel::root() const
0366 {
0367     return d->m_root;
0368 }
0369 
0370 void CGroupDataModel::setRoot(const QString &root)
0371 {
0372     if (root == d->m_root) {
0373         return;
0374     }
0375     d->m_root = root;
0376     Q_EMIT rootChanged();
0377     QMetaObject::invokeMethod(
0378         this,
0379         [this] {
0380             update();
0381         },
0382         Qt::QueuedConnection);
0383 
0384     const QString path = CGroup::cgroupSysBasePath() + root;
0385     bool available = QFile::exists(path);
0386 
0387     if (available) {
0388         d->m_rootGroup.reset(new CGroup(root));
0389     } else {
0390         d->m_rootGroup.reset();
0391     }
0392 
0393     if (available != d->m_available) {
0394         d->m_available = available;
0395         Q_EMIT availableChanged();
0396     }
0397 }
0398 
0399 void CGroupDataModel::update()
0400 {
0401     if (!d->m_rootGroup) {
0402         return;
0403     }
0404 
0405     d->m_oldGroups = d->m_cgroupMap;
0406 
0407     Processes::UpdateFlags flags;
0408     for (auto attribute : std::as_const(d->m_enabledAttributes)) {
0409         flags |= attribute->requiredUpdateFlags();
0410     }
0411 
0412     // In an ideal world we would only the relevant process
0413     // but Ksysguard::Processes doesn't handle that very well
0414     d->m_processes->updateAllProcesses(d->m_updateTimer->interval(), flags);
0415 
0416     update(d->m_rootGroup.data());
0417 
0418     for (auto c : std::as_const(d->m_oldGroups)) {
0419         int row = d->m_cGroups.indexOf(c);
0420         if (row >= 0) {
0421             beginRemoveRows(QModelIndex(), row, row);
0422             d->m_cGroups.removeOne(c);
0423             endRemoveRows();
0424         }
0425         d->m_cgroupMap.remove(c->id());
0426         delete c;
0427     }
0428 }
0429 
0430 bool CGroupDataModel::filterAcceptsCGroup(const QString &id)
0431 {
0432     return id.endsWith(QLatin1String(".service")) || id.endsWith(QLatin1String(".scope"));
0433 }
0434 
0435 void CGroupDataModel::update(CGroup *node)
0436 {
0437     namespace fs = std::filesystem;
0438     const QString path = CGroup::cgroupSysBasePath() + node->id();
0439 
0440     // Update our own stat info
0441     // This may trigger some dataChanged
0442     node->requestPids(this, [this, node](QList<pid_t> pids) {
0443         auto row = d->m_cGroups.indexOf(node);
0444         if (row >= 0) {
0445             d->m_cGroups[row]->setPids(pids);
0446             d->m_processMap.remove(d->m_cGroups[row]);
0447             Q_EMIT dataChanged(index(row, 0, QModelIndex()), index(row, columnCount() - 1, QModelIndex()));
0448         }
0449     });
0450 
0451     std::error_code error;
0452     const fs::directory_iterator iterator(path.toUtf8().data(), error);
0453     if (error) {
0454         return;
0455     }
0456     for (const auto &entry : iterator) {
0457         if (!entry.is_directory()) {
0458             continue;
0459         }
0460         const QString childId = node->id() % QLatin1Char('/') % QString::fromUtf8(entry.path().filename().c_str());
0461         CGroup *childNode = d->m_cgroupMap[childId];
0462         if (!childNode) {
0463             childNode = new CGroup(childId);
0464             d->m_cgroupMap[childNode->id()] = childNode;
0465 
0466             if (filterAcceptsCGroup(childId)) {
0467                 int row = d->m_cGroups.count();
0468                 beginInsertRows(QModelIndex(), row, row);
0469                 d->m_cGroups.append(childNode);
0470                 endInsertRows();
0471             }
0472         }
0473         update(childNode);
0474         d->m_oldGroups.remove(childId);
0475     }
0476 }
0477 
0478 bool CGroupDataModel::isAvailable() const
0479 {
0480     return d->m_available;
0481 }
0482 
0483 QList<Process *> CGroupDataModelPrivate::processesFor(CGroup *app)
0484 {
0485     if (m_processMap.contains(app)) {
0486         return m_processMap.value(app);
0487     }
0488 
0489     QList<Process *> result;
0490     const auto pids = app->pids();
0491     std::for_each(pids.begin(), pids.end(), [this, &result](pid_t pid) {
0492         auto process = m_processes->getProcess(pid);
0493         if (process) {
0494             result.append(process);
0495         }
0496     });
0497 
0498     m_processMap.insert(app, result);
0499 
0500     return result;
0501 }