| 1 | /* |
| 2 | This file is part of the KDE Frameworks |
| 3 | |
| 4 | SPDX-FileCopyrightText: 2011 Nokia Corporation and/or its subsidiary(-ies). |
| 5 | SPDX-FileCopyrightText: 2019 David Hallas <[email protected]> |
| 6 | |
| 7 | SPDX-License-Identifier: LGPL-2.1-only WITH Qt-LGPL-exception-1.1 OR LicenseRef-Qt-Commercial |
| 8 | */ |
| 9 | |
| 10 | /* |
| 11 | * Implementation notes: |
| 12 | * |
| 13 | * This file implements KProcessInfo and KProcessInfoList via Linux /proc |
| 14 | * **or** via ps(1). If there's no /proc, it falls back to ps(1), usually. |
| 15 | * |
| 16 | * Although the code contains #ifdefs for FreeBSD (e.g. for ps(1) command- |
| 17 | * line arguments), FreeBSD should never use this code, only the |
| 18 | * procstat-based code in `kprocesslist_unix_procstat.cpp`. |
| 19 | */ |
| 20 | |
| 21 | #include "kcoreaddons_debug.h" |
| 22 | #include "kprocesslist.h" |
| 23 | |
| 24 | #include <QDebug> |
| 25 | #include <QDir> |
| 26 | #include <QProcess> |
| 27 | |
| 28 | #ifdef Q_OS_FREEBSD |
| 29 | #error This KProcessInfo implementation is not supported on FreeBSD (use procstat) |
| 30 | #endif |
| 31 | |
| 32 | using namespace KProcessList; |
| 33 | |
| 34 | namespace |
| 35 | { |
| 36 | bool isUnixProcessId(const QString &procname) |
| 37 | { |
| 38 | return std::none_of(first: procname.cbegin(), last: procname.cend(), pred: [](const QChar ch) { |
| 39 | return !ch.isDigit(); |
| 40 | }); |
| 41 | } |
| 42 | |
| 43 | // Determine UNIX processes by running ps |
| 44 | KProcessInfoList unixProcessListPS() |
| 45 | { |
| 46 | KProcessInfoList rc; |
| 47 | QProcess psProcess; |
| 48 | const QStringList args{ |
| 49 | #ifdef Q_OS_OPENBSD |
| 50 | QStringLiteral("-ww" ), |
| 51 | QStringLiteral("-x" ), |
| 52 | #endif |
| 53 | QStringLiteral("-e" ), |
| 54 | QStringLiteral("-o" ), |
| 55 | #ifdef Q_OS_MAC |
| 56 | // command goes last, otherwise it is cut off |
| 57 | QStringLiteral("pid state user comm command" ), |
| 58 | #elif defined(Q_OS_OPENBSD) |
| 59 | // On OpenBSD "login" is user who started the process in difference to |
| 60 | // Linux where it is the effective user "ename" name. |
| 61 | QStringLiteral("pid,state,login,comm,args" ), |
| 62 | #else |
| 63 | QStringLiteral("pid,state,user,comm,cmd" ), |
| 64 | #endif |
| 65 | }; |
| 66 | psProcess.start(QStringLiteral("ps" ), arguments: args); |
| 67 | if (!psProcess.waitForStarted()) { |
| 68 | qCWarning(KCOREADDONS_DEBUG) << "Failed to execute ps" << args; |
| 69 | return rc; |
| 70 | } |
| 71 | psProcess.waitForFinished(); |
| 72 | const QByteArray output = psProcess.readAllStandardOutput(); |
| 73 | const QByteArray errorOutput = psProcess.readAllStandardError(); |
| 74 | if (!errorOutput.isEmpty()) { |
| 75 | qCWarning(KCOREADDONS_DEBUG) << "ps said" << errorOutput; |
| 76 | } |
| 77 | // Split "457 S+ /Users/foo.app" |
| 78 | const QStringList lines = QString::fromLocal8Bit(ba: output).split(sep: QLatin1Char('\n')); |
| 79 | const int lineCount = lines.size(); |
| 80 | const QChar blank = QLatin1Char(' '); |
| 81 | for (int l = 1; l < lineCount; l++) { // Skip header |
| 82 | const QString line = lines.at(i: l).simplified(); |
| 83 | // we can't just split on blank as the process name might |
| 84 | // contain them |
| 85 | const int endOfPid = line.indexOf(ch: blank); |
| 86 | const int endOfState = line.indexOf(ch: blank, from: endOfPid + 1); |
| 87 | const int endOfUser = line.indexOf(ch: blank, from: endOfState + 1); |
| 88 | const int endOfName = line.indexOf(ch: blank, from: endOfUser + 1); |
| 89 | |
| 90 | if (endOfPid >= 0 && endOfState >= 0 && endOfUser >= 0) { |
| 91 | const qint64 pid = QStringView(line).left(n: endOfPid).toUInt(); |
| 92 | |
| 93 | QString user = line.mid(position: endOfState + 1, n: endOfUser - endOfState - 1); |
| 94 | QString name = line.mid(position: endOfUser + 1, n: endOfName - endOfUser - 1); |
| 95 | QString command = line.right(n: line.size() - endOfName - 1); |
| 96 | rc.push_back(t: KProcessInfo(pid, command, name, user)); |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | return rc; |
| 101 | } |
| 102 | |
| 103 | bool getProcessInfo(const QString &procId, KProcessInfo &processInfo) |
| 104 | { |
| 105 | if (!isUnixProcessId(procname: procId)) { |
| 106 | return false; |
| 107 | } |
| 108 | QString statusFileName(QStringLiteral("/stat" )); |
| 109 | QString filename = QStringLiteral("/proc/" ); |
| 110 | filename += procId; |
| 111 | filename += statusFileName; |
| 112 | QFile file(filename); |
| 113 | if (!file.open(flags: QIODevice::ReadOnly)) { |
| 114 | return false; // process may have exited |
| 115 | } |
| 116 | |
| 117 | const QStringList data = QString::fromLocal8Bit(ba: file.readAll()).split(sep: QLatin1Char(' ')); |
| 118 | if (data.length() < 2) { |
| 119 | return false; |
| 120 | } |
| 121 | qint64 pid = procId.toUInt(); |
| 122 | QString name = data.at(i: 1); |
| 123 | if (name.startsWith(c: QLatin1Char('(')) && name.endsWith(c: QLatin1Char(')'))) { |
| 124 | name.chop(n: 1); |
| 125 | name.remove(i: 0, len: 1); |
| 126 | } |
| 127 | // State is element 2 |
| 128 | // PPID is element 3 |
| 129 | QString user = QFileInfo(file).owner(); |
| 130 | file.close(); |
| 131 | |
| 132 | QString command = name; |
| 133 | |
| 134 | QFile cmdFile(QLatin1String("/proc/" ) + procId + QLatin1String("/cmdline" )); |
| 135 | if (cmdFile.open(flags: QFile::ReadOnly)) { |
| 136 | QByteArray cmd = cmdFile.readAll(); |
| 137 | |
| 138 | if (!cmd.isEmpty()) { |
| 139 | // extract non-truncated name from cmdline |
| 140 | int zeroIndex = cmd.indexOf(ch: '\0'); |
| 141 | int processNameStart = cmd.lastIndexOf(ch: '/', from: zeroIndex); |
| 142 | if (processNameStart == -1) { |
| 143 | processNameStart = 0; |
| 144 | } else { |
| 145 | processNameStart++; |
| 146 | } |
| 147 | name = QString::fromLocal8Bit(ba: cmd.mid(index: processNameStart, len: zeroIndex - processNameStart)); |
| 148 | |
| 149 | cmd.replace(before: '\0', after: ' '); |
| 150 | command = QString::fromLocal8Bit(ba: cmd).trimmed(); |
| 151 | } |
| 152 | } |
| 153 | cmdFile.close(); |
| 154 | processInfo = KProcessInfo(pid, command, name, user); |
| 155 | return true; |
| 156 | } |
| 157 | |
| 158 | } // unnamed namespace |
| 159 | |
| 160 | // Determine UNIX processes by reading "/proc". Default to ps if |
| 161 | // it does not exist |
| 162 | KProcessInfoList KProcessList::processInfoList() |
| 163 | { |
| 164 | const QDir procDir(QStringLiteral("/proc/" )); |
| 165 | if (!procDir.exists()) { |
| 166 | return unixProcessListPS(); |
| 167 | } |
| 168 | const QStringList procIds = procDir.entryList(filters: QDir::Dirs | QDir::NoDotAndDotDot); |
| 169 | KProcessInfoList rc; |
| 170 | rc.reserve(asize: procIds.size()); |
| 171 | for (const QString &procId : procIds) { |
| 172 | KProcessInfo processInfo; |
| 173 | if (getProcessInfo(procId, processInfo)) { |
| 174 | rc.push_back(t: processInfo); |
| 175 | } |
| 176 | } |
| 177 | return rc; |
| 178 | } |
| 179 | |
| 180 | // Determine UNIX process by reading "/proc". |
| 181 | // |
| 182 | // TODO: Use ps if "/proc" does not exist or is bogus; use code |
| 183 | // from unixProcessListPS() but add a `-p pid` argument. |
| 184 | // |
| 185 | KProcessInfo KProcessList::processInfo(qint64 pid) |
| 186 | { |
| 187 | KProcessInfo processInfo; |
| 188 | getProcessInfo(procId: QString::number(pid), processInfo); |
| 189 | return processInfo; |
| 190 | } |
| 191 | |