C++ / fmt (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 33s
Rust / build (pull_request) Successful in 48s
Python / verify (pull_request) Successful in 1m42s
Rust / fmt (pull_request) Successful in 26s
Rust / clippy (pull_request) Successful in 45s
1340 lines
32 KiB
C++
1340 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
|