diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,6 +86,9 @@ add_definitions(-DQT_NO_NARROWING_CONVERSIONS_IN_CONNECT) #add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x060000) +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED TRUE) + add_subdirectory( lsofui ) add_subdirectory( processcore ) add_subdirectory( processui ) diff --git a/processcore/CMakeLists.txt b/processcore/CMakeLists.txt --- a/processcore/CMakeLists.txt +++ b/processcore/CMakeLists.txt @@ -9,6 +9,7 @@ processes_remote_p.cpp processes_base_p.cpp processes_atop_p.cpp + process_controller.cpp ) ecm_qt_declare_logging_category(ksysguard_LIB_SRCS HEADER processcore_debug.h IDENTIFIER LIBKSYSGUARD_PROCESSCORE CATEGORY_NAME org.kde.libksysguard.processcore) @@ -21,6 +22,7 @@ Qt5::Core PRIVATE KF5::I18n + KF5::AuthCore ${ZLIB_LIBRARIES} ) @@ -42,6 +44,7 @@ install( FILES processes.h process.h + process_controller.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/ksysguard/processcore COMPONENT Devel ) diff --git a/processcore/process_controller.h b/processcore/process_controller.h new file mode 100644 --- /dev/null +++ b/processcore/process_controller.h @@ -0,0 +1,182 @@ +/* + * This file is part of KSysGuard. + * Copyright 2019 Arjen Hiemstra + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KSYSGUARD_PROCESSCONTROLLER_H +#define KSYSGUARD_PROCESSCONTROLLER_H + +#include + +#include + +#include "process.h" + +class QWidget; + +/** + * Control processes' priority, scheduling and sending signals. + * + * This class contains methods for sending signals to processes, setting their + * priority and setting their scheduler. It will first try to manipulate the + * processes directly, if that fails, it will use KAuth to try and perform the + * action as root. + */ +namespace KSysGuard { + +class Q_DECL_EXPORT ProcessController : public QObject +{ + Q_OBJECT + +public: + ProcessController(QObject* parent = nullptr); + ~ProcessController(); + + /** + * A signal that can be sent to a process. + */ + enum Signal { + StopSignal, + ContinueSignal, + HangupSignal, + InterruptSignal, + TerminateSignal, + KillSignal, + User1Signal, + User2Signal + }; + Q_ENUM(Signal) + + /** + * What kind of result a call to one of the ProcessController methods had. + */ + enum class Result { + Unknown, ///< Something happened, we just do not know what. + Success, ///< Everything went alright. + InsufficientPermissions, ///< Some processes require privileges to modify and we failed getting those. + NoSuchProcess, ///< Tried to modify a process that no longer exists. + Unsupported, ///< The specified action is not supported. + UserCancelled, ///< The user cancelled the action, usually when requesting privileges. + Error, ///< An error occurred when requesting privileges. + }; + Q_ENUM(Result) + + /** + * The widget used as parent for any dialogs that get shown. + */ + QWidget *widget() const; + /** + * Set the widget to use as parent for dialogs. + * + * \param widget The widget to use. + */ + void setWidget(QWidget *widget); + + /** + * Send a signal to a number of processes. + * + * This will send \p signal to all processes in \p pids. Should a number of + * these be owned by different users, an attempt will be made to send the + * signal as root using KAuth. + * + * \param pids A vector of pids to send the signal to. + * \param signal The signal to send. See Signal for possible values. + * + * \return A Result value that indicates whether the action succeeded. Note + * that a non-Success result may indicate any of the processes in + * \p pids encountered that result. + */ + Q_INVOKABLE Result sendSignal(const QVector &pids, int signal); + /** + * \overload Result sendSignal(const QVector &pids, int signal) + */ + Q_INVOKABLE Result sendSignal(const QList &pids, int signal); + + /** + * Set the priority (niceness) of a number of processes. + * + * This will set the priority of all processes in \p pids to \p priority. + * Should a number of these be owned by different users, an attempt will be + * made to send the signal as root using KAuth. + * + * \param pids A vector of pids to set the priority of. + * \param priority The priority to set. Lower means higher priority. + * + * \return A Result value that indicates whether the action succeeded. Note + * that a non-Success result may indicate any of the processes in + * \p pids encountered that result. + */ + Q_INVOKABLE Result setPriority(const QVector &pids, int priority); + /** + * \overload Result setPriority(const QVector &pids, int priority) + */ + Q_INVOKABLE Result setPriority(const QList &pids, int priority); + + /** + * Set the CPU scheduling policy and priority of a number of processes. + * + * This will set the CPU scheduling policy and priority of all processes in + * \p pids to \p scheduler and \p priority. Should a number of these be + * owned by different users, an attempt will be made to send the signal as + * root using KAuth. + * + * \param pids A vector of pids to set the scheduler of. + * \param scheduler The scheduling policy to use. + * \param priority The priority to set. Lower means higher priority. + * + * \return A Result value that indicates whether the action succeeded. Note + * that a non-Success result may indicate any of the processes in + * \p pids encountered that result. + */ + Q_INVOKABLE Result setCPUScheduler(const QVector &pids, Process::Scheduler scheduler, int priority); + /** + * \overload Result setCPUScheduler(const QVector &pids, Process::Scheduler scheduler, int priority) + */ + Q_INVOKABLE Result setCPUScheduler(const QList &pids, Process::Scheduler scheduler, int priority); + + /** + * Set the IO scheduling policy and priority of a number of processes. + * + * This will set the IO scheduling policy and priority of all processes in + * \p pids to \p priorityClass and \p priority. Should a number of these be + * owned by different users, an attempt will be made to send the signal as + * root using KAuth. + * + * \param pids A vector of pids to set the scheduler of. + * \param priorityClass The scheduling policy to use. + * \param priority The priority to set. Lower means higher priority. + * + * \return A Result value that indicates whether the action succeeded. Note + * that a non-Success result may indicate any of the processes in + * \p pids encountered that result. + */ + Q_INVOKABLE Result setIOScheduler(const QVector &pids, Process::IoPriorityClass priorityClass, int priority); + /** + * \overload Result setIOScheduler(const QVector &pids, Process::IoPriorityClass priorityClass, int priority) + */ + Q_INVOKABLE Result setIOScheduler(const QList &pids, Process::IoPriorityClass priorityClass, int priority); + +private: + class Private; + const std::unique_ptr d; +}; + +} + +#endif // KSYSGUARD_PROCESSCONTROLLER_H diff --git a/processcore/process_controller.cpp b/processcore/process_controller.cpp new file mode 100644 --- /dev/null +++ b/processcore/process_controller.cpp @@ -0,0 +1,251 @@ +/* + * This file is part of KSysGuard. + * Copyright 2019 Arjen Hiemstra + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "process_controller.h" + +#include + +#include +#include + +#include "processes_local_p.h" +#include "processcore_debug.h" + +using namespace KSysGuard; + +struct ApplyResult +{ + ProcessController::Result resultCode = ProcessController::Result::Success; + QVector unchanged; +}; + +class ProcessController::Private +{ +public: + ApplyResult applyToPids(const QVector &pids, const std::function &function); + ProcessController::Result runKAuthAction(const QString &actionId, const QVector &pids, const QVariantMap &options); + QVector listToVector(const QList &list); + + QWidget *widget; + + // Note: This instance is only to have access to the platform-specific code + // for sending signals, setting priority etc. Therefore, it should never be + // used to access information about processes. + static std::unique_ptr localProcesses; +}; + +std::unique_ptr ProcessController::Private::localProcesses; + +ProcessController::ProcessController(QObject* parent) + : QObject(parent), d(new Private) +{ + if (!d->localProcesses) { + d->localProcesses = std::make_unique(); + } +} + +KSysGuard::ProcessController::~ProcessController() +{ + // Empty destructor needed for std::unique_ptr to incomplete class. +} + +QWidget * KSysGuard::ProcessController::widget() const +{ + return d->widget; +} + +void KSysGuard::ProcessController::setWidget(QWidget* widget) +{ + d->widget = widget; +} + +ProcessController::Result ProcessController::sendSignal(const QVector& pids, int signal) +{ + qCDebug(LIBKSYSGUARD_PROCESSCORE) << "Sending signal" << signal << "to" << pids; + + auto result = d->applyToPids(pids, [this, signal](int pid) { return d->localProcesses->sendSignal(pid, signal); }); + if (result.unchanged.isEmpty()) { + return result.resultCode; + } + + return d->runKAuthAction( + QStringLiteral("org.kde.ksysguard.processlisthelper.sendsignal"), + result.unchanged, + { {QStringLiteral("signal"), signal} } + ); +} + +KSysGuard::ProcessController::Result KSysGuard::ProcessController::sendSignal(const QList& pids, int signal) +{ + return sendSignal(d->listToVector(pids), signal); +} + +ProcessController::Result ProcessController::setPriority(const QVector& pids, int priority) +{ + auto result = d->applyToPids(pids, [this, priority](int pid) { return d->localProcesses->setNiceness(pid, priority); }); + if (result.unchanged.isEmpty()) { + return result.resultCode; + } + + return d->runKAuthAction( + QStringLiteral("org.kde.ksysguard.processlisthelper.renice"), + result.unchanged, + { { QStringLiteral("nicevalue"), priority } } + ); +} + +KSysGuard::ProcessController::Result KSysGuard::ProcessController::setPriority(const QList& pids, int priority) +{ + return setPriority(d->listToVector(pids), priority); +} + +ProcessController::Result ProcessController::setCPUScheduler(const QVector& pids, Process::Scheduler scheduler, int priority) +{ + if (scheduler == KSysGuard::Process::Other || scheduler == KSysGuard::Process::Batch) { + priority = 0; + } + + auto result = d->applyToPids(pids, [this, scheduler, priority](int pid) { + return d->localProcesses->setScheduler(pid, scheduler, priority); + }); + if (result.unchanged.isEmpty()) { + return result.resultCode; + } + + return d->runKAuthAction( + QStringLiteral("org.kde.ksysguard.processlisthelper.changecpuscheduler"), + result.unchanged, + {{QStringLiteral("cpuScheduler"), scheduler}, {QStringLiteral("cpuSchedulerPriority"), priority}} + ); +} + +KSysGuard::ProcessController::Result KSysGuard::ProcessController::setCPUScheduler(const QList& pids, Process::Scheduler scheduler, int priority) +{ + return setCPUScheduler(d->listToVector(pids), scheduler, priority); +} + +ProcessController::Result ProcessController::setIOScheduler(const QVector& pids, Process::IoPriorityClass priorityClass, int priority) +{ + if (!d->localProcesses->supportsIoNiceness()) { + return Result::Unsupported; + } + + if (priorityClass == KSysGuard::Process::None) { + priorityClass = KSysGuard::Process::BestEffort; + } + + if (priorityClass == KSysGuard::Process::Idle) { + priority = 0; + } + + auto result = d->applyToPids(pids, [this, priorityClass, priority](int pid) { + return d->localProcesses->setIoNiceness(pid, priorityClass, priority); + }); + if (result.unchanged.isEmpty()) { + return result.resultCode; + } + + return d->runKAuthAction( + QStringLiteral("org.kde.ksysguard.processlisthelper.changeioscheduler"), + result.unchanged, + {{QStringLiteral("ioScheduler"), priorityClass}, {QStringLiteral("ioSchedulerPriority"), priority}} + ); +} + +KSysGuard::ProcessController::Result KSysGuard::ProcessController::setIOScheduler(const QList& pids, Process::IoPriorityClass priorityClass, int priority) +{ + return setIOScheduler(d->listToVector(pids), priorityClass, priority); +} + +ApplyResult KSysGuard::ProcessController::Private::applyToPids(const QVector& pids, const std::function& function) +{ + ApplyResult result; + for (auto pid : pids) { + auto success = function(pid); + if (!success + && (localProcesses->errorCode == KSysGuard::Processes::InsufficientPermissions + || localProcesses->errorCode == KSysGuard::Processes::Unknown)) { + result.unchanged << pid; + result.resultCode = Result::InsufficientPermissions; + } else if (result.resultCode == Result::Success) { + switch (localProcesses->errorCode) { + case Processes::InvalidPid: + case Processes::ProcessDoesNotExistOrZombie: + case Processes::InvalidParameter: + result.resultCode = Result::NoSuchProcess; + break; + case Processes::NotSupported: + result.resultCode = Result::Unsupported; + break; + default: + result.resultCode = Result::Unknown; + break; + } + } + } + return result; +} + + +ProcessController::Result ProcessController::Private::runKAuthAction(const QString& actionId, const QVector &pids, const QVariantMap& options) +{ + KAuth::Action action(actionId); + if (!action.isValid()) { + qCWarning(LIBKSYSGUARD_PROCESSCORE) << "Executing KAuth action" << actionId << "failed because it is an invalid action"; + return Result::InsufficientPermissions; + } + action.setParentWidget(widget); + action.setHelperId(QStringLiteral("org.kde.ksysguard.processlisthelper")); + + const int processCount = pids.count(); + for (int i = 0; i < processCount; ++i) { + action.addArgument(QStringLiteral("pid%1").arg(i), pids.at(i)); + } + action.addArgument(QStringLiteral("pidcount"), processCount); + + for (auto itr = options.cbegin(); itr != options.cend(); ++itr) { + action.addArgument(itr.key(), itr.value()); + } + + KAuth::ExecuteJob *job = action.execute(); + if(job->exec()) { + return Result::Success; + } else { + if (job->error() == KAuth::ActionReply::UserCancelledError) { + return Result::UserCancelled; + } + + if (job->error() == KAuth::ActionReply::AuthorizationDeniedError) { + return Result::InsufficientPermissions; + } + + qCWarning(LIBKSYSGUARD_PROCESSCORE) << "Executing KAuth action" << actionId << "failed with error code" << job->error(); + qCWarning(LIBKSYSGUARD_PROCESSCORE) << job->errorString(); + return Result::Error; + } +} + +QVector KSysGuard::ProcessController::Private::listToVector(const QList& list) +{ + QVector vector; + std::transform(list.cbegin(), list.cend(), std::back_inserter(vector), [](long long entry) { return entry; }); + return vector; +} diff --git a/processui/ksysguardprocesslist.cpp b/processui/ksysguardprocesslist.cpp --- a/processui/ksysguardprocesslist.cpp +++ b/processui/ksysguardprocesslist.cpp @@ -63,6 +63,7 @@ #include "ReniceDlg.h" #include "ui_ProcessWidgetUI.h" #include "scripting.h" +#include "process_controller.h" #include #include @@ -248,6 +249,8 @@ */ ProcessFilter mFilterModel; + KSysGuard::ProcessController *mProcessController = nullptr; + /** The graphical user interface for this process list widget, auto-generated by Qt Designer */ Ui::ProcessWidget *mUi; @@ -298,6 +301,9 @@ qRegisterMetaType >(); qDBusRegisterMetaType >(); + d->mProcessController = new KSysGuard::ProcessController(this); + d->mProcessController->setWidget(window()); + d->mUpdateIntervalMSecs = 0; //Set process to not update manually by default d->mUi->setupUi(this); d->mFilterModel.setSourceModel(&d->mModel); @@ -1129,29 +1135,13 @@ bool KSysGuardProcessList::reniceProcesses(const QList &pids, int niceValue) { - QList< long long> unreniced_pids; - for (int i = 0; i < pids.size(); ++i) { - bool success = d->mModel.processController()->setNiceness(pids.at(i), niceValue); - if(!success) { - unreniced_pids << pids.at(i); - } - } - if(unreniced_pids.isEmpty()) return true; //All processes were reniced successfully - if(!d->mModel.isLocalhost()) return false; //We can't use kauth to renice non-localhost processes - - - KAuth::Action action(QStringLiteral("org.kde.ksysguard.processlisthelper.renice")); - action.setParentWidget(window()); - d->setupKAuthAction( action, unreniced_pids); - action.addArgument(QStringLiteral("nicevalue"), niceValue); - KAuth::ExecuteJob *job = action.execute(); - - if (job->exec()) { + auto result = d->mProcessController->setPriority(pids, niceValue); + if (result == KSysGuard::ProcessController::Result::Success) { updateList(); - } else if (!job->exec()) { + return true; + } else if (result == KSysGuard::ProcessController::Result::Error) { KMessageBox::sorry(this, i18n("You do not have the permission to renice the process and there " - "was a problem trying to run as root. Error %1 %2", job->error(), job->errorString())); - return false; + "was a problem trying to run as root.")); } return true; } @@ -1289,95 +1279,44 @@ bool KSysGuardProcessList::changeIoScheduler(const QList< long long> &pids, KSysGuard::Process::IoPriorityClass newIoSched, int newIoSchedPriority) { - if(newIoSched == KSysGuard::Process::None) newIoSched = KSysGuard::Process::BestEffort; - if(newIoSched == KSysGuard::Process::Idle) newIoSchedPriority = 0; - QList< long long> unchanged_pids; - for (int i = 0; i < pids.size(); ++i) { - bool success = d->mModel.processController()->setIoNiceness(pids.at(i), newIoSched, newIoSchedPriority); - if(!success) { - unchanged_pids << pids.at(i); - } - } - if(unchanged_pids.isEmpty()) return true; - if(!d->mModel.isLocalhost()) return false; //We can't use kauth to affect non-localhost processes - - KAuth::Action action(QStringLiteral("org.kde.ksysguard.processlisthelper.changeioscheduler")); - action.setParentWidget(window()); - - d->setupKAuthAction( action, unchanged_pids); - action.addArgument(QStringLiteral("ioScheduler"), (int)newIoSched); - action.addArgument(QStringLiteral("ioSchedulerPriority"), newIoSchedPriority); - - KAuth::ExecuteJob *job = action.execute(); - - if (job->exec()) { + auto result = d->mProcessController->setIOScheduler(pids, newIoSched, newIoSchedPriority); + if (result == KSysGuard::ProcessController::Result::Success) { updateList(); - } else if (!job->exec()) { + return true; + } else if (result == KSysGuard::ProcessController::Result::Error) { KMessageBox::sorry(this, i18n("You do not have the permission to change the I/O priority of the process and there " - "was a problem trying to run as root. Error %1 %2", job->error(), job->errorString())); - return false; + "was a problem trying to run as root.")); } - return true; + + return false; } bool KSysGuardProcessList::changeCpuScheduler(const QList< long long> &pids, KSysGuard::Process::Scheduler newCpuSched, int newCpuSchedPriority) { - if(newCpuSched == KSysGuard::Process::Other || newCpuSched == KSysGuard::Process::Batch) newCpuSchedPriority = 0; - QList< long long> unchanged_pids; - for (int i = 0; i < pids.size(); ++i) { - bool success = d->mModel.processController()->setScheduler(pids.at(i), newCpuSched, newCpuSchedPriority); - if(!success) { - unchanged_pids << pids.at(i); - } - } - if(unchanged_pids.isEmpty()) return true; - if(!d->mModel.isLocalhost()) return false; //We can't use KAuth to affect non-localhost processes - - KAuth::Action action(QStringLiteral("org.kde.ksysguard.processlisthelper.changecpuscheduler")); - action.setParentWidget(window()); - d->setupKAuthAction( action, unchanged_pids); - action.addArgument(QStringLiteral("cpuScheduler"), (int)newCpuSched); - action.addArgument(QStringLiteral("cpuSchedulerPriority"), newCpuSchedPriority); - KAuth::ExecuteJob *job = action.execute(); + auto result = d->mProcessController->setCPUScheduler(pids, newCpuSched, newCpuSchedPriority); - if (job->exec()) { + if (result == KSysGuard::ProcessController::Result::Success) { updateList(); - } else if (!job->exec()) { + return true; + } else if (result == KSysGuard::ProcessController::Result::Error) { KMessageBox::sorry(this, i18n("You do not have the permission to change the CPU Scheduler for the process and there " - "was a problem trying to run as root. Error %1 %2", job->error(), job->errorString())); - return false; + "was a problem trying to run as root.")); } - return true; + return false; } bool KSysGuardProcessList::killProcesses(const QList< long long> &pids, int sig) { - QList< long long> unkilled_pids; - for (int i = 0; i < pids.size(); ++i) { - bool success = d->mModel.processController()->sendSignal(pids.at(i), sig); - // If we failed due to a reason other than insufficient permissions, elevating to root can't - // help us - if(!success && (d->mModel.processController()->lastError() == KSysGuard::Processes::InsufficientPermissions || d->mModel.processController()->lastError() == KSysGuard::Processes::Unknown)) { - unkilled_pids << pids.at(i); - } - } - if(unkilled_pids.isEmpty()) return true; - if(!d->mModel.isLocalhost()) return false; //We can't elevate privileges to kill non-localhost processes + auto result = d->mProcessController->sendSignal(pids, sig); - KAuth::Action action(QStringLiteral("org.kde.ksysguard.processlisthelper.sendsignal")); - action.setParentWidget(window()); - d->setupKAuthAction( action, unkilled_pids); - action.addArgument(QStringLiteral("signal"), sig); - KAuth::ExecuteJob *job = action.execute(); - - if (job->exec()) { + if (result == KSysGuard::ProcessController::Result::Success) { updateList(); - } else if (!job->exec()) { + return true; + } else if (result == KSysGuard::ProcessController::Result::Error) { KMessageBox::sorry(this, i18n("You do not have the permission to kill the process and there " - "was a problem trying to run as root. Error %1 %2", job->error(), job->errorString())); - return false; + "was a problem trying to run as root.")); } - return true; + return false; } void KSysGuardProcessList::killSelectedProcesses()