#include "processes.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_LOGGING_CATEGORY(lcProcessModel, "ZShell.services.processes", QtInfoMsg) namespace ZShell::services { namespace { QVector splitWhitespace(QByteArrayView s) { QVector out; qsizetype i = 0; const qsizetype n = s.size(); while (i < n) { while (i < n && std::isspace(static_cast(s[i]))) { ++i; } const qsizetype start = i; while (i < n && !std::isspace(static_cast(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 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(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(pid), what); } QVector listProcPids() { QVector pids; const int fd = ::openat(AT_FDCWD, "/proc", O_RDONLY | O_DIRECTORY); if (fd < 0) { return pids; } std::array buf; for (;;) { const ssize_t n = ::getdents64( fd, reinterpret_cast(buf.data()), buf.size()); if (n <= 0) { break; } for (ssize_t off = 0; off < n;) { const auto* d = reinterpret_cast(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(v)); } } off += static_cast(d->d_reclen); } } ::close(fd); return pids; } struct FuzzyText { QString chars; QVector 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(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(text.chars.size()); const int m = static_cast(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& launcherWrappers() { static const QSet s = { "flatpak", "env", "distrobox", "distrobox-enter", "toolbox", "toolbox-enter", "toolbx", "toolbx-enter", }; return s; } const QSet& cmdlineInterpreters() { static const QSet s = { "sh", "bash", "dash", "zsh", "fish", "ash", "python", "python2", "python3", "node", "nodejs", "perl", "ruby", "lua", "php", "java", "dotnet", }; return s; } const QHash& execNameExceptions() { static const QHash 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(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(p.pid); case PpidRole: return static_cast(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(p.rssKb); case ElapsedRole: return p.elapsedSecs; case UidRole: return static_cast(p.uid); case UserRole: return p.username; case IoReadBytesRole: return static_cast(p.ioReadBytes); case IoWriteBytesRole: return static_cast(p.ioWriteBytes); case IconNameRole: return p.iconName; default: return {}; } } QHash 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(mProcesses.size()); } void ProcessModel::refresh() { doRefresh(); } bool ProcessModel::killProcess(qint64 pid, int signal) const { if (pid <= 0) { return false; } return ::kill(static_cast(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 execPathToIcon, QHash 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(n)); } std::optional 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(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(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& cache) { if (const auto it = cache.constFind(uid); it != cache.constEnd()) { return it.value(); } std::array 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 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& pids, QVector& out, QHash& uidCache) { const QHash& prevPidToAll = *ctx.prevPidToAll; const QVector& prevAll = *ctx.prevAll; const QHash& execPathToIcon = *ctx.execPathToIcon; const QHash& 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(stat.starttime) / static_cast(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(deltaTicks) / (ctx.deltaSecs * static_cast(ctx.clockTicksPerSec))) * 100.0; } else { entry.cpuPercent = 0.0; } out.push_back(std::move(entry)); } } ProcessModel::ScanResult ProcessModel::scanProcesses( double uptime, double deltaSecs, QHash prevPidToAll, QVector prevAll, QHash uidCache, long clockTicksPerSec, QHash execPathToIcon, QHash execNameToIcon, QStringList pathDirs, bool desktopEntryIconsChanged, quint64 iconVersion) { Q_UNUSED( pathDirs); // kept for future cgroup fallback; sanitizeExec already ran on GUI thread const QVector 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 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> chunks(nThreads); QVector> outs(nThreads); QVector> uidParts(nThreads); QVector> futures; for (int t = 0; t < nThreads; ++t) { const qsizetype a = static_cast(t) * per; const qsizetype b = qMin(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& 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 prevPidToAll = mPidToAll; QVector prevAll = mAll; QHash uidCache = mUidToName; QHash execPathToIcon = mExecPathToIcon; QHash execNameToIcon = mExecNameToIcon; const long ticks = mClockTicksPerSec; const bool desktopEntryIconsChanged = (m_iconVersion != m_scannedIconVersion); mWatcher = new QFutureWatcher(this); QObject::connect( mWatcher, &QFutureWatcher::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* 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 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 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 shown; shown.reserve(mProcesses.size()); for (const ProcessEntry& e : mProcesses) { shown.insert(e.pid); } QSet 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& 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 kVolatileRoles = { CpuUsageRole, MemoryUsageRole, ElapsedRole, StateRole, SleepingRole, IoReadBytesRole, IoWriteBytesRole, IconNameRole}; if (!mProcesses.isEmpty()) { const int last = static_cast(desired.size()) - 1; Q_EMIT dataChanged(index(0), index(last), kVolatileRoles); } return; } } QSet oldPids; oldPids.reserve(oldCount); for (const auto& e : mProcesses) { oldPids.insert(e.pid); } QSet keepPids; keepPids.reserve(desired.size()); QVector added; for (const auto& e : desired) { keepPids.insert(e.pid); if (!oldPids.contains(e.pid)) { added.append(&e); } } QVector removed; for (qsizetype i = 0; i < oldCount; ++i) { if (!keepPids.contains(mProcesses.at(i).pid)) { removed.append(static_cast(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(mProcesses.size()); beginInsertRows( QModelIndex(), start, start + static_cast(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(), 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(m_model->sortField()); } void Processes::setSortField(SortField field) { m_model->setSortField(static_cast(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 execPathToIcon; QHash 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(field)) { m_model->setSortAscending(!m_model->sortAscending()); } else { m_model->setSortField(static_cast(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