Files
z-bar-qt/Plugins/ZShell/Services/processes.cpp
T

1162 lines
27 KiB
C++

#include "processes.hpp"
#include <algorithm>
#include <array>
#include <pwd.h>
#include <signal.h>
#include <unistd.h>
#include <qbytearray.h>
#include <qbytearrayview.h>
#include <qdir.h>
#include <qfile.h>
#include <qfuturewatcher.h>
#include <qloggingcategory.h>
#include <qregularexpression.h>
#include <qset.h>
#include <QtConcurrent/qtconcurrentrun.h>
Q_LOGGING_CATEGORY(lcProcessModel, "ZShell.services.processes", QtInfoMsg)
namespace ZShell::services {
namespace {
QVector<QByteArrayView> splitWhitespace(QByteArrayView s) {
QVector<QByteArrayView> out;
qsizetype i = 0;
const qsizetype n = s.size();
while (i < n) {
while (i < n && std::isspace(static_cast<unsigned char>(s[i]))) {
++i;
}
const qsizetype start = i;
while (i < n && !std::isspace(static_cast<unsigned char>(s[i]))) {
++i;
}
if (i > start) {
out.push_back(s.mid(start, i - start));
}
}
return out;
}
qint64 toI64(QByteArrayView v) {
return QByteArray(v.data(), v.size()).toLongLong();
}
quint64 toU64(QByteArrayView v) {
return QByteArray(v.data(), v.size()).toULongLong();
}
template <typename T> int cmpVals(const T& a, const T& b) {
return (a < b) - (a > b);
}
struct FuzzyText {
QString chars;
QVector<quint8> classes;
};
constexpr int kFuzzyScoreMatch = 16;
constexpr int kFuzzyScoreGapStart = -3;
constexpr int kFuzzyScoreGapExtension = -1;
constexpr int kFuzzyBonusBoundary = kFuzzyScoreMatch / 2;
constexpr int kFuzzyBonusNonWord = kFuzzyScoreMatch / 2;
constexpr int kFuzzyBonusCamel123 =
kFuzzyBonusBoundary + kFuzzyScoreGapExtension;
constexpr int kFuzzyBonusConsecutive =
-(kFuzzyScoreGapStart + kFuzzyScoreGapExtension);
constexpr int kFuzzyFirstCharMultiplier = 2;
int fuzzyCharClass(QChar c) {
if (c.isLower()) {
return 1;
}
if (c.isUpper()) {
return 2;
}
if (c.isDigit() || c.category() == QChar::Number_Other) {
return 4;
}
if (c.isLetter()) {
return 3;
}
return 0;
}
int fuzzyBonusFor(int prevClass, int currClass) {
if (prevClass == 0 && currClass != 0) {
return kFuzzyBonusBoundary;
}
if ((prevClass == 1 && currClass == 2) ||
(prevClass != 4 && currClass == 4)) {
return kFuzzyBonusCamel123;
}
if (currClass == 0) {
return kFuzzyBonusNonWord;
}
return 0;
}
FuzzyText fuzzyPrepare(const QString& src) {
FuzzyText out;
const QString decomposed = src.normalized(QString::NormalizationForm_D);
out.chars.reserve(decomposed.size());
out.classes.reserve(decomposed.size());
for (int i = 0; i < decomposed.size(); ++i) {
const QChar c = decomposed.at(i);
const uint cp = c.unicode();
if (cp >= 0x0300 && cp <= 0x036F) {
continue;
}
out.classes.append(static_cast<quint8>(fuzzyCharClass(c)));
out.chars.append(c.toLower());
}
return out;
}
int fuzzyTermScore(
const FuzzyText& text, const QString& term, int* matchEnd = nullptr) {
const int n = static_cast<int>(text.chars.size());
const int m = static_cast<int>(term.size());
if (m == 0) {
if (matchEnd) {
*matchEnd = 0;
}
return 0;
}
if (m > n) {
return -1;
}
int pidx = 0;
int sidx = -1;
int eidx = -1;
for (int i = 0; i < n; ++i) {
if (text.chars.at(i) == term.at(pidx)) {
if (sidx < 0) {
sidx = i;
}
++pidx;
if (pidx == m) {
eidx = i + 1;
break;
}
}
}
if (eidx < 0) {
return -1;
}
pidx = m - 1;
for (int i = eidx - 1; i >= sidx; --i) {
if (text.chars.at(i) == term.at(pidx)) {
--pidx;
if (pidx < 0) {
sidx = i;
break;
}
}
}
if (matchEnd) {
*matchEnd = eidx;
}
int score = 0;
pidx = 0;
bool inGap = false;
int consecutive = 0;
int firstBonus = 0;
int prevClass = sidx > 0 ? text.classes.at(sidx - 1) : 0;
for (int i = sidx; i < eidx; ++i) {
const int cc = text.classes.at(i);
if (text.chars.at(i) == term.at(pidx)) {
score += kFuzzyScoreMatch;
int bonus = fuzzyBonusFor(prevClass, cc);
if (consecutive == 0) {
firstBonus = bonus;
} else {
if (bonus == kFuzzyBonusBoundary) {
firstBonus = bonus;
}
bonus = qMax(bonus, qMax(firstBonus, kFuzzyBonusConsecutive));
}
score += (pidx == 0) ? bonus * kFuzzyFirstCharMultiplier : bonus;
inGap = false;
++consecutive;
++pidx;
} else {
score += inGap ? kFuzzyScoreGapExtension : kFuzzyScoreGapStart;
inGap = true;
consecutive = 0;
firstBonus = 0;
}
prevClass = cc;
}
return score;
}
int fuzzyQueryScore(const FuzzyText& text, const QStringList& terms) {
int score = 0;
int from = 0;
for (const QString& term : terms) {
const FuzzyText rest{text.chars.mid(from), text.classes.mid(from)};
int end = 0;
const int s = fuzzyTermScore(rest, term, &end);
if (s < 0) {
return -1;
}
score += s;
from += end;
}
return score;
}
const QSet<QString>& launcherWrappers() {
static const QSet<QString> s = {
"flatpak",
"env",
"distrobox",
"distrobox-enter",
"toolbox",
"toolbox-enter",
"toolbx",
"toolbx-enter",
};
return s;
}
const QSet<QString>& cmdlineInterpreters() {
static const QSet<QString> s = {
"sh",
"bash",
"dash",
"zsh",
"fish",
"ash",
"python",
"python2",
"python3",
"node",
"nodejs",
"perl",
"ruby",
"lua",
"php",
"java",
"dotnet",
};
return s;
}
const QHash<QString, QString>& execNameExceptions() {
static const QHash<QString, QString> s = {
{"firefox-bin", "firefox"},
{"soffice.bin", "libreoffice"},
{"oosplash", "libreoffice"},
{"gnome-terminal-server", "gnome-terminal"},
{"chrome", "google-chrome-stable"},
};
return s;
}
QString sanitizeExec(const QString& execLine, const QStringList& pathDirs) {
const QStringList tokens =
execLine.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
bool skippingLaunchers = true;
for (const QString& rawTok : tokens) {
if (rawTok.startsWith('-') || rawTok.startsWith('%')) {
continue;
}
QString tok = rawTok;
tok.remove('"');
QString resolved;
const QFileInfo fi(tok);
if (fi.isAbsolute()) {
resolved = fi.canonicalFilePath();
} else {
for (const QString& dir : pathDirs) {
QFileInfo cand(QDir(dir).filePath(tok));
if (cand.exists()) {
resolved = cand.canonicalFilePath();
break;
}
}
}
if (resolved.isEmpty()) {
continue;
}
const QString base = QFileInfo(resolved).fileName();
if (skippingLaunchers && launcherWrappers().contains(base)) {
continue;
}
skippingLaunchers = false;
return cmdlineInterpreters().contains(base) ? QString() : resolved;
}
return {};
}
} // namespace
ProcessModel::ProcessModel(QObject* parent) : QAbstractListModel(parent) {
const long ticks = ::sysconf(_SC_CLK_TCK);
if (ticks > 0) {
mClockTicksPerSec = ticks;
}
m_currentUid = ::getuid();
}
int ProcessModel::rowCount(const QModelIndex& parent) const {
if (parent.isValid()) {
return 0;
}
return static_cast<int>(mProcesses.size());
}
QVariant ProcessModel::data(const QModelIndex& index, int role) const {
if (!index.isValid() || index.row() < 0 ||
index.row() >= mProcesses.size()) {
return {};
}
const ProcessEntry& p = mProcesses.at(index.row());
switch (role) {
case PidRole:
return static_cast<qint64>(p.pid);
case PpidRole:
return static_cast<qint64>(p.ppid);
case CommandRole:
return p.command;
case CommandLineRole:
return p.commandLine;
case StateRole:
return QString(QChar::fromLatin1(p.state));
case SleepingRole:
return p.state == 'S' || p.state == 'D';
case CpuUsageRole:
return p.cpuPercent;
case MemoryUsageRole:
return static_cast<qulonglong>(p.rssKb);
case ElapsedRole:
return p.elapsedSecs;
case UidRole:
return static_cast<uint>(p.uid);
case UserRole:
return p.username;
case IoReadBytesRole:
return static_cast<qulonglong>(p.ioReadBytes);
case IoWriteBytesRole:
return static_cast<qulonglong>(p.ioWriteBytes);
case IconNameRole:
return p.iconName;
default:
return {};
}
}
QHash<int, QByteArray> ProcessModel::roleNames() const {
return {
{PidRole, "pid"},
{PpidRole, "ppid"},
{CommandRole, "command"},
{CommandLineRole, "commandLine"},
{StateRole, "state"},
{SleepingRole, "sleeping"},
{CpuUsageRole, "cpuUsage"},
{MemoryUsageRole, "memoryUsage"},
{ElapsedRole, "elapsed"},
{UidRole, "uid"},
{UserRole, "user"},
{IoReadBytesRole, "ioReadBytes"},
{IoWriteBytesRole, "ioWriteBytes"},
{IconNameRole, "iconName"},
};
}
int ProcessModel::count() const {
return static_cast<int>(mProcesses.size());
}
void ProcessModel::refresh() {
doRefresh();
}
bool ProcessModel::killProcess(qint64 pid, int signal) const {
if (pid <= 0) {
return false;
}
return ::kill(static_cast<pid_t>(pid), signal) == 0;
}
QString ProcessModel::searchText() const {
return m_searchText;
}
void ProcessModel::setSearchText(const QString& text) {
if (text == m_searchText) {
return;
}
m_searchText = text;
Q_EMIT searchTextChanged();
applyView();
}
bool ProcessModel::currentUserOnly() const {
return m_currentUserOnly;
}
void ProcessModel::setCurrentUserOnly(bool enabled) {
if (enabled == m_currentUserOnly) {
return;
}
m_currentUserOnly = enabled;
Q_EMIT currentUserOnlyChanged();
applyView();
}
int ProcessModel::sortField() const {
return m_sortField;
}
void ProcessModel::setSortField(int field) {
if (field == m_sortField) {
return;
}
m_sortField = field;
Q_EMIT sortFieldChanged();
applyView();
}
bool ProcessModel::sortAscending() const {
return m_sortAscending;
}
void ProcessModel::setSortAscending(bool ascending) {
if (ascending == m_sortAscending) {
return;
}
m_sortAscending = ascending;
Q_EMIT sortAscendingChanged();
applyView();
}
void ProcessModel::setDesktopEntryIcons(
QHash<QString, QString> execPathToIcon,
QHash<QString, QString> execNameToIcon) {
mExecPathToIcon = std::move(execPathToIcon);
mExecNameToIcon = std::move(execNameToIcon);
++m_iconVersion;
}
QString ProcessModel::resolveProcessExe(pid_t pid) {
const QString path = QStringLiteral("/proc/%1/exe").arg(pid);
const QString target = QFile::symLinkTarget(path);
return target;
}
std::optional<ProcessModel::RawStat> ProcessModel::readStat(pid_t pid) {
QFile f(QStringLiteral("/proc/%1/stat").arg(pid));
if (!f.open(QIODevice::ReadOnly)) {
return std::nullopt;
}
const QByteArray data = f.readAll();
f.close();
const qsizetype openParen = data.indexOf('(');
const qsizetype closeParen = data.lastIndexOf(')');
if (openParen < 0 || closeParen < 0 || closeParen <= openParen) {
return std::nullopt;
}
RawStat out;
out.pid = pid;
out.comm = QString::fromLatin1(
data.mid(openParen + 1, closeParen - openParen - 1));
const QByteArrayView rest =
QByteArrayView(data).mid(closeParen + 1).trimmed();
const auto fields = splitWhitespace(rest);
if (fields.size() < 20) {
return std::nullopt;
}
out.state = fields[0].isEmpty() ? '?' : fields[0][0];
out.ppid = static_cast<pid_t>(toI64(fields[1]));
out.utime = toU64(fields[11]);
out.stime = toU64(fields[12]);
out.starttime = toU64(fields[19]);
return out;
}
QString ProcessModel::readCommandLine(pid_t pid, const QString& fallbackComm) {
QFile f(QStringLiteral("/proc/%1/cmdline").arg(pid));
if (!f.open(QIODevice::ReadOnly)) {
return QStringLiteral("[%1]").arg(fallbackComm);
}
QByteArray data = f.readAll();
f.close();
if (data.isEmpty()) {
return QStringLiteral("[%1]").arg(fallbackComm);
}
while (!data.isEmpty() && data.endsWith('\0')) {
data.chop(1);
}
for (qsizetype i = 0; i < data.size(); ++i) {
if (data[i] == '\0') {
data[i] = ' ';
}
}
return QString::fromUtf8(data);
}
ProcessModel::StatusInfo ProcessModel::readStatus(pid_t pid) {
QFile f(QStringLiteral("/proc/%1/status").arg(pid));
if (!f.open(QIODevice::ReadOnly)) {
return {};
}
const QByteArray data = f.readAll();
f.close();
StatusInfo out;
qsizetype pos = 0;
while (pos < data.size()) {
const qsizetype nl = data.indexOf('\n', pos);
const qsizetype lineLen = nl < 0 ? data.size() - pos : nl - pos;
const QByteArrayView line = QByteArrayView(data).mid(pos, lineLen);
pos = nl < 0 ? data.size() : nl + 1;
if (line.startsWith("VmRSS:")) {
const auto fields = splitWhitespace(line);
if (fields.size() >= 2) {
out.rssKb = toU64(fields[1]);
}
} else if (line.startsWith("Uid:")) {
const auto fields = splitWhitespace(line);
if (fields.size() >= 2) {
out.uid = static_cast<uid_t>(toU64(fields[1]));
}
}
}
return out;
}
ProcessModel::IoInfo ProcessModel::readIo(pid_t pid) {
QFile f(QStringLiteral("/proc/%1/io").arg(pid));
if (!f.open(QIODevice::ReadOnly)) {
return {};
}
const QByteArray data = f.readAll();
f.close();
IoInfo out;
qsizetype pos = 0;
while (pos < data.size()) {
const qsizetype nl = data.indexOf('\n', pos);
const qsizetype lineLen = nl < 0 ? data.size() - pos : nl - pos;
const QByteArrayView line = QByteArrayView(data).mid(pos, lineLen);
pos = nl < 0 ? data.size() : nl + 1;
if (line.startsWith("read_bytes:")) {
const auto fields = splitWhitespace(line);
if (fields.size() >= 2) {
out.readBytes = toU64(fields[1]);
}
} else if (line.startsWith("write_bytes:")) {
const auto fields = splitWhitespace(line);
if (fields.size() >= 2) {
out.writeBytes = toU64(fields[1]);
}
}
}
return out;
}
QString ProcessModel::resolveUsername(uid_t uid, QHash<uid_t, QString>& cache) {
if (const auto it = cache.constFind(uid); it != cache.constEnd()) {
return it.value();
}
std::array<char, 1024> buf{};
struct passwd pwd{};
struct passwd* result = nullptr;
QString name;
if (::getpwuid_r(uid, &pwd, buf.data(), buf.size(), &result) == 0 &&
result != nullptr) {
name = QString::fromLocal8Bit(pwd.pw_name);
} else {
name = QString::number(uid);
}
cache.insert(uid, name);
return name;
}
std::optional<double> ProcessModel::readUptimeSeconds() {
QFile f(QStringLiteral("/proc/uptime"));
if (!f.open(QIODevice::ReadOnly)) {
return std::nullopt;
}
const QByteArray data = f.readAll();
f.close();
const auto fields = splitWhitespace(QByteArrayView(data));
if (fields.isEmpty()) {
return std::nullopt;
}
bool ok = false;
const double uptime =
QByteArray(fields[0].data(), fields[0].size()).toDouble(&ok);
return ok ? std::make_optional(uptime) : std::nullopt;
}
ProcessModel::ScanResult ProcessModel::scanProcesses(
double uptime,
double deltaSecs,
QHash<pid_t, int> prevPidToAll,
QVector<ProcessEntry> prevAll,
QHash<uid_t, QString> uidCache,
long clockTicksPerSec,
QHash<QString, QString> execPathToIcon,
QHash<QString, QString> execNameToIcon,
QStringList pathDirs,
bool desktopEntryIconsChanged,
quint64 iconVersion) {
Q_UNUSED(
pathDirs); // kept for future cgroup fallback; sanitizeExec already ran on GUI thread
QDir procDir(QStringLiteral("/proc"));
const auto entries =
procDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::NoSort);
QVector<ProcessEntry> fresh;
fresh.reserve(entries.size());
for (const QString& name : entries) {
bool isPid = false;
const pid_t pid = static_cast<pid_t>(name.toLong(&isPid));
if (!isPid || pid <= 0) {
continue;
}
const auto statOpt = readStat(pid);
if (!statOpt) {
continue;
}
const RawStat& stat = *statOpt;
ProcessEntry entry;
entry.pid = stat.pid;
entry.ppid = stat.ppid;
entry.command = stat.comm;
entry.state = stat.state;
entry.starttime = stat.starttime;
const auto prevIt = prevPidToAll.constFind(pid);
const bool samePidInstance = prevIt != prevPidToAll.constEnd() &&
prevAll.at(prevIt.value()).starttime ==
stat.starttime;
if (samePidInstance) {
entry.commandLine = prevAll.at(prevIt.value()).commandLine;
} else {
entry.commandLine = readCommandLine(pid, stat.comm);
}
if (!samePidInstance || desktopEntryIconsChanged) {
const QString exePath = resolveProcessExe(pid);
QString iconName;
if (!exePath.isEmpty()) {
iconName = execPathToIcon.value(exePath);
}
if (iconName.isEmpty()) {
QString execName = execNameExceptions().value(
QFileInfo(exePath).fileName(),
QFileInfo(exePath).fileName());
iconName = execNameToIcon.value(execName);
}
if (iconName.isEmpty()) {
QString baseName = stat.comm;
const auto exIt = execNameExceptions().constFind(baseName);
if (exIt != execNameExceptions().constEnd()) {
baseName = exIt.value();
}
iconName = execNameToIcon.value(baseName);
}
entry.iconName = iconName;
} else {
entry.iconName = prevAll.at(prevIt.value()).iconName;
}
const StatusInfo status = readStatus(pid);
entry.rssKb = status.rssKb;
entry.uid = status.uid;
entry.username = resolveUsername(status.uid, uidCache);
const IoInfo io = readIo(pid);
entry.ioReadBytes = io.readBytes;
entry.ioWriteBytes = io.writeBytes;
entry.elapsedSecs = std::max(
0.0,
uptime - (static_cast<double>(stat.starttime) /
static_cast<double>(clockTicksPerSec)));
entry.lastUtime = stat.utime;
entry.lastStime = stat.stime;
if (samePidInstance && deltaSecs > 0.0) {
const ProcessEntry& prev = prevAll.at(prevIt.value());
const quint64 prevTicks = prev.lastUtime + prev.lastStime;
const quint64 nowTicks = stat.utime + stat.stime;
const quint64 deltaTicks =
nowTicks > prevTicks ? nowTicks - prevTicks : 0;
entry.cpuPercent =
(static_cast<double>(deltaTicks) /
(deltaSecs * static_cast<double>(clockTicksPerSec))) *
100.0;
} else {
entry.cpuPercent = 0.0;
}
fresh.push_back(std::move(entry));
}
ScanResult result;
result.entries = std::move(fresh);
result.newUidNames = std::move(uidCache);
result.iconVersion = iconVersion;
return result;
}
void ProcessModel::doRefresh() {
if (mWatcher) {
mRefreshPending = true;
return;
}
const auto uptimeOpt = readUptimeSeconds();
if (!uptimeOpt) {
qCWarning(lcProcessModel, "failed to read /proc/uptime");
return;
}
const double uptime = *uptimeOpt;
const double deltaSecs = mLastUptimeSecs >= 0.0 ? (uptime - mLastUptimeSecs)
: -1.0;
mPendingUptime = uptime;
QHash<pid_t, int> prevPidToAll = mPidToAll;
QVector<ProcessEntry> prevAll = mAll;
QHash<uid_t, QString> uidCache = mUidToName;
QHash<QString, QString> execPathToIcon = mExecPathToIcon;
QHash<QString, QString> execNameToIcon = mExecNameToIcon;
const long ticks = mClockTicksPerSec;
const bool desktopEntryIconsChanged =
(m_iconVersion != m_scannedIconVersion);
mWatcher = new QFutureWatcher<ScanResult>(this);
QObject::connect(
mWatcher,
&QFutureWatcher<ScanResult>::finished,
this,
&ProcessModel::onScanFinished);
mWatcher->setFuture(
QtConcurrent::run(
&ProcessModel::scanProcesses,
uptime,
deltaSecs,
std::move(prevPidToAll),
std::move(prevAll),
std::move(uidCache),
ticks,
std::move(execPathToIcon),
std::move(execNameToIcon),
QStringList{},
desktopEntryIconsChanged,
m_iconVersion));
}
void ProcessModel::onScanFinished() {
QFutureWatcher<ScanResult>* watcher = mWatcher;
mWatcher = nullptr;
if (!watcher) {
return;
}
ScanResult result = watcher->result();
watcher->deleteLater();
mAll = std::move(result.entries);
m_scannedIconVersion = result.iconVersion;
mPidToAll.clear();
mPidToAll.reserve(mAll.size());
for (int i = 0; i < mAll.size(); ++i) {
mPidToAll.insert(mAll.at(i).pid, i);
}
for (auto it = result.newUidNames.constBegin();
it != result.newUidNames.constEnd();
++it) {
mUidToName.insert(it.key(), it.value());
}
mLastUptimeSecs = mPendingUptime;
applyView();
if (mRefreshPending) {
mRefreshPending = false;
doRefresh();
}
}
void ProcessModel::applyView() {
const bool searching = !m_searchText.isEmpty();
QStringList terms;
if (searching) {
terms = m_searchText.split(
QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
for (int i = 0; i < terms.size(); ++i) {
terms[i] = fuzzyPrepare(terms.at(i)).chars;
}
}
QVector<ProcessEntry> filtered;
filtered.reserve(mAll.size());
for (const ProcessEntry& src : mAll) {
if (m_currentUserOnly && src.uid != m_currentUid) {
continue;
}
ProcessEntry entry = src;
if (searching) {
int score = fuzzyQueryScore(fuzzyPrepare(src.command), terms);
if (!src.commandLine.isEmpty() && src.commandLine != src.command) {
score = qMax(
score,
fuzzyQueryScore(fuzzyPrepare(src.commandLine), terms));
}
if (score < 0) {
continue;
}
entry.searchScore = score;
}
filtered.push_back(std::move(entry));
}
QVector<ProcessEntry> desired;
desired.reserve(filtered.size());
if (searching || m_sortField != 0) {
desired = filtered;
std::stable_sort(
desired.begin(),
desired.end(),
[this, searching](const ProcessEntry& a, const ProcessEntry& b) {
if (searching && a.searchScore != b.searchScore) {
return a.searchScore > b.searchScore;
}
const int c = compareEntries(a, b);
if (c != 0) {
return m_sortAscending ? c < 0 : c > 0;
}
return a.pid < b.pid;
});
} else {
QSet<pid_t> shown;
shown.reserve(mProcesses.size());
for (const ProcessEntry& e : mProcesses) {
shown.insert(e.pid);
}
QSet<pid_t> placed;
placed.reserve(filtered.size());
for (const ProcessEntry& e : filtered) {
if (shown.contains(e.pid)) {
desired.push_back(e);
placed.insert(e.pid);
}
}
for (const ProcessEntry& e : filtered) {
if (!placed.contains(e.pid)) {
desired.push_back(e);
}
}
}
converge(desired);
}
void ProcessModel::converge(const QVector<ProcessEntry>& desired) {
const qsizetype oldCount = mProcesses.size();
if (desired.size() == oldCount) {
bool sameOrder = true;
for (qsizetype i = 0; i < desired.size(); ++i) {
if (desired.at(i).pid != mProcesses.at(i).pid) {
sameOrder = false;
break;
}
}
if (sameOrder) {
for (qsizetype i = 0; i < desired.size(); ++i) {
mProcesses[i] = desired.at(i);
}
static const QList<int> kVolatileRoles = {
CpuUsageRole,
MemoryUsageRole,
ElapsedRole,
StateRole,
SleepingRole,
IoReadBytesRole,
IoWriteBytesRole,
IconNameRole};
if (!mProcesses.isEmpty()) {
const int last = static_cast<int>(desired.size()) - 1;
Q_EMIT dataChanged(index(0), index(last), kVolatileRoles);
}
return;
}
}
QSet<pid_t> oldPids;
oldPids.reserve(oldCount);
for (const auto& e : mProcesses) {
oldPids.insert(e.pid);
}
QSet<pid_t> keepPids;
keepPids.reserve(desired.size());
QVector<const ProcessEntry*> added;
for (const auto& e : desired) {
keepPids.insert(e.pid);
if (!oldPids.contains(e.pid)) {
added.append(&e);
}
}
QVector<int> removed;
for (qsizetype i = 0; i < oldCount; ++i) {
if (!keepPids.contains(mProcesses.at(i).pid)) {
removed.append(static_cast<int>(i));
}
}
std::sort(removed.begin(), removed.end(), [](int a, int b) {
return a > b;
});
for (int idx : removed) {
beginRemoveRows(QModelIndex(), idx, idx);
mProcesses.removeAt(idx);
endRemoveRows();
}
if (!added.isEmpty()) {
const int start = static_cast<int>(mProcesses.size());
beginInsertRows(
QModelIndex(), start, start + static_cast<int>(added.size()) - 1);
for (const auto* e : added) {
mProcesses.append(*e);
}
endInsertRows();
}
bool ordered = true;
for (qsizetype i = 0; i < desired.size(); ++i) {
if (mProcesses.at(i).pid != desired.at(i).pid) {
ordered = false;
break;
}
}
if (!ordered) {
mProcesses = desired;
layoutChanged(
QList<QPersistentModelIndex>(),
QAbstractItemModel::VerticalSortHint);
}
if (mProcesses.size() != oldCount) {
Q_EMIT countChanged();
}
}
int ProcessModel::compareEntries(
const ProcessEntry& a, const ProcessEntry& b) const {
switch (m_sortField) {
case 1:
return cmpVals(a.pid, b.pid);
case 2:
return cmpVals(a.ppid, b.ppid);
case 3:
return QString::compare(a.command, b.command, Qt::CaseInsensitive);
case 4:
return QString::compare(
a.commandLine, b.commandLine, Qt::CaseInsensitive);
case 5:
return cmpVals(a.state, b.state);
case 6:
return cmpVals(isSleeping(a), isSleeping(b));
case 7:
return cmpVals(a.cpuPercent, b.cpuPercent);
case 8:
return cmpVals(a.rssKb, b.rssKb);
case 9:
return cmpVals(a.elapsedSecs, b.elapsedSecs);
case 10:
return cmpVals(a.uid, b.uid);
case 11:
return QString::compare(a.username, b.username, Qt::CaseInsensitive);
case 12:
return cmpVals(a.ioReadBytes, b.ioReadBytes);
case 13:
return cmpVals(a.ioWriteBytes, b.ioWriteBytes);
default:
return 0;
}
}
bool ProcessModel::isSleeping(const ProcessEntry& e) {
return e.state == 'S' || e.state == 'D';
}
Processes::Processes(QObject* parent)
: TickingService(parent), m_model(new ProcessModel(this)) {
QObject::connect(
m_model, &ProcessModel::countChanged, this, &Processes::countChanged);
QObject::connect(
m_model,
&ProcessModel::sortFieldChanged,
this,
&Processes::sortFieldChanged);
QObject::connect(
m_model,
&ProcessModel::sortAscendingChanged,
this,
&Processes::sortAscendingChanged);
QObject::connect(
m_model,
&ProcessModel::searchTextChanged,
this,
&Processes::searchTextChanged);
QObject::connect(
m_model,
&ProcessModel::currentUserOnlyChanged,
this,
&Processes::currentUserOnlyChanged);
}
QAbstractItemModel* Processes::list() const {
return m_model;
}
int Processes::count() const {
return m_model->count();
}
Processes::SortField Processes::sortField() const {
return static_cast<SortField>(m_model->sortField());
}
void Processes::setSortField(SortField field) {
m_model->setSortField(static_cast<int>(field));
}
bool Processes::sortAscending() const {
return m_model->sortAscending();
}
void Processes::setSortAscending(bool ascending) {
m_model->setSortAscending(ascending);
}
QString Processes::searchText() const {
return m_model->searchText();
}
void Processes::setSearchText(const QString& text) {
m_model->setSearchText(text);
}
bool Processes::currentUserOnly() const {
return m_model->currentUserOnly();
}
void Processes::setCurrentUserOnly(bool enabled) {
m_model->setCurrentUserOnly(enabled);
}
void Processes::setDesktopEntries(const QVariantList& entries) {
QHash<QString, QString> execPathToIcon;
QHash<QString, QString> execNameToIcon;
execPathToIcon.reserve(entries.size());
execNameToIcon.reserve(entries.size());
const QStringList pathDirs =
qEnvironmentVariable("PATH").split(':', Qt::SkipEmptyParts);
for (const QVariant& v : entries) {
const QVariantMap m = v.toMap();
const QString icon = m.value(QStringLiteral("icon")).toString();
const QStringList command =
m.value(QStringLiteral("command")).toStringList();
if (icon.isEmpty()) {
continue;
}
if (command.isEmpty()) {
continue;
}
const QString execPath = sanitizeExec(command.join(' '), pathDirs);
if (execPath.isEmpty()) {
continue;
}
execPathToIcon.insert(execPath, icon);
execNameToIcon.insert(QFileInfo(execPath).fileName(), icon);
}
m_model->setDesktopEntryIcons(
std::move(execPathToIcon), std::move(execNameToIcon));
}
void Processes::sortBy(SortField field) {
if (m_model->sortField() == static_cast<int>(field)) {
m_model->setSortAscending(!m_model->sortAscending());
} else {
m_model->setSortField(static_cast<int>(field));
m_model->setSortAscending(true);
}
}
void Processes::refresh() {
m_model->refresh();
}
void Processes::tick() {
m_model->refresh();
}
bool Processes::killProcess(qint64 pid, int signal) const {
return m_model->killProcess(pid, signal);
}
} // namespace ZShell::services