Files
z-bar-qt/Plugins/ZShell/Services/processes.cpp
2026-09-30 14:26:59 +02:00

1341 lines
32 KiB
C++

#include "processes.hpp"
#include <algorithm>
#include <array>
#include <cerrno>
#include <cstdio>
#include <dirent.h>
#include <fcntl.h>
#include <pwd.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <qbytearray.h>
#include <qbytearrayview.h>
#include <qdir.h>
#include <qfile.h>
#include <qfuturewatcher.h>
#include <qloggingcategory.h>
#include <qthreadpool.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);
}
// glibc hides getdents64 behind __USE_MISC; declare directly (no _GNU_SOURCE
// in this project).
extern "C" {
struct ProcDirEnt64 {
unsigned long d_ino;
off_t d_off;
unsigned short d_reclen;
unsigned char d_type;
char d_name[1];
};
ssize_t getdents64(int fd, void* dirp, size_t count);
}
// Raw open/read/close instead of QFile: QFSFileEngine issues an extra
// statx per open, and the per-file object churn adds up across ~500
// processes per refresh.
QByteArray readProcFile(const char* path) {
const int fd = ::open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
return {};
}
QByteArray data;
char buf[4096];
for (;;) {
const ssize_t n = ::read(fd, buf, sizeof(buf));
if (n < 0) {
if (errno == EINTR) {
continue;
}
break;
}
if (n == 0) {
break;
}
data.append(buf, static_cast<qsizetype>(n));
}
::close(fd);
return data;
}
void procPath(char* out, size_t size, pid_t pid, const char* what) {
::snprintf(out, size, "/proc/%d/%s", static_cast<int>(pid), what);
}
QVector<pid_t> listProcPids() {
QVector<pid_t> pids;
const int fd = ::openat(AT_FDCWD, "/proc", O_RDONLY | O_DIRECTORY);
if (fd < 0) {
return pids;
}
std::array<char, 256 * 1024> buf;
for (;;) {
const ssize_t n = ::getdents64(
fd, reinterpret_cast<ProcDirEnt64*>(buf.data()), buf.size());
if (n <= 0) {
break;
}
for (ssize_t off = 0; off < n;) {
const auto* d =
reinterpret_cast<const ProcDirEnt64*>(buf.data() + off);
if (d->d_type == DT_DIR) {
bool ok = false;
const long v = QByteArray(d->d_name).toLong(&ok);
if (ok && v > 0) {
pids.push_back(static_cast<pid_t>(v));
}
}
off += static_cast<ssize_t>(d->d_reclen);
}
}
::close(fd);
return pids;
}
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) {
char path[40];
procPath(path, sizeof(path), pid, "exe");
char buf[4096];
const ssize_t n = ::readlink(path, buf, sizeof(buf) - 1);
if (n < 0) {
return {};
}
buf[n] = '\0';
return QString::fromLocal8Bit(buf, static_cast<int>(n));
}
std::optional<ProcessModel::RawStat> ProcessModel::readStat(pid_t pid) {
char path[40];
procPath(path, sizeof(path), pid, "stat");
const QByteArray data = readProcFile(path);
if (data.isEmpty()) {
return std::nullopt;
}
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) {
char path[40];
procPath(path, sizeof(path), pid, "cmdline");
QByteArray data = readProcFile(path);
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) {
char path[40];
procPath(path, sizeof(path), pid, "status");
const QByteArray data = readProcFile(path);
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) {
char path[40];
procPath(path, sizeof(path), pid, "io");
const QByteArray data = readProcFile(path);
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() {
const QByteArray data = readProcFile("/proc/uptime");
if (data.isEmpty()) {
return std::nullopt;
}
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;
}
void ProcessModel::scanChunk(
const ScanContext& ctx,
const QVector<pid_t>& pids,
QVector<ProcessEntry>& out,
QHash<uid_t, QString>& uidCache) {
const QHash<pid_t, int>& prevPidToAll = *ctx.prevPidToAll;
const QVector<ProcessEntry>& prevAll = *ctx.prevAll;
const QHash<QString, QString>& execPathToIcon = *ctx.execPathToIcon;
const QHash<QString, QString>& execNameToIcon = *ctx.execNameToIcon;
for (pid_t pid : pids) {
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 || ctx.iconsChanged) {
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);
// /proc/PID/io is only readable by root or the process owner; skip
// the open when it is guaranteed to fail.
IoInfo io;
if (ctx.selfEuid == 0 || status.uid == ctx.selfEuid) {
io = readIo(pid);
}
entry.ioReadBytes = io.readBytes;
entry.ioWriteBytes = io.writeBytes;
entry.elapsedSecs = std::max(
0.0,
ctx.uptime - (static_cast<double>(stat.starttime) /
static_cast<double>(ctx.clockTicksPerSec)));
entry.lastUtime = stat.utime;
entry.lastStime = stat.stime;
if (samePidInstance && ctx.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) /
(ctx.deltaSecs * static_cast<double>(ctx.clockTicksPerSec))) *
100.0;
} else {
entry.cpuPercent = 0.0;
}
out.push_back(std::move(entry));
}
}
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
const QVector<pid_t> pids = listProcPids();
ScanContext ctx;
ctx.uptime = uptime;
ctx.deltaSecs = deltaSecs;
ctx.prevPidToAll = &prevPidToAll;
ctx.prevAll = &prevAll;
ctx.clockTicksPerSec = clockTicksPerSec;
ctx.execPathToIcon = &execPathToIcon;
ctx.execNameToIcon = &execNameToIcon;
ctx.iconsChanged = desktopEntryIconsChanged;
ctx.selfEuid = ::geteuid();
QVector<ProcessEntry> fresh;
fresh.reserve(pids.size());
// Split the pid list across worker threads; a single pass over /proc is
// syscall-bound (open/read/close per file per process) and serializes
// badly on large systems. Keep one pool thread free for this function,
// which itself runs inside the global pool.
int nThreads =
qMin(16, QThreadPool::globalInstance()->maxThreadCount() - 1);
if (pids.size() < 128 || nThreads < 2) {
nThreads = 1;
}
if (nThreads <= 1) {
scanChunk(ctx, pids, fresh, uidCache);
} else {
const qsizetype per = (pids.size() + nThreads - 1) / nThreads;
QVector<QVector<pid_t>> chunks(nThreads);
QVector<QVector<ProcessEntry>> outs(nThreads);
QVector<QHash<uid_t, QString>> uidParts(nThreads);
QVector<QFuture<void>> futures;
for (int t = 0; t < nThreads; ++t) {
const qsizetype a = static_cast<qsizetype>(t) * per;
const qsizetype b = qMin<qsizetype>(a + per, pids.size());
if (a >= b) {
continue;
}
chunks[t] = pids.mid(a, b - a);
futures.append(
QtConcurrent::run([&ctx, &chunks, &outs, &uidParts, t]() {
scanChunk(ctx, chunks[t], outs[t], uidParts[t]);
}));
}
for (QFuture<void>& f : futures) {
f.waitForFinished();
}
for (qsizetype t = 0; t < nThreads; ++t) {
for (ProcessEntry& e : outs[t]) {
fresh.push_back(std::move(e));
}
for (auto it = uidParts[t].constBegin();
it != uidParts[t].constEnd();
++it) {
uidCache.insert(it.key(), it.value());
}
}
std::sort(
fresh.begin(),
fresh.end(),
[](const ProcessEntry& a, const ProcessEntry& b) {
return a.pid < b.pid;
});
}
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 = mFuzzyCache.begin(); it != mFuzzyCache.end();) {
if (mPidToAll.contains(it.key())) {
++it;
} else {
it = mFuzzyCache.erase(it);
}
}
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) {
// Normalizing (NFD) + lowering per process per tick is the
// expensive part of search; commands are stable for the life of
// a process instance (pid + starttime), so cache the prepared
// text. comm can still change without an exec (prctl), so
// re-prepare when the raw command differs.
FuzzyCacheEntry& fe = mFuzzyCache[src.pid];
if (fe.starttime != src.starttime || fe.command != src.command) {
fe.starttime = src.starttime;
fe.command = src.command;
const FuzzyText prep = fuzzyPrepare(src.command);
fe.commandChars = std::move(prep.chars);
fe.commandClasses = std::move(prep.classes);
fe.hasCommandLine = !src.commandLine.isEmpty() &&
src.commandLine != src.command;
if (fe.hasCommandLine) {
const FuzzyText prepCl = fuzzyPrepare(src.commandLine);
fe.commandLineChars = std::move(prepCl.chars);
fe.commandLineClasses = std::move(prepCl.classes);
} else {
fe.commandLineChars = QString();
fe.commandLineClasses = {};
}
}
const FuzzyText cmdFuzzy{fe.commandChars, fe.commandClasses};
int score = fuzzyQueryScore(cmdFuzzy, terms);
if (fe.hasCommandLine) {
const FuzzyText clFuzzy{
fe.commandLineChars, fe.commandLineClasses};
score = qMax(score, fuzzyQueryScore(clFuzzy, 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