diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -48,6 +48,7 @@ kusertest.cpp kdelibs4migrationtest.cpp kdelibs4configmigratortest.cpp + kprocesslisttest.cpp LINK_LIBRARIES Qt5::Test KF5::CoreAddons ) diff --git a/autotests/kprocesslisttest.h b/autotests/kprocesslisttest.h new file mode 100644 --- /dev/null +++ b/autotests/kprocesslisttest.h @@ -0,0 +1,36 @@ +/* + * This file is part of the KDE project + * Copyright (C) 2019 David Hallas + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License version 2 as published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. +*/ + +#ifndef KPROCESSLISTTEST_H +#define KPROCESSLISTTEST_H + +#include + +class KProcessListTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testKProcessInfoConstructionAssignment(); + void testProcessInfoList(); + void testProcessInfo(); + void testProcessInfoNotFound(); +}; + +#endif diff --git a/autotests/kprocesslisttest.cpp b/autotests/kprocesslisttest.cpp new file mode 100644 --- /dev/null +++ b/autotests/kprocesslisttest.cpp @@ -0,0 +1,94 @@ +/* + * This file is part of the KDE project + * Copyright (C) 2019 David Hallas + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License version 2 as published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. +*/ + +#include "kprocesslisttest.h" +#include "kprocesslist.h" +#include "kuser.h" +#include +#include +#include + +namespace { + +QString getTestExeName() +{ + static QString testExeName = QCoreApplication::instance()->applicationFilePath().section(QLatin1Char('/'), -1); + return testExeName; +} + +} + +QTEST_MAIN(KProcessListTest) + +void KProcessListTest::testKProcessInfoConstructionAssignment() +{ + KProcessList::KProcessInfo processInfoDefaultConstructed; + QVERIFY(processInfoDefaultConstructed.isValid() == false); + const qint64 pid(42); + const QString name(QStringLiteral("/bin/some_exe")); + const QString user(QStringLiteral("some_user")); + KProcessList::KProcessInfo processInfo(pid, name, user); + QVERIFY(processInfo.isValid() == true); + QCOMPARE(processInfo.pid(), pid); + QCOMPARE(processInfo.name(), name); + QCOMPARE(processInfo.user(), user); + KProcessList::KProcessInfo processInfoCopy(processInfo); + QVERIFY(processInfoCopy.isValid() == true); + QCOMPARE(processInfoCopy.pid(), pid); + QCOMPARE(processInfoCopy.name(), name); + QCOMPARE(processInfoCopy.user(), user); + KProcessList::KProcessInfo processInfoAssignment; + processInfoAssignment = processInfo; + QVERIFY(processInfoAssignment.isValid() == true); + QCOMPARE(processInfoAssignment.pid(), pid); + QCOMPARE(processInfoAssignment.name(), name); + QCOMPARE(processInfoAssignment.user(), user); +} + +void KProcessListTest::testProcessInfoList() +{ + KProcessList::KProcessInfoList processInfoList = KProcessList::processInfoList(); + QVERIFY(processInfoList.empty() == false); + auto testProcessIterator = std::find_if(processInfoList.begin(), processInfoList.end(), [](const KProcessList::KProcessInfo& info) + { + return info.name().endsWith(QStringLiteral("/") + getTestExeName()); + }); + QVERIFY(testProcessIterator != processInfoList.end()); + const auto& processInfo = *testProcessIterator; + QVERIFY(processInfo.isValid() == true); + QVERIFY(processInfo.name().endsWith(QStringLiteral("/") + getTestExeName())); + QCOMPARE(processInfo.pid(), QCoreApplication::applicationPid()); + QCOMPARE(processInfo.user(), KUser().loginName()); +} + +void KProcessListTest::testProcessInfo() +{ + const qint64 testExePid = QCoreApplication::applicationPid(); + KProcessList::KProcessInfo processInfo = KProcessList::processInfo(testExePid); + QVERIFY(processInfo.isValid() == true); + QVERIFY(processInfo.name().endsWith(QStringLiteral("/") + getTestExeName())); + QCOMPARE(processInfo.pid(), testExePid); + QCOMPARE(processInfo.user(), KUser().loginName()); +} + +void KProcessListTest::testProcessInfoNotFound() +{ + KProcessList::KProcessInfo processInfo = KProcessList::processInfo(-1); + QVERIFY(processInfo.isValid() == false); +} diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -45,6 +45,7 @@ set(kcoreaddons_OPTIONAL_SRCS ${kcoreaddons_OPTIONAL_SRCS} text/kmacroexpander_win.cpp + util/kprocesslist_win.cpp util/kshell_win.cpp util/kuser_win.cpp ) @@ -54,6 +55,7 @@ set(kcoreaddons_OPTIONAL_SRCS ${kcoreaddons_OPTIONAL_SRCS} text/kmacroexpander_unix.cpp + util/kprocesslist_unix.cpp util/kuser_unix.cpp util/kshell_unix.cpp ) @@ -86,6 +88,7 @@ util/kdelibs4configmigrator.cpp util/kformat.cpp util/kformatprivate.cpp + util/kprocesslist.cpp util/kshell.cpp ${kcoreaddons_OPTIONAL_SRCS} ${kcoreaddons_QM_LOADER} @@ -199,6 +202,7 @@ KFormat KUser KShell + KProcessList Kdelibs4Migration Kdelibs4ConfigMigrator RELATIVE util @@ -241,6 +245,7 @@ text/ktexttohtml.h text/ktexttohtmlemoticonsinterface.h util/kformat.h + util/kprocesslist.h util/kuser.h util/kshell.h util/kdelibs4migration.h diff --git a/src/lib/util/kprocesslist.h b/src/lib/util/kprocesslist.h new file mode 100644 --- /dev/null +++ b/src/lib/util/kprocesslist.h @@ -0,0 +1,96 @@ +/************************************************************************** +** +** This file is part of the KDE Frameworks +** +** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies). +** Copyright (c) 2019 David Hallas +** +** GNU Lesser General Public License Usage +** +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this file. +** Please review the following information to ensure the GNU Lesser General +** Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** Other Usage +** +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** If you have questions regarding the use of this file, please contact +** Nokia at info@qt.nokia.com. +** +**************************************************************************/ + +#ifndef KPROCESSLIST_H +#define KPROCESSLIST_H + +#include +#include +#include +#include + +namespace KProcessList +{ + +class KProcessInfoPrivate; + +/** + * @brief Contains information about a process. This class is usually not used alone but rather returned by + * processInfoList and processInfo. To check if the data contained in this class is valid use the isValid method. + * @since 5.58 + */ +class KCOREADDONS_EXPORT KProcessInfo { +public: + KProcessInfo(); + KProcessInfo(qint64 pid, const QString &name, const QString &user); + KProcessInfo(const KProcessInfo &other); + ~KProcessInfo(); + KProcessInfo &operator=(const KProcessInfo &other); + /** + * @brief If the KProcessInfo contains valid information. If it returns true the pid, name and user function + * returns valid information, otherwise they return value is undefined. + */ + bool isValid() const; + /** + * @brief The pid of the process + */ + qint64 pid() const; + /** + * @brief The name of the process. The class will try to get the full path to the executable file for the process + * but if it is not available the name of the process will be used instead. + */ + QString name() const; + /** + * @brief The username the process is running under. + */ + QString user() const; +private: + QSharedDataPointer d_ptr; +}; + +using KProcessInfoList = QList; + +/** + * @brief Retrieves the list of currently active processes. + * @since 5.58 + */ +KCOREADDONS_EXPORT KProcessInfoList processInfoList(); + +/** + * @brief Retrieves process information for a specific process-id. If the process is not found a KProcessInfo with + * isValid == false will be returned. + * @param pid The process-id to retrieve information for. + * @since 5.58 + */ +KCOREADDONS_EXPORT KProcessInfo processInfo(qint64 pid); + +} // KProcessList namespace + +#endif // KPROCESSLIST_H diff --git a/src/lib/util/kprocesslist.cpp b/src/lib/util/kprocesslist.cpp new file mode 100644 --- /dev/null +++ b/src/lib/util/kprocesslist.cpp @@ -0,0 +1,105 @@ +/************************************************************************** +** +** This file is part of the KDE Frameworks +** +** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies). +** Copyright (c) 2019 David Hallas +** +** GNU Lesser General Public License Usage +** +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this file. +** Please review the following information to ensure the GNU Lesser General +** Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** Other Usage +** +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** If you have questions regarding the use of this file, please contact +** Nokia at info@qt.nokia.com. +** +**************************************************************************/ + +#include "kprocesslist.h" +#include "kprocesslist_p.h" +#include + +using namespace KProcessList; + +KProcessInfoPrivate::KProcessInfoPrivate() : + valid(false), + pid(-1) +{ +} + +KProcessInfo::KProcessInfo() : + d_ptr(new KProcessInfoPrivate) +{ +} + +KProcessInfo::KProcessInfo(qint64 pid, const QString& name, const QString& user) : + d_ptr(new KProcessInfoPrivate) +{ + d_ptr->valid = true; + d_ptr->pid = pid; + d_ptr->name = name; + d_ptr->user = user; +} + +KProcessInfo::KProcessInfo(const KProcessInfo &other) : + d_ptr(new KProcessInfoPrivate) +{ + *this = other; +} + +KProcessInfo::~KProcessInfo() +{ +} + +KProcessInfo &KProcessInfo::operator=(const KProcessInfo &other) +{ + d_ptr = other.d_ptr; + return *this; +} + +bool KProcessInfo::isValid() const +{ + return d_ptr->valid; +} + +qint64 KProcessInfo::pid() const +{ + return d_ptr->pid; +} + +QString KProcessInfo::name() const +{ + return d_ptr->name; +} + +QString KProcessInfo::user() const +{ + return d_ptr->user; +} + +KProcessInfo KProcessList::processInfo(qint64 pid) +{ + KProcessInfoList processInfoList = KProcessList::processInfoList(); + auto testProcessIterator = std::find_if(processInfoList.begin(), processInfoList.end(), + [pid](const KProcessList::KProcessInfo& info) + { + return info.pid() == pid; + }); + if (testProcessIterator != processInfoList.end()) { + return *testProcessIterator; + } + return KProcessInfo(); +} diff --git a/src/lib/util/kprocesslist_p.h b/src/lib/util/kprocesslist_p.h new file mode 100644 --- /dev/null +++ b/src/lib/util/kprocesslist_p.h @@ -0,0 +1,52 @@ +/************************************************************************** +** +** This file is part of the KDE Frameworks +** +** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies). +** Copyright (c) 2019 David Hallas +** +** GNU Lesser General Public License Usage +** +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this file. +** Please review the following information to ensure the GNU Lesser General +** Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** Other Usage +** +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** If you have questions regarding the use of this file, please contact +** Nokia at info@qt.nokia.com. +** +**************************************************************************/ + +#ifndef KPROCESSLIST_P_H +#define KPROCESSLIST_P_H + +#include +#include "kprocesslist.h" + +namespace KProcessList +{ + +class KProcessInfoPrivate : public QSharedData { +public: + KProcessInfoPrivate(); + + bool valid; + qint64 pid; + QString name; + QString user; +}; + +} // KProcessList namespace + +#endif // KPROCESSLIST_P_H diff --git a/src/lib/util/kprocesslist_unix.cpp b/src/lib/util/kprocesslist_unix.cpp new file mode 100644 --- /dev/null +++ b/src/lib/util/kprocesslist_unix.cpp @@ -0,0 +1,140 @@ +/************************************************************************** +** +** This file is part of the KDE Frameworks +** +** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies). +** Copyright (c) 2019 David Hallas +** +** GNU Lesser General Public License Usage +** +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this file. +** Please review the following information to ensure the GNU Lesser General +** Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** Other Usage +** +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** If you have questions regarding the use of this file, please contact +** Nokia at info@qt.nokia.com. +** +**************************************************************************/ + +#include "kprocesslist.h" + +#include +#include + +using namespace KProcessList; + +namespace { + +bool isUnixProcessId(const QString &procname) +{ + for (int i = 0; i != procname.size(); ++i) { + if (!procname.at(i).isDigit()) + return false; + } + return true; +} + +// Determine UNIX processes by running ps +KProcessInfoList unixProcessListPS() +{ +#ifdef Q_OS_MAC + // command goes last, otherwise it is cut off + static const char formatC[] = "pid state user command"; +#else + static const char formatC[] = "pid,state,user,cmd"; +#endif + KProcessInfoList rc; + QProcess psProcess; + QStringList args; + args << QStringLiteral("-e") << QStringLiteral("-o") << QLatin1String(formatC); + psProcess.start(QStringLiteral("ps"), args); + if (!psProcess.waitForStarted()) + return rc; + psProcess.waitForFinished(); + QByteArray output = psProcess.readAllStandardOutput(); + // Split "457 S+ /Users/foo.app" + const QStringList lines = QString::fromLocal8Bit(output).split(QLatin1Char('\n')); + const int lineCount = lines.size(); + const QChar blank = QLatin1Char(' '); + for (int l = 1; l < lineCount; l++) { // Skip header + const QString line = lines.at(l).simplified(); + // we can't just split on blank as the process name might + // contain them + const int endOfPid = line.indexOf(blank); + const int endOfState = line.indexOf(blank, endOfPid+1); + const int endOfUser = line.indexOf(blank, endOfState+1); + if (endOfPid >= 0 && endOfState >= 0 && endOfUser >= 0) { + qint64 pid = line.left(endOfPid).toUInt(); + QString user = line.mid(endOfState+1, endOfUser-endOfState-1); + QString name = line.right(line.size()-endOfUser-1); + rc.push_back(KProcessInfo(pid, name, user)); + } + } + + return rc; +} + +} // unnamed namespace + +// Determine UNIX processes by reading "/proc". Default to ps if +// it does not exist +KProcessInfoList KProcessList::processInfoList() +{ + const QDir procDir(QStringLiteral("/proc/")); +#ifdef Q_OS_FREEBSD + QString statusFileName(QStringLiteral("/status")); +#else + QString statusFileName(QStringLiteral("/stat")); +#endif + if (!procDir.exists()) + return unixProcessListPS(); + KProcessInfoList rc; + const QStringList procIds = procDir.entryList(); + if (procIds.isEmpty()) + return rc; + for (const QString &procId : procIds) { + if (!isUnixProcessId(procId)) + continue; + QString filename = QStringLiteral("/proc/"); + filename += procId; + filename += statusFileName; + QFile file(filename); + if (!file.open(QIODevice::ReadOnly)) + continue; // process may have exited + + const QStringList data = QString::fromLocal8Bit(file.readAll()).split(QLatin1Char(' ')); + qint64 pid = procId.toUInt(); + QString name = data.at(1); + if (name.startsWith(QLatin1Char('(')) && name.endsWith(QLatin1Char(')'))) { + name.truncate(name.size() - 1); + name.remove(0, 1); + } + // State is element 2 + // PPID is element 3 + QString user = QFileInfo(file).owner(); + file.close(); + + QFile cmdFile(QLatin1String("/proc/") + procId + QLatin1String("/cmdline")); + if (cmdFile.open(QFile::ReadOnly)) { + QByteArray cmd = cmdFile.readAll(); + cmd.replace('\0', ' '); + if (!cmd.isEmpty()) + name = QString::fromLocal8Bit(cmd).trimmed(); + } + cmdFile.close(); + rc.push_back(KProcessInfo(pid, name, user)); + } + return rc; +} diff --git a/src/lib/util/kprocesslist_win.cpp b/src/lib/util/kprocesslist_win.cpp new file mode 100644 --- /dev/null +++ b/src/lib/util/kprocesslist_win.cpp @@ -0,0 +1,132 @@ +/************************************************************************** +** +** This file is part of the KDE Frameworks +** +** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies). +** Copyright (c) 2019 David Hallas +** +** GNU Lesser General Public License Usage +** +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this file. +** Please review the following information to ensure the GNU Lesser General +** Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** Other Usage +** +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** If you have questions regarding the use of this file, please contact +** Nokia at info@qt.nokia.com. +** +**************************************************************************/ + +#include "kprocesslist.h" + +#include + +// Enable Win API of XP SP1 and later +#ifdef Q_OS_WIN +# if !defined(_WIN32_WINNT) +# define _WIN32_WINNT 0x0502 +# endif +# include +# if !defined(PROCESS_SUSPEND_RESUME) // Check flag for MinGW +# define PROCESS_SUSPEND_RESUME (0x0800) +# endif // PROCESS_SUSPEND_RESUME +#endif // Q_OS_WIN + +#include +#include + +// Resolve QueryFullProcessImageNameW out of kernel32.dll due +// to incomplete MinGW import libs and it not being present +// on Windows XP. +static inline BOOL queryFullProcessImageName(HANDLE h, DWORD flags, LPWSTR buffer, DWORD *size) +{ + // Resolve required symbols from the kernel32.dll + typedef BOOL (WINAPI *QueryFullProcessImageNameWProtoType)(HANDLE, DWORD, LPWSTR, PDWORD); + static QueryFullProcessImageNameWProtoType queryFullProcessImageNameW = 0; + if (!queryFullProcessImageNameW) { + QLibrary kernel32Lib(QLatin1String("kernel32.dll"), 0); + if (kernel32Lib.isLoaded() || kernel32Lib.load()) { + queryFullProcessImageNameW + = (QueryFullProcessImageNameWProtoType)kernel32Lib.resolve( + "QueryFullProcessImageNameW"); + } + } + if (!queryFullProcessImageNameW) + return FALSE; + // Read out process + return (*queryFullProcessImageNameW)(h, flags, buffer, size); +} + +struct ProcessInfo { + QString processOwner; +}; + +static inline ProcessInfo processInfo(DWORD processId) +{ + ProcessInfo pi; + HANDLE handle = OpenProcess(PROCESS_QUERY_INFORMATION, TOKEN_READ, processId); + if (handle == INVALID_HANDLE_VALUE) + return pi; + HANDLE processTokenHandle = NULL; + if (!OpenProcessToken(handle, TOKEN_READ, &processTokenHandle) || !processTokenHandle) + return pi; + + DWORD size = 0; + GetTokenInformation(processTokenHandle, TokenUser, NULL, 0, &size); + + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { + QByteArray buf; + buf.resize(size); + PTOKEN_USER userToken = reinterpret_cast(buf.data()); + if (userToken + && GetTokenInformation(processTokenHandle, TokenUser, userToken, size, &size)) { + SID_NAME_USE sidNameUse; + TCHAR user[MAX_PATH] = { 0 }; + DWORD userNameLength = MAX_PATH; + TCHAR domain[MAX_PATH] = { 0 }; + DWORD domainNameLength = MAX_PATH; + + if (LookupAccountSid(NULL, + userToken->User.Sid, + user, + &userNameLength, + domain, + &domainNameLength, + &sidNameUse)) + pi.processOwner = QString::fromUtf16(reinterpret_cast(user)); + } + } + + CloseHandle(processTokenHandle); + CloseHandle(handle); + return pi; +} + +KProcessList GetProcessList() +{ + KProcessList rc; + + PROCESSENTRY32 pe; + pe.dwSize = sizeof(PROCESSENTRY32); + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot == INVALID_HANDLE_VALUE) + return rc; + + for (bool hasNext = Process32First(snapshot, &pe); hasNext; hasNext = Process32Next(snapshot, &pe)) { + const ProcessInfo processInf = processInfo(pe.th32ProcessID); + rc.push_back(KProcessInfo(pe.th32ProcessID, QString::fromUtf16(reinterpret_cast(pe.szExeFile)), processInf.processOwner)); + } + CloseHandle(snapshot); + return rc; +}