Commit beae8176 authored by David Edmundson's avatar David Edmundson Committed by David Edmundson
Browse files

Introduce API for handling cgroups

A model is exposed that groups processes by cgroup and provides grouped
information.

The model can be started and stopped and can be set to follow a given
root. All system units, all users, or a specific slice within that.

Information for all pids is calcuated totalled and included via the
ProcessAttribute helpers.

Cgroups are set to handle applications that follow
https://systemd.io/DESKTOP_ENVIRONMENTS for grouping of applications. A
convenience model ApplicationDataModel subclass exposes this directly.

The exposed model is a 1:1 mapping of ProcessDataModel for easy drop-in.
parent a4a978a3
......@@ -3,6 +3,9 @@ add_definitions(-DTRANSLATION_DOMAIN=\"processcore\")
########### next target ###############
set(ksysguard_LIB_SRCS
application_data_model.cpp
cgroup.cpp
cgroup_data_model.cpp
extended_process_list.cpp
processes.cpp
process.cpp
......@@ -29,6 +32,7 @@ target_link_libraries(processcore
KF5::I18n
KF5::AuthCore
KF5::CoreAddons
KF5::Service
${ZLIB_LIBRARIES}
)
......
/*
Copyright (c) 2019 David Edmundson <davidedmundson@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "application_data_model.h"
#include <KUser>
#include <QDebug>
using namespace KSysGuard;
ApplicationDataModel::ApplicationDataModel(QObject *parent)
: CGroupDataModel(parent)
{
const QString userId = KUserId::currentEffectiveUserId().toString();
setRoot(QStringLiteral("/user.slice/user-%1.slice/user@%1.service").arg(userId));
}
bool ApplicationDataModel::filterAcceptsCGroup(const QString &id)
{
if (!CGroupDataModel::filterAcceptsCGroup(id)) {
return false;
}
// this class is all temporary. In the future as per https://systemd.io/DESKTOP_ENVIRONMENTS/
// all apps will have a managed by a drop-in that puts apps in the app.slice
// when this happens adjust the root above and drop this filterAcceptsCGroup line
return id.contains(QLatin1String("/app")) || (id.contains(QLatin1String("/flatpak")) && id.endsWith(QLatin1String("scope")));
}
/*
Copyright (c) 2019 David Edmundson <davidedmundson@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#pragma once
#include "cgroup_data_model.h"
namespace KSysGuard {
class Q_DECL_EXPORT ApplicationDataModel : public CGroupDataModel
{
Q_OBJECT
public:
ApplicationDataModel(QObject *parent = nullptr);
bool filterAcceptsCGroup(const QString &id) override;
};
}
/*
Copyright (c) 2019 David Edmundson <davidedmundson@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "cgroup.h"
#include <QDebug>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QFile>
#include <QDir>
#include "process.h"
using namespace KSysGuard;
class KSysGuard::CGroupPrivate
{
public:
CGroupPrivate(const QString &_processGroupId)
: processGroupId(_processGroupId)
, service(serviceFromAppId(_processGroupId))
{
}
const QString processGroupId;
const KService::Ptr service;
QVector<Process *> processes;
static KService::Ptr serviceFromAppId(const QString &appId);
static QRegularExpression s_appIdFromProcessGroupPattern;
QVector<Process *> procs;
};
class CGroupSystemInformation
{
public:
CGroupSystemInformation();
QString sysGgroupRoot;
};
Q_GLOBAL_STATIC(CGroupSystemInformation, s_cGroupSystemInformation)
// Flatpak's are currently in a cgroup, but they don't follow the specification
// this has been fixed, but this provides some compatability till that lands
// app vs apps exists because the spec changed.
QRegularExpression CGroupPrivate::s_appIdFromProcessGroupPattern(QStringLiteral("[apps|app|flatpak]-([^-]+)-.*"));
CGroup::CGroup(const QString &id)
: d(new CGroupPrivate(id))
{
}
CGroup::~CGroup()
{
}
QString KSysGuard::CGroup::id() const
{
return d->processGroupId;
}
KService::Ptr KSysGuard::CGroup::service() const
{
return d->service;
}
QVector<Process *> CGroup::processes() const
{
return d->procs;
}
void CGroup::setProcesses(QVector<Process *> procs)
{
d->procs = procs;
}
QVector<pid_t> KSysGuard::CGroup::getPids() const
{
const QString pidFilePath = cgroupSysBasePath() + d->processGroupId + QLatin1String("/cgroup.procs");
QFile pidFile(pidFilePath);
pidFile.open(QFile::ReadOnly | QIODevice::Text);
QTextStream stream(&pidFile);
QVector<pid_t> procs;
QString line = stream.readLine();
while (!line.isNull()) {
procs.append(line.toLong());
line = stream.readLine();
}
return procs;
}
KService::Ptr CGroupPrivate::serviceFromAppId(const QString &processGroup)
{
const int lastSlash = processGroup.lastIndexOf(QLatin1Char('/'));
QString serviceName = processGroup;
if (lastSlash != -1) {
serviceName = processGroup.mid(lastSlash + 1);
}
const QRegularExpressionMatch &appIdMatch = s_appIdFromProcessGroupPattern.match(serviceName);
if (!appIdMatch.isValid() || !appIdMatch.hasMatch()) {
// create a transient service object just to have a sensible name
return KService::Ptr(new KService(serviceName, QString(), QString()));
}
const QString appId = appIdMatch.captured(1);
KService::Ptr service = KService::serviceByMenuId(appId + QStringLiteral(".desktop"));
if (!service) {
service = new KService(appId, QString(), QString());
}
return service;
}
QString CGroup::cgroupSysBasePath()
{
return s_cGroupSystemInformation->sysGgroupRoot;
}
CGroupSystemInformation::CGroupSystemInformation()
{
QDir base(QStringLiteral("/sys/fs/cgroup"));
if (base.exists(QLatin1String("unified"))) {
sysGgroupRoot = base.absoluteFilePath(QStringLiteral("unified"));
return;
}
if (base.exists()) {
sysGgroupRoot = base.absolutePath();
}
}
/*
Copyright (c) 2019 David Edmundson <davidedmundson@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#pragma once
#include <QScopedPointer>
#include <QString>
#include <QVector>
#include <KService>
namespace KSysGuard
{
class Process;
class CGroupPrivate;
/**
* @brief The CGroup class represents a cgroup. This could be a
* service, slice or scope
*/
class Q_DECL_EXPORT CGroup
{
public:
virtual ~CGroup();
/**
* @brief id
* @return The cgroup ID passed from the constructor
*/
QString id() const;
/**
* @brief Returns metadata about the given service
* Only applicable for .service entries and really only useful for applications.
* This KService object is always valid, but may not correspond to a real desktop entry
* @return
*/
KService::Ptr service() const;
/**
* @brief updates and fetches the list of processes associated with the process
* @return A Vector of pids
* @note This reloads the data on every fetch
*/
QVector<pid_t> getPids() const;
/**
* @brief updates and fetches the list of processes associated with the process
* @return A Vector of pids
* @note This reloads the data on every fetch
*/
QVector<Process*> processes() const;
/**
* Returns the base path to exposed cgroup information. Either /sys/fs/cgroup or /sys/fs/cgroup/unified as applicable
* If cgroups are unavailable this will be an empty string
*/
static QString cgroupSysBasePath();
private:
/**
* Create a new cgroup object for a given cgroup entry
* The id is the fully formed separated path, such as
* "system.slice/dbus.service"
*/
CGroup(const QString &id);
/**
* Set the updated processes of this cgroup object.
* Managed by CgroupDataModel exclusively
*/
void setProcesses(QVector<Process*> procs);
QScopedPointer<CGroupPrivate> d;
friend class CGroupDataModel;
friend class CGroupDataModelPrivate;
};
}
/*
Copyright (c) 2019 David Edmundson <davidedmundson@kde.org>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "cgroup_data_model.h"
#include "cgroup.h"
#include "extended_process_list.h"
#include "process_attribute.h"
#include "process_data_model.h"
#include "Formatter.h"
#include <KLocalizedString>
#include <QMetaEnum>
#include <QTimer>
#include <QDebug>
#include <QDir>
#include <algorithm>
using namespace KSysGuard;
class KSysGuard::CGroupDataModelPrivate
{
public:
ExtendedProcesses *m_processes;
QTimer *m_updateTimer;
ProcessAttributeModel *m_attributeModel = nullptr;
QHash<QString, KSysGuard::ProcessAttribute* > m_availableAttributes;
QVector<KSysGuard::ProcessAttribute* > m_enabledAttributes;
QString m_root;
QScopedPointer<CGroup> m_rootGroup;
QVector<CGroup *> m_cGroups; // an ordered list of unfiltered cgroups from our root
QHash<QString, CGroup *> m_cgroupMap; // all known cgroups from our root
QHash<QString, CGroup *> m_oldGroups;
};
class GroupNameAttribute : public ProcessAttribute
{
public:
GroupNameAttribute(QObject *parent) :
KSysGuard::ProcessAttribute(QStringLiteral("menuId"), i18nc("@title", "Desktop ID"), parent) {
}
QVariant cgroupData(CGroup *app) const override {
return app->service()->menuId();
}
};
class AppIconAttribute : public KSysGuard::ProcessAttribute
{
public:
AppIconAttribute(QObject *parent) :
KSysGuard::ProcessAttribute(QStringLiteral("iconName"), i18nc("@title", "Icon"), parent) {
}
QVariant cgroupData(CGroup *app) const override {
return app->service()->icon();
}
};
class AppNameAttribute : public KSysGuard::ProcessAttribute
{
public:
AppNameAttribute(QObject *parent) :
KSysGuard::ProcessAttribute(QStringLiteral("appName"), i18nc("@title", "Name"), parent) {
}
QVariant cgroupData(CGroup *app) const override {
return app->service()->name();
}
};
CGroupDataModel::CGroupDataModel(QObject *parent)
: QAbstractItemModel(parent)
, d(new CGroupDataModelPrivate)
{
d->m_updateTimer = new QTimer(this);
d->m_processes = new ExtendedProcesses(this);
QVector<ProcessAttribute *> attributes = d->m_processes->attributes();
attributes.reserve(attributes.count() + 3);
attributes.append(new GroupNameAttribute(this));
attributes.append(new AppNameAttribute(this));
attributes.append(new AppIconAttribute(this));
for (auto attr : qAsConst(attributes)) {
d->m_availableAttributes[attr->id()] = attr;
}
connect(d->m_updateTimer, &QTimer::timeout, this, [this]() {
update();
});
d->m_updateTimer->setInterval(2000);
d->m_updateTimer->start();
setRoot(QStringLiteral("/"));
}
CGroupDataModel::~CGroupDataModel()
{
}
int CGroupDataModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return d->m_cGroups.count();
}
QModelIndex CGroupDataModel::index(int row, int column, const QModelIndex &parent) const
{
if (row < 0 || row >= d->m_cGroups.count()) {
return QModelIndex();
}
if (parent.isValid()) {
return QModelIndex();
}
return createIndex(row, column, d->m_cGroups.at(row));
}
QModelIndex CGroupDataModel::parent(const QModelIndex &child) const
{
Q_UNUSED(child)
return QModelIndex();
}
int CGroupDataModel::columnCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
return 0;
}
return d->m_enabledAttributes.count();
}
QStringList CGroupDataModel::availableAttributes() const
{
return d->m_availableAttributes.keys();
}
QStringList CGroupDataModel::enabledAttributes() const
{
QStringList rc;
rc.reserve(d->m_enabledAttributes.size());
for (auto attr : qAsConst(d->m_enabledAttributes)) {
rc << attr->id();
}
return rc;
}
void CGroupDataModel::setEnabledAttributes(const QStringList &enabledAttributes)
{
beginResetModel();
QVector<ProcessAttribute*> unusedAttributes = d->m_enabledAttributes;
d->m_enabledAttributes.clear();
for (auto attribute: enabledAttributes) {
auto attr = d->m_availableAttributes.value(attribute, nullptr);
if (!attr) {
qWarning() << "Could not find attribute" << attribute;
continue;
}
unusedAttributes.removeOne(attr);
d->m_enabledAttributes << attr;
int columnIndex = d->m_enabledAttributes.count() - 1;
// reconnect as using the attribute in the lambda makes everything super fast
disconnect(attr, &KSysGuard::ProcessAttribute::dataChanged, this, nullptr);
connect(attr, &KSysGuard::ProcessAttribute::dataChanged, this, [this, columnIndex](KSysGuard::Process *process) {
auto cgroup = d->m_cgroupMap.value(process->cGroup());
if (!cgroup) {
return;
}
const QModelIndex index = getQModelIndex(cgroup, columnIndex);
emit dataChanged(index, index);
});
attr->setEnabled(true);
}
for (auto unusedAttr : qAsConst(unusedAttributes)) {
disconnect(unusedAttr, &KSysGuard::ProcessAttribute::dataChanged, this, nullptr);
unusedAttr->setEnabled(false);
}
endResetModel();
emit enabledAttributesChanged();
}
QModelIndex CGroupDataModel::getQModelIndex(CGroup *cgroup, int column) const
{
Q_ASSERT(cgroup);
int row = d->m_cGroups.indexOf(cgroup);
return index(row, column, QModelIndex());
}
QHash<int, QByteArray> CGroupDataModel::roleNames() const
{
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
QMetaEnum e = ProcessDataModel::staticMetaObject.enumerator(ProcessDataModel::staticMetaObject.indexOfEnumerator("AdditionalRoles"));
for (int i = 0; i < e.keyCount(); ++i) {
roles.insert(e.value(i), e.key(i));
}
return roles;
}
QVariant CGroupDataModel::data(const QModelIndex &index, int role) const
{
if (!checkIndex(index, CheckIndexOption::IndexIsValid)) {
return QVariant();
}
int attr = index.column();
auto attribute = d->m_enabledAttributes[attr];
switch(role) {
case Qt::DisplayRole:
case ProcessDataModel::FormattedValue: {
KSysGuard::CGroup *app = reinterpret_cast< KSysGuard::CGroup* > (index.internalPointer());
const QVariant value = attribute->cgroupData(app);
return KSysGuard::Formatter::formatValue(value, attribute->unit());
}
case ProcessDataModel::Value: {
KSysGuard::CGroup *app = reinterpret_cast< KSysGuard::CGroup* > (index.internalPointer());
const QVariant value = attribute->cgroupData(app);
return value;
}
case ProcessDataModel::Attribute: {
return attribute->id();
}
case ProcessDataModel::Minimum: {
return attribute->min();
}
case ProcessDataModel::Maximum: {
return attribute->max();
}
case ProcessDataModel::ShortName: {
if (!attribute->shortName().isEmpty()) {
return attribute->shortName();
}
return attribute->name();
}
case ProcessDataModel::Name: {
return attribute->name();
}
case ProcessDataModel::Unit: {
return attribute->unit();
}
case ProcessDataModel::PIDs: {
KSysGuard::CGroup *app = reinterpret_cast< KSysGuard::CGroup* > (index.internalPointer());
QVariantList pidList;
std::transform(app->processes().constBegin(), app->processes().constEnd(), std::back_inserter(pidList), [](Process* process) -> QVariant {
return QVariant::fromValue(process->pid());