diff --git a/CMakeLists.txt b/CMakeLists.txt index f2aecf1..2d0e6d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,124 +1,127 @@ cmake_minimum_required(VERSION 3.0) project(libksysguard) set(PROJECT_VERSION "5.16.80") set(PROJECT_VERSION_MAJOR 5) # check with non-Plasma consumers (e.g. KDevelop) before bumping these versions set(QT_MIN_VERSION "5.5.0") set(KF5_MIN_VERSION "5.58.0") find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMAddTests) include(ECMInstallIcons) include(ECMSetupVersion) include(ECMQtDeclareLoggingCategory) include(CMakePackageConfigHelpers) include(CheckIncludeFiles) include(CheckLibraryExists) include(FeatureSummary) find_package(Qt5 ${QT_MIN_VERSION} REQUIRED CONFIG COMPONENTS DBus Network Widgets) find_package(Qt5WebEngineWidgets ${QT_MIN_VERSION} CONFIG) set_package_properties(Qt5WebEngineWidgets PROPERTIES URL "git://code.qt.org/qt/qtwebenginewidgets.git" DESCRIPTION "Qt WebEngine module (web browsing engine)" TYPE OPTIONAL PURPOSE "Used by the HTML-based GUI ksysguard library" ) find_package(KF5 REQUIRED COMPONENTS CoreAddons Config I18n WindowSystem Completion Auth WidgetsAddons IconThemes ConfigWidgets Service GlobalAccel KIO) find_package(KF5 OPTIONAL_COMPONENTS Plasma) set_package_properties(KF5Plasma PROPERTIES URL "https://cgit.kde.org/plasma-framework.git/" DESCRIPTION "The library of the plasma project" TYPE OPTIONAL PURPOSE "Used by signalplotter to use Plasma themes" ) find_package(ZLIB REQUIRED) set_package_properties(ZLIB PROPERTIES DESCRIPTION "Support for gzip compressed files and data streams" URL "http://www.zlib.net" TYPE REQUIRED ) check_library_exists(c clock_gettime "time.h" HAVE_CLOCK_GETTIME_C) ecm_setup_version(${PROJECT_VERSION} VARIABLE_PREFIX KSYSGUARD VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/ksysguard_version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF5SysGuardConfigVersion.cmake" SOVERSION 7 ) find_package(X11) set_package_properties(X11 PROPERTIES DESCRIPTION "X11 libraries" URL "http://www.x.org" TYPE OPTIONAL PURPOSE "Required for building the X11 based workspace" ) if(X11_FOUND) find_package(Qt5X11Extras REQUIRED) find_library(X11_XRes_LIB XRes ${X11_LIB_SEARCH_PATH}) find_path(X11_XRes_INCLUDE_PATH X11/extensions/XRes.h ${X11_INC_SEARCH_PATH}) if(X11_XRes_LIB AND X11_XRes_INCLUDE_PATH) set(X11_XRes_FOUND TRUE) endif() endif() set(HAVE_X11 ${X11_FOUND}) set(HAVE_XRES ${X11_XRes_FOUND}) set(HAVE_QTWEBENGINEWIDGETS ${Qt5WebEngineWidgets_FOUND}) configure_file(config-ksysguard.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-ksysguard.h ) add_definitions(-DQT_NO_URL_CAST_FROM_STRING) add_definitions(-DQT_USE_QSTRINGBUILDER) add_definitions(-DQT_NO_CAST_FROM_ASCII) add_definitions(-DQT_NO_CAST_TO_ASCII) 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 ) if (KF5Plasma_FOUND) add_subdirectory( signalplotter ) endif() add_subdirectory( ksgrd ) if(BUILD_TESTING) add_subdirectory( tests ) endif() install(DIRECTORY scripts/ DESTINATION ${KDE_INSTALL_DATADIR}/ksysguard/scripts) set(CMAKECONFIG_INSTALL_DIR ${KDE_INSTALL_LIBDIR}/cmake/KF5SysGuard) configure_package_config_file(KF5SysGuardConfig.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/KF5SysGuardConfig.cmake" INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/KF5SysGuardConfig.cmake ${CMAKE_CURRENT_BINARY_DIR}/KF5SysGuardConfigVersion.cmake DESTINATION ${CMAKECONFIG_INSTALL_DIR}) install(EXPORT libksysguardLibraryTargets NAMESPACE KF5:: DESTINATION ${CMAKECONFIG_INSTALL_DIR} FILE KF5SysGuardLibraryTargets.cmake ) if (${ECM_VERSION} STRGREATER "5.58.0") install(FILES libksysguard.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}) else() install(FILES libksysguard.categories DESTINATION ${KDE_INSTALL_CONFDIR}) endif() feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/processcore/CMakeLists.txt b/processcore/CMakeLists.txt index e92fd50..4feeb1a 100644 --- a/processcore/CMakeLists.txt +++ b/processcore/CMakeLists.txt @@ -1,66 +1,69 @@ add_definitions(-DTRANSLATION_DOMAIN=\"processcore\") ########### next target ############### set(ksysguard_LIB_SRCS processes.cpp process.cpp processes_local_p.cpp 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) add_library(processcore ${ksysguard_LIB_SRCS}) add_library(KF5::ProcessCore ALIAS processcore) target_link_libraries(processcore PUBLIC Qt5::Core PRIVATE KF5::I18n + KF5::AuthCore ${ZLIB_LIBRARIES} ) if( ${CMAKE_SYSTEM_NAME} MATCHES "NetBSD" ) message(STATUS "Adding kvm library on NetBSD") target_link_libraries(processcore kvm) endif() target_include_directories(processcore PUBLIC "$" "$" ) set_target_properties(processcore PROPERTIES VERSION ${KSYSGUARD_VERSION_STRING} SOVERSION ${KSYSGUARD_SOVERSION} EXPORT_NAME ProcessCore) install(TARGETS processcore EXPORT libksysguardLibraryTargets ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) ########### install files ############### install( FILES processes.h process.h + process_controller.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/ksysguard/processcore COMPONENT Devel ) #------ KAuth stuff # Auth example helper set(ksysguardprocesslist_helper_srcs helper.cpp process.cpp processes_local_p.cpp processes_base_p.cpp) add_executable(ksysguardprocesslist_helper ${ksysguardprocesslist_helper_srcs}) target_link_libraries(ksysguardprocesslist_helper Qt5::Core KF5::AuthCore KF5::I18n) install(TARGETS ksysguardprocesslist_helper DESTINATION ${KAUTH_HELPER_INSTALL_DIR}) kauth_install_helper_files(ksysguardprocesslist_helper org.kde.ksysguard.processlisthelper root) kauth_install_actions(org.kde.ksysguard.processlisthelper actions.actions) set_target_properties(ksysguardprocesslist_helper PROPERTIES COMPILE_FLAGS "-Wall -ggdb") diff --git a/processcore/process_controller.cpp b/processcore/process_controller.cpp new file mode 100644 index 0000000..338f0db --- /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/processcore/process_controller.h b/processcore/process_controller.h new file mode 100644 index 0000000..014406f --- /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/processui/ksysguardprocesslist.cpp b/processui/ksysguardprocesslist.cpp index e6bf43b..f999b94 100644 --- a/processui/ksysguardprocesslist.cpp +++ b/processui/ksysguardprocesslist.cpp @@ -1,1590 +1,1529 @@ /* KSysGuard, the KDE System Guard Copyright (c) 1999 - 2001 Chris Schlaeger Copyright (c) 2006 - 2007 John Tapsell This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 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 "ksysguardprocesslist.h" #include "../config-ksysguard.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include //For SIGTERM #include #include #include #include #include #include #include #include #include #include "ReniceDlg.h" #include "ui_ProcessWidgetUI.h" #include "scripting.h" +#include "process_controller.h" #include #include //Trolltech have a testing class for classes that inherit QAbstractItemModel. If you want to run with this run-time testing enabled, put the modeltest.* files in this directory and uncomment the next line //#define DO_MODELCHECK #ifdef DO_MODELCHECK #include "modeltest.h" #endif class ProgressBarItemDelegate : public QStyledItemDelegate { public: ProgressBarItemDelegate(QObject *parent) : QStyledItemDelegate(parent) { } void paint(QPainter *painter, const QStyleOptionViewItem &opt, const QModelIndex &index) const override { QStyleOptionViewItem option = opt; initStyleOption(&option,index); float percentage = index.data(ProcessModel::PercentageRole).toFloat(); auto history = index.data(ProcessModel::PercentageHistoryRole).value>(); if (percentage >= 0 || history.size() > 1) drawPercentageDisplay(painter, option, percentage, history); else QStyledItemDelegate::paint(painter, option, index); } private: inline void drawPercentageDisplay(QPainter *painter, QStyleOptionViewItem &option, float percentage, const QVector &history) const { QStyle *style = option.widget ? option.widget->style() : QApplication::style(); const QRect &rect = option.rect; const int HIST_MS_PER_PX = 100; // 100 ms = 1 px -> 1 s = 10 px bool hasHistory = history.size() > 1; // Make sure that more than one entry is visible if (hasHistory) { int width = (history.crbegin()->timestamp - (history.crbegin() + 1)->timestamp) / HIST_MS_PER_PX; hasHistory = width < rect.width(); } // draw the background style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, option.widget); QPalette::ColorGroup cg = option.state & QStyle::State_Enabled ? QPalette::Normal : QPalette::Disabled; if (cg == QPalette::Normal && !(option.state & QStyle::State_Active)) cg = QPalette::Inactive; //Now draw our percentage thingy int size = qMin(int(percentage * rect.height()), rect.height()); if(size > 2 ) { //make sure the line will have a width of more than 1 pixel painter->setPen(Qt::NoPen); QColor color = option.palette.color(cg, QPalette::Link); color.setAlpha(33); painter->fillRect( rect.x(), rect.y() + rect.height() - size, rect.width(), size, color); } // Draw the history graph if(hasHistory) { QColor color = option.palette.color(cg, QPalette::Link); color.setAlpha(66); painter->setPen(Qt::NoPen); QPainterPath path; // From right to left path.moveTo(rect.right(), rect.bottom()); int xNow = rect.right(); auto now = history.constLast(); int height = qMin(int(rect.height() * now.value), rect.height()); path.lineTo(xNow, rect.bottom() - height); for(int index = history.size() - 2; index >= 0 && xNow > rect.left(); --index) { auto next = history.at(index); int width = (now.timestamp - next.timestamp) / HIST_MS_PER_PX; int xNext = qMax(xNow - width, rect.left()); now = next; xNow = xNext; int height = qMin(int(rect.height() * now.value), rect.height()); path.lineTo(xNow, rect.bottom() - height); } path.lineTo(xNow, rect.bottom()); path.lineTo(rect.right(), rect.bottom()); painter->fillPath(path, color); } // draw the text if (!option.text.isEmpty()) { QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &option, option.widget); if (option.state & QStyle::State_Selected) { painter->setPen(option.palette.color(cg, QPalette::HighlightedText)); } else { painter->setPen(option.palette.color(cg, QPalette::Text)); } painter->setFont(option.font); QTextOption textOption; textOption.setWrapMode(QTextOption::ManualWrap); textOption.setTextDirection(option.direction); textOption.setAlignment(QStyle::visualAlignment(option.direction, option.displayAlignment)); painter->drawText(textRect, option.text, textOption); } // draw the focus rect if (option.state & QStyle::State_HasFocus) { QStyleOptionFocusRect o; o.QStyleOption::operator=(option); o.rect = style->subElementRect(QStyle::SE_ItemViewItemFocusRect, &option, option.widget); o.state |= QStyle::State_KeyboardFocusChange; o.state |= QStyle::State_Item; QPalette::ColorGroup cg = (option.state & QStyle::State_Enabled) ? QPalette::Normal : QPalette::Disabled; o.backgroundColor = option.palette.color(cg, (option.state & QStyle::State_Selected) ? QPalette::Highlight : QPalette::Window); style->drawPrimitive(QStyle::PE_FrameFocusRect, &o, painter, option.widget); } } }; struct KSysGuardProcessListPrivate { KSysGuardProcessListPrivate(KSysGuardProcessList* q, const QString &hostName) : mModel(q, hostName), mFilterModel(q), mUi(new Ui::ProcessWidget()), mProcessContextMenu(nullptr), mUpdateTimer(nullptr), mToolsMenu(new QMenu(q)) { mScripting = nullptr; mNeedToExpandInit = false; mNumItemsSelected = -1; mResortCountDown = 2; //The items added initially will be already sorted, but without CPU info. On the second refresh we will have CPU usage, so /then/ we can resort renice = new QAction(i18np("Set Priority...", "Set Priority...", 1), q); renice->setShortcut(Qt::Key_F8); selectParent = new QAction(i18n("Jump to Parent Process"), q); selectTracer = new QAction(i18n("Jump to Process Debugging This One"), q); window = new QAction(i18n("Show Application Window"), q); resume = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-start")), i18n("Resume Stopped Process"), q); terminate = new QAction(i18np("End Process", "End Processes", 1), q); terminate->setIcon(QIcon::fromTheme(QStringLiteral("process-stop"))); terminate->setShortcut(Qt::Key_Delete); kill = new QAction(i18np("Forcibly Kill Process", "Forcibly Kill Processes", 1), q); kill->setIcon(QIcon::fromTheme(QStringLiteral("process-stop"))); kill->setShortcut(Qt::SHIFT + Qt::Key_Delete); sigStop = new QAction(i18n("Suspend (STOP)"), q); sigCont = new QAction(i18n("Continue (CONT)"), q); sigHup = new QAction(i18n("Hangup (HUP)"), q); sigInt = new QAction(i18n("Interrupt (INT)"), q); sigTerm = new QAction(i18n("Terminate (TERM)"), q); sigKill = new QAction(i18n("Kill (KILL)"), q); sigUsr1 = new QAction(i18n("User 1 (USR1)"), q); sigUsr2 = new QAction(i18n("User 2 (USR2)"), q); //Set up '/' as a shortcut to jump to the quick search text widget jumpToSearchFilter = new QAction(i18n("Focus on Quick Search"), q); jumpToSearchFilter->setShortcuts(QList() << QKeySequence::Find << '/'); } ~KSysGuardProcessListPrivate() { delete mUi; mUi = nullptr; } /** The number rows and their children for the given parent in the mFilterModel model */ int totalRowCount(const QModelIndex &parent) const; /** Helper function to setup 'action' with the given pids */ void setupKAuthAction(KAuth::Action &action, const QList & pids) const; /** fire a timer event if we are set to use our internal timer*/ void fireTimerEvent(); /** The process model. This contains all the data on all the processes running on the system */ ProcessModel mModel; /** The process filter. The mModel is connected to this, and this filter model connects to the view. This lets us * sort the view and filter (by using the combo box or the search line) */ ProcessFilter mFilterModel; + KSysGuard::ProcessController *mProcessController = nullptr; + /** The graphical user interface for this process list widget, auto-generated by Qt Designer */ Ui::ProcessWidget *mUi; /** The context menu when you right click on a process */ QMenu *mProcessContextMenu; /** A timer to call updateList() every mUpdateIntervalMSecs. * NULL is mUpdateIntervalMSecs is <= 0. */ QTimer *mUpdateTimer; /** The time to wait, in milliseconds, between updating the process list */ int mUpdateIntervalMSecs; /** Number of items that are selected */ int mNumItemsSelected; /** Class to deal with the scripting. NULL if scripting is disabled */ Scripting *mScripting; /** A counter to mark when to resort, so that we do not resort on every update */ int mResortCountDown; bool mNeedToExpandInit; QAction *renice; QAction *terminate; QAction *kill; QAction *selectParent; QAction *selectTracer; QAction *jumpToSearchFilter; QAction *window; QAction *resume; QAction *sigStop; QAction *sigCont; QAction *sigHup; QAction *sigInt; QAction *sigTerm; QAction *sigKill; QAction *sigUsr1; QAction *sigUsr2; QMenu* mToolsMenu; }; KSysGuardProcessList::KSysGuardProcessList(QWidget* parent, const QString &hostName) : QWidget(parent), d(new KSysGuardProcessListPrivate(this, hostName)) { 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); d->mUi->treeView->setModel(&d->mFilterModel); #ifdef DO_MODELCHECK new ModelTest(&d->mModel, this); #endif d->mUi->treeView->setItemDelegate(new ProgressBarItemDelegate(d->mUi->treeView)); d->mUi->treeView->header()->setContextMenuPolicy(Qt::CustomContextMenu); connect(d->mUi->treeView->header(), &QWidget::customContextMenuRequested, this, &KSysGuardProcessList::showColumnContextMenu); d->mProcessContextMenu = new QMenu(d->mUi->treeView); d->mUi->treeView->setContextMenuPolicy(Qt::CustomContextMenu); connect(d->mUi->treeView, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showProcessContextMenu(QPoint))); d->mUi->treeView->header()->setSectionsClickable(true); d->mUi->treeView->header()->setSortIndicatorShown(true); d->mUi->treeView->header()->setCascadingSectionResizes(false); connect(d->mUi->btnKillProcess, &QAbstractButton::clicked, this, &KSysGuardProcessList::killSelectedProcesses); connect(d->mUi->txtFilter, &QLineEdit::textChanged, this, &KSysGuardProcessList::filterTextChanged); connect(d->mUi->cmbFilter, SIGNAL(currentIndexChanged(int)), this, SLOT(setStateInt(int))); connect(d->mUi->treeView, &QTreeView::expanded, this, &KSysGuardProcessList::expandAllChildren); connect(d->mUi->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &KSysGuardProcessList::selectionChanged); connect(&d->mFilterModel, &QAbstractItemModel::rowsInserted, this, &KSysGuardProcessList::rowsInserted); connect(&d->mFilterModel, &QAbstractItemModel::rowsRemoved, this, &KSysGuardProcessList::processListChanged); setMinimumSize(sizeHint()); d->mFilterModel.setFilterKeyColumn(-1); /* Hide various columns by default, to reduce information overload */ d->mUi->treeView->header()->hideSection(ProcessModel::HeadingVmSize); d->mUi->treeView->header()->hideSection(ProcessModel::HeadingNiceness); d->mUi->treeView->header()->hideSection(ProcessModel::HeadingTty); d->mUi->treeView->header()->hideSection(ProcessModel::HeadingStartTime); d->mUi->treeView->header()->hideSection(ProcessModel::HeadingNoNewPrivileges); d->mUi->treeView->header()->hideSection(ProcessModel::HeadingCommand); d->mUi->treeView->header()->hideSection(ProcessModel::HeadingPid); d->mUi->treeView->header()->hideSection(ProcessModel::HeadingCPUTime); d->mUi->treeView->header()->hideSection(ProcessModel::HeadingIoRead); d->mUi->treeView->header()->hideSection(ProcessModel::HeadingIoWrite); d->mUi->treeView->header()->hideSection(ProcessModel::HeadingXMemory); d->mUi->treeView->header()->hideSection(ProcessModel::HeadingCGroup); d->mUi->treeView->header()->hideSection(ProcessModel::HeadingMACContext); d->mUi->treeView->header()->hideSection(ProcessModel::HeadingVmPSS); // NOTE! After this is all setup, the settings for the header are restored // from the user's last run. (in restoreHeaderState) // So making changes here only affects the default settings. To // test changes temporarily, comment out the lines in restoreHeaderState. // When you are happy with the changes and want to commit, increase the // value of PROCESSHEADERVERSION. This will force the header state // to be reset back to the defaults for all users. d->mUi->treeView->header()->resizeSection(ProcessModel::HeadingCPUUsage, d->mUi->treeView->header()->sectionSizeHint(ProcessModel::HeadingCPUUsage)); d->mUi->treeView->header()->resizeSection(ProcessModel::HeadingMemory, d->mUi->treeView->header()->sectionSizeHint(ProcessModel::HeadingMemory)); d->mUi->treeView->header()->resizeSection(ProcessModel::HeadingSharedMemory, d->mUi->treeView->header()->sectionSizeHint(ProcessModel::HeadingSharedMemory)); d->mUi->treeView->header()->setSectionResizeMode(0, QHeaderView::Interactive); d->mUi->treeView->header()->setStretchLastSection(true); //Process names can have mixed case. Make the filter case insensitive. d->mFilterModel.setFilterCaseSensitivity(Qt::CaseInsensitive); d->mFilterModel.setSortCaseSensitivity(Qt::CaseInsensitive); d->mUi->txtFilter->installEventFilter(this); d->mUi->treeView->installEventFilter(this); d->mUi->treeView->setDragEnabled(true); d->mUi->treeView->setDragDropMode(QAbstractItemView::DragOnly); //Sort by username by default d->mUi->treeView->sortByColumn(ProcessModel::HeadingUser, Qt::AscendingOrder); // Add all the actions to the main widget, and get all the actions to call actionTriggered when clicked QList actions; actions << d->renice << d->kill << d->terminate << d->selectParent << d->selectTracer << d->window << d->jumpToSearchFilter; actions << d->resume << d->sigStop << d->sigCont << d->sigHup << d->sigInt << d->sigTerm << d->sigKill << d->sigUsr1 << d->sigUsr2; foreach(QAction *action, actions) { addAction(action); connect(action, &QAction::triggered, this, [this, action]() { actionTriggered(action); }); } retranslateUi(); d->mUi->btnKillProcess->setIcon(QIcon::fromTheme(QStringLiteral("process-stop"))); d->mUi->btnKillProcess->setToolTip(i18n("End the selected process. Warning - you may lose unsaved work.
Right click on a process to send other signals.
See What's This for technical information.")); auto addByDesktopName = [this](const QString& desktopName) { auto kService = KService::serviceByDesktopName(desktopName); if (kService) { auto action = new QAction(QIcon::fromTheme(kService->icon()), kService->name(), this); connect(action, &QAction::triggered, this, [kService](bool) { KRun::runService(*kService, { }, nullptr); }); d->mToolsMenu->addAction(action); } }; addByDesktopName(QStringLiteral("org.kde.konsole")); const QString ksysguardDesktopName = QStringLiteral("org.kde.ksysguard"); // The following expression is true when the libksysguard process list is _not_ embedded in KSysGuard. // Only then we add KSysGuard to the menu if (qApp->desktopFileName() != ksysguardDesktopName) { addByDesktopName(ksysguardDesktopName); } addByDesktopName(QStringLiteral("org.kde.ksystemlog")); addByDesktopName(QStringLiteral("org.kde.kinfocenter")); addByDesktopName(QStringLiteral("org.kde.filelight")); addByDesktopName(QStringLiteral("org.kde.sweeper")); addByDesktopName(QStringLiteral("org.kde.kmag")); addByDesktopName(QStringLiteral("htop")); // Add Run Command... // auto runCommandAction = new QAction(i18nc("@action:inmenu", "Run Command"), this); // //INFO: This is one way of how to find out the two required parameters for the globalShortcut method: // auto list = KGlobalAccel::getGlobalShortcutsByKey(QKeySequence(QStringLiteral("Alt+Space"))); // foreach (auto item, list) { // qDebug() << item.componentUniqueName() << item.uniqueName(); // //prints: 'krunner', 'run command' // } const auto runCommandShortcutList = KGlobalAccel::self()->globalShortcut(QStringLiteral("krunner"), QStringLiteral("run command")); runCommandAction->setShortcuts(runCommandShortcutList); runCommandAction->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); connect(runCommandAction, &QAction::triggered, this, [](){ KRun::runCommand(QStringLiteral("krunner"), nullptr); }); d->mToolsMenu->addAction(runCommandAction); // Add the xkill functionality... auto killWindowAction = new QAction(QIcon::fromTheme(QStringLiteral("document-close")), i18nc("@action:inmenu", "Kill a Window"), this); // Find shortcut of xkill functionality which is defined in KWin const auto killWindowShortcutList = KGlobalAccel::self()->globalShortcut(QStringLiteral("kwin"), QStringLiteral("Kill Window")); killWindowAction->setShortcuts(killWindowShortcutList); // We don't use xkill directly but the method in KWin which allows to press Esc to abort. auto killWindowKwinMethod = new QDBusInterface(QStringLiteral("org.kde.KWin"), QStringLiteral("/KWin"), QStringLiteral("org.kde.KWin")); // If KWin is not the window manager, then we disable the entry: if (!killWindowKwinMethod->isValid()) { killWindowAction->setEnabled(false); } connect(killWindowAction, &QAction::triggered, this, [this, killWindowKwinMethod](bool) { // with DBus call, always use the async method. // Otherwise it could wait up to 30 seconds in certain situations. killWindowKwinMethod->asyncCall(QStringLiteral("killWindow")); }); d->mToolsMenu->addAction(killWindowAction); d->mUi->btnTools->setMenu(d->mToolsMenu); } KSysGuardProcessList::~KSysGuardProcessList() { delete d; } QTreeView *KSysGuardProcessList::treeView() const { return d->mUi->treeView; } QLineEdit *KSysGuardProcessList::filterLineEdit() const { return d->mUi->txtFilter; } ProcessFilter::State KSysGuardProcessList::state() const { return d->mFilterModel.filter(); } void KSysGuardProcessList::setStateInt(int state) { setState((ProcessFilter::State) state); d->mUi->treeView->scrollTo( d->mUi->treeView->currentIndex()); } void KSysGuardProcessList::setState(ProcessFilter::State state) { //index is the item the user selected in the combo box d->mFilterModel.setFilter(state); d->mModel.setSimpleMode( (state != ProcessFilter::AllProcessesInTreeForm) ); d->mUi->cmbFilter->setCurrentIndex( (int)state); if(isVisible()) expandInit(); } void KSysGuardProcessList::filterTextChanged(const QString &newText) { d->mFilterModel.setFilterRegExp(newText.trimmed()); if(isVisible()) expandInit(); d->mUi->btnKillProcess->setEnabled(d->mUi->treeView->selectionModel()->hasSelection()); d->mUi->treeView->scrollTo(d->mUi->treeView->currentIndex()); } int KSysGuardProcessList::visibleProcessesCount() const { //This assumes that all the visible rows are processes. This is true currently, but might not be //true if we add support for showing threads etc if(d->mModel.isSimpleMode()) return d->mFilterModel.rowCount(); return d->totalRowCount(QModelIndex()); } int KSysGuardProcessListPrivate::totalRowCount(const QModelIndex &parent ) const { int numRows = mFilterModel.rowCount(parent); int total = numRows; for (int i = 0; i < numRows; ++i) { QModelIndex index = mFilterModel.index(i, 0,parent); //if it has children add the total if (mFilterModel.hasChildren(index)) total += totalRowCount(index); } return total; } void KSysGuardProcessListPrivate::setupKAuthAction(KAuth::Action &action, const QList & pids) const { 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[i]); } action.addArgument(QStringLiteral("pidcount"), processCount); } void KSysGuardProcessList::selectionChanged() { int numSelected = d->mUi->treeView->selectionModel()->selectedRows().size(); if(numSelected == d->mNumItemsSelected) return; d->mNumItemsSelected = numSelected; d->mUi->btnKillProcess->setEnabled(numSelected != 0); d->renice->setText(i18np("Set Priority...", "Set Priority...", numSelected)); d->kill->setText(i18np("Forcibly Kill Process", "Forcibly Kill Processes", numSelected)); d->terminate->setText(i18ncp("Context menu", "End Process", "End Processes", numSelected)); } void KSysGuardProcessList::showProcessContextMenu(const QModelIndex &index) { if(!index.isValid()) return; QRect rect = d->mUi->treeView->visualRect(index); QPoint point(rect.x() + rect.width()/4, rect.y() + rect.height()/2 ); showProcessContextMenu(point); } void KSysGuardProcessList::showProcessContextMenu(const QPoint &point) { d->mProcessContextMenu->clear(); const QModelIndexList selectedIndexes = d->mUi->treeView->selectionModel()->selectedRows(); const int numProcesses = selectedIndexes.size(); if(numProcesses == 0) { //No processes selected, so no process context menu //Check just incase we have no columns visible. In which case show the column context menu //so that users can unhide columns if there are no columns visible for(int i = 0; i < d->mFilterModel.columnCount(); ++i) { if(!d->mUi->treeView->header()->isSectionHidden(i)) return; } showColumnContextMenu(point); return; } QModelIndex realIndex = d->mFilterModel.mapToSource(selectedIndexes.at(0)); KSysGuard::Process *process = reinterpret_cast (realIndex.internalPointer()); //If the selected process is a zombie, do not bother offering renice and kill options bool showSignalingEntries = numProcesses != 1 || process->status() != KSysGuard::Process::Zombie; if(showSignalingEntries) { d->mProcessContextMenu->addAction(d->renice); QMenu *signalMenu = d->mProcessContextMenu->addMenu(i18n("Send Signal")); signalMenu->addAction(d->sigStop); signalMenu->addAction(d->sigCont); signalMenu->addAction(d->sigHup); signalMenu->addAction(d->sigInt); signalMenu->addAction(d->sigTerm); signalMenu->addAction(d->sigKill); signalMenu->addAction(d->sigUsr1); signalMenu->addAction(d->sigUsr2); } if(numProcesses == 1 && process->parentPid() > 1) { //As a design decision, I do not show the 'Jump to parent process' option when the //parent is just 'init'. KSysGuard::Process *parent_process = d->mModel.getProcess(process->parentPid()); if(parent_process) { //it should not be possible for this process to not exist, but check just incase QString parent_name = parent_process->name(); d->selectParent->setText(i18n("Jump to Parent Process (%1)", parent_name)); d->mProcessContextMenu->addAction(d->selectParent); } } if(numProcesses == 1 && process->tracerpid() >= 0) { //If the process is being debugged, offer to select it d->mProcessContextMenu->addAction(d->selectTracer); } if (numProcesses == 1 && !d->mModel.data(realIndex, ProcessModel::WindowIdRole).isNull()) { d->mProcessContextMenu->addAction(d->window); } if(numProcesses == 1 && process->status() == KSysGuard::Process::Stopped) { //If the process is stopped, offer to resume it d->mProcessContextMenu->addAction(d->resume); } if(numProcesses == 1 && d->mScripting) { foreach(QAction *action, d->mScripting->actions()) { d->mProcessContextMenu->addAction(action); } } if (showSignalingEntries) { d->mProcessContextMenu->addSeparator(); d->mProcessContextMenu->addAction(d->terminate); if (numProcesses == 1 && !process->timeKillWasSent().isNull()) d->mProcessContextMenu->addAction(d->kill); } d->mProcessContextMenu->popup(d->mUi->treeView->viewport()->mapToGlobal(point)); } void KSysGuardProcessList::actionTriggered(QObject *object) { if(!isVisible()) //Ignore triggered actions if we are not visible! return; //Reset the text back to normal d->selectParent->setText(i18n("Jump to Parent Process")); QAction *result = qobject_cast(object); if(result == nullptr) { //Escape was pressed. Do nothing. } else if(result == d->renice) { reniceSelectedProcesses(); } else if(result == d->terminate) { sendSignalToSelectedProcesses(SIGTERM, true); } else if(result == d->kill) { sendSignalToSelectedProcesses(SIGKILL, true); } else if(result == d->selectParent) { QModelIndexList selectedIndexes = d->mUi->treeView->selectionModel()->selectedRows(); int numProcesses = selectedIndexes.size(); if(numProcesses == 0) return; //No processes selected QModelIndex realIndex = d->mFilterModel.mapToSource(selectedIndexes.at(0)); KSysGuard::Process *process = reinterpret_cast (realIndex.internalPointer()); if(process) selectAndJumpToProcess(process->parentPid()); } else if(result == d->selectTracer) { QModelIndexList selectedIndexes = d->mUi->treeView->selectionModel()->selectedRows(); int numProcesses = selectedIndexes.size(); if(numProcesses == 0) return; //No processes selected QModelIndex realIndex = d->mFilterModel.mapToSource(selectedIndexes.at(0)); KSysGuard::Process *process = reinterpret_cast (realIndex.internalPointer()); if(process) selectAndJumpToProcess(process->tracerpid()); } else if(result == d->window) { QModelIndexList selectedIndexes = d->mUi->treeView->selectionModel()->selectedRows(); int numProcesses = selectedIndexes.size(); if(numProcesses == 0) return; //No processes selected foreach( const QModelIndex &index, selectedIndexes) { QModelIndex realIndex = d->mFilterModel.mapToSource(index); QVariant widVar= d->mModel.data(realIndex, ProcessModel::WindowIdRole); if( !widVar.isNull() ) { int wid = widVar.toInt(); KWindowSystem::activateWindow(wid); } } } else if(result == d->jumpToSearchFilter) { d->mUi->txtFilter->setFocus(); } else { int sig; if(result == d->resume || result == d->sigCont) sig = SIGCONT; //Despite the function name, this sends a signal, rather than kill it. Silly unix :) else if(result == d->sigStop) sig = SIGSTOP; else if(result == d->sigHup) sig = SIGHUP; else if(result == d->sigInt) sig = SIGINT; else if(result == d->sigTerm) sig = SIGTERM; else if(result == d->sigKill) sig = SIGKILL; else if(result == d->sigUsr1) sig = SIGUSR1; else if(result == d->sigUsr2) sig = SIGUSR2; else return; sendSignalToSelectedProcesses(sig, false); } } void KSysGuardProcessList::selectAndJumpToProcess(int pid) { KSysGuard::Process *process = d->mModel.getProcess(pid); if(!process) return; QModelIndex sourceIndex = d->mModel.getQModelIndex(process, 0); QModelIndex filterIndex = d->mFilterModel.mapFromSource( sourceIndex ); if(!filterIndex.isValid() && !d->mUi->txtFilter->text().isEmpty()) { //The filter is preventing us from finding the parent. Clear the filter //(It could also be the combo box - should we deal with that case as well?) d->mUi->txtFilter->clear(); filterIndex = d->mFilterModel.mapFromSource( sourceIndex ); } d->mUi->treeView->clearSelection(); d->mUi->treeView->setCurrentIndex(filterIndex); d->mUi->treeView->scrollTo( filterIndex, QAbstractItemView::PositionAtCenter); } void KSysGuardProcessList::showColumnContextMenu(const QPoint &point){ QMenu menu; QAction *action; int num_headings = d->mFilterModel.columnCount(); int index = d->mUi->treeView->header()->logicalIndexAt(point); if(index >= 0) { bool anyOtherVisibleColumns = false; for(int i = 0; i < num_headings; ++i) { if(i != index && !d->mUi->treeView->header()->isSectionHidden(i)) { anyOtherVisibleColumns = true; break; } } if(anyOtherVisibleColumns) { //selected a column. Give the option to hide it action = new QAction(&menu); action->setData(-index-1); //We set data to be negative (and minus 1) to hide a column, and positive to show a column action->setText(i18n("Hide Column '%1'", d->mFilterModel.headerData(index, Qt::Horizontal, Qt::DisplayRole).toString())); menu.addAction(action); if(d->mUi->treeView->header()->sectionsHidden()) { menu.addSeparator(); } } } if(d->mUi->treeView->header()->sectionsHidden()) { for(int i = 0; i < num_headings; ++i) { if(d->mUi->treeView->header()->isSectionHidden(i)) { #ifndef HAVE_XRES if(i == ProcessModel::HeadingXMemory) continue; #endif action = new QAction(&menu); action->setText(i18n("Show Column '%1'", d->mFilterModel.headerData(i, Qt::Horizontal, Qt::DisplayRole).toString())); action->setData(i); //We set data to be negative (and minus 1) to hide a column, and positive to show a column menu.addAction(action); } } } QAction *actionAuto = nullptr; QAction *actionKB = nullptr; QAction *actionMB = nullptr; QAction *actionGB = nullptr; QAction *actionPercentage = nullptr; QAction *actionShowCmdlineOptions = nullptr; QAction *actionShowTooltips = nullptr; QAction *actionNormalizeCPUUsage = nullptr; QAction *actionIoCharacters = nullptr; QAction *actionIoSyscalls = nullptr; QAction *actionIoActualCharacters = nullptr; QAction *actionIoShowRate = nullptr; bool showIoRate = false; if(index == ProcessModel::HeadingIoRead || index == ProcessModel::HeadingIoWrite) showIoRate = d->mModel.ioInformation() == ProcessModel::BytesRate || d->mModel.ioInformation() == ProcessModel::SyscallsRate || d->mModel.ioInformation() == ProcessModel::ActualBytesRate; if( index == ProcessModel::HeadingVmSize || index == ProcessModel::HeadingMemory || index == ProcessModel::HeadingXMemory || index == ProcessModel::HeadingSharedMemory || index == ProcessModel::HeadingVmPSS || ( (index == ProcessModel::HeadingIoRead || index == ProcessModel::HeadingIoWrite) && d->mModel.ioInformation() != ProcessModel::Syscalls)) { //If the user right clicks on a column that contains a memory size, show a toggle option for displaying //the memory in different units. e.g. "2000 k" or "2 m" menu.addSeparator()->setText(i18n("Display Units")); QActionGroup *unitsGroup = new QActionGroup(&menu); /* Automatic (human readable)*/ actionAuto = new QAction(&menu); actionAuto->setText(i18n("Mixed")); actionAuto->setCheckable(true); menu.addAction(actionAuto); unitsGroup->addAction(actionAuto); /* Kilobytes */ actionKB = new QAction(&menu); actionKB->setText((showIoRate)?i18n("Kilobytes per second"):i18n("Kilobytes")); actionKB->setCheckable(true); menu.addAction(actionKB); unitsGroup->addAction(actionKB); /* Megabytes */ actionMB = new QAction(&menu); actionMB->setText((showIoRate)?i18n("Megabytes per second"):i18n("Megabytes")); actionMB->setCheckable(true); menu.addAction(actionMB); unitsGroup->addAction(actionMB); /* Gigabytes */ actionGB = new QAction(&menu); actionGB->setText((showIoRate)?i18n("Gigabytes per second"):i18n("Gigabytes")); actionGB->setCheckable(true); menu.addAction(actionGB); unitsGroup->addAction(actionGB); ProcessModel::Units currentUnit; if(index == ProcessModel::HeadingIoRead || index == ProcessModel::HeadingIoWrite) { currentUnit = d->mModel.ioUnits(); } else { actionPercentage = new QAction(&menu); actionPercentage->setText(i18n("Percentage")); actionPercentage->setCheckable(true); menu.addAction(actionPercentage); unitsGroup->addAction(actionPercentage); currentUnit = d->mModel.units(); } switch(currentUnit) { case ProcessModel::UnitsAuto: actionAuto->setChecked(true); break; case ProcessModel::UnitsKB: actionKB->setChecked(true); break; case ProcessModel::UnitsMB: actionMB->setChecked(true); break; case ProcessModel::UnitsGB: actionGB->setChecked(true); break; case ProcessModel::UnitsPercentage: actionPercentage->setChecked(true); break; default: break; } unitsGroup->setExclusive(true); } else if(index == ProcessModel::HeadingName) { menu.addSeparator(); actionShowCmdlineOptions = new QAction(&menu); actionShowCmdlineOptions->setText(i18n("Display command line options")); actionShowCmdlineOptions->setCheckable(true); actionShowCmdlineOptions->setChecked(d->mModel.isShowCommandLineOptions()); menu.addAction(actionShowCmdlineOptions); } else if(index == ProcessModel::HeadingCPUUsage) { menu.addSeparator(); actionNormalizeCPUUsage = new QAction(&menu); actionNormalizeCPUUsage->setText(i18n("Divide CPU usage by number of CPUs")); actionNormalizeCPUUsage->setCheckable(true); actionNormalizeCPUUsage->setChecked(d->mModel.isNormalizedCPUUsage()); menu.addAction(actionNormalizeCPUUsage); } if(index == ProcessModel::HeadingIoRead || index == ProcessModel::HeadingIoWrite) { menu.addSeparator()->setText(i18n("Displayed Information")); QActionGroup *ioInformationGroup = new QActionGroup(&menu); actionIoCharacters = new QAction(&menu); actionIoCharacters->setText(i18n("Characters read/written")); actionIoCharacters->setCheckable(true); menu.addAction(actionIoCharacters); ioInformationGroup->addAction(actionIoCharacters); actionIoSyscalls = new QAction(&menu); actionIoSyscalls->setText(i18n("Number of Read/Write operations")); actionIoSyscalls->setCheckable(true); menu.addAction(actionIoSyscalls); ioInformationGroup->addAction(actionIoSyscalls); actionIoActualCharacters = new QAction(&menu); actionIoActualCharacters->setText(i18n("Bytes actually read/written")); actionIoActualCharacters->setCheckable(true); menu.addAction(actionIoActualCharacters); ioInformationGroup->addAction(actionIoActualCharacters); actionIoShowRate = new QAction(&menu); actionIoShowRate->setText(i18n("Show I/O rate")); actionIoShowRate->setCheckable(true); actionIoShowRate->setChecked(showIoRate); menu.addAction(actionIoShowRate); switch(d->mModel.ioInformation()) { case ProcessModel::Bytes: case ProcessModel::BytesRate: actionIoCharacters->setChecked(true); break; case ProcessModel::Syscalls: case ProcessModel::SyscallsRate: actionIoSyscalls->setChecked(true); break; case ProcessModel::ActualBytes: case ProcessModel::ActualBytesRate: actionIoActualCharacters->setChecked(true); break; default: break; } } menu.addSeparator(); actionShowTooltips = new QAction(&menu); actionShowTooltips->setCheckable(true); actionShowTooltips->setChecked(d->mModel.isShowingTooltips()); actionShowTooltips->setText(i18n("Show Tooltips")); menu.addAction(actionShowTooltips); QAction *result = menu.exec(d->mUi->treeView->header()->mapToGlobal(point)); if(!result) return; //Menu cancelled if(result == actionAuto) { if(index == ProcessModel::HeadingIoRead || index == ProcessModel::HeadingIoWrite) d->mModel.setIoUnits(ProcessModel::UnitsAuto); else d->mModel.setUnits(ProcessModel::UnitsAuto); return; } else if(result == actionKB) { if(index == ProcessModel::HeadingIoRead || index == ProcessModel::HeadingIoWrite) d->mModel.setIoUnits(ProcessModel::UnitsKB); else d->mModel.setUnits(ProcessModel::UnitsKB); return; } else if(result == actionMB) { if(index == ProcessModel::HeadingIoRead || index == ProcessModel::HeadingIoWrite) d->mModel.setIoUnits(ProcessModel::UnitsMB); else d->mModel.setUnits(ProcessModel::UnitsMB); return; } else if(result == actionGB) { if(index == ProcessModel::HeadingIoRead || index == ProcessModel::HeadingIoWrite) d->mModel.setIoUnits(ProcessModel::UnitsGB); else d->mModel.setUnits(ProcessModel::UnitsGB); return; } else if(result == actionPercentage) { d->mModel.setUnits(ProcessModel::UnitsPercentage); return; } else if(result == actionShowCmdlineOptions) { d->mModel.setShowCommandLineOptions(actionShowCmdlineOptions->isChecked()); return; } else if(result == actionNormalizeCPUUsage) { d->mModel.setNormalizedCPUUsage(actionNormalizeCPUUsage->isChecked()); return; } else if(result == actionShowTooltips) { d->mModel.setShowingTooltips(actionShowTooltips->isChecked()); return; } else if(result == actionIoCharacters) { d->mModel.setIoInformation((showIoRate)?ProcessModel::BytesRate:ProcessModel::Bytes); return; } else if(result == actionIoSyscalls) { d->mModel.setIoInformation((showIoRate)?ProcessModel::SyscallsRate:ProcessModel::Syscalls); return; } else if(result == actionIoActualCharacters) { d->mModel.setIoInformation((showIoRate)?ProcessModel::ActualBytesRate:ProcessModel::ActualBytes); return; } else if(result == actionIoShowRate) { showIoRate = actionIoShowRate->isChecked(); switch(d->mModel.ioInformation()) { case ProcessModel::Bytes: case ProcessModel::BytesRate: d->mModel.setIoInformation((showIoRate)?ProcessModel::BytesRate:ProcessModel::Bytes); break; case ProcessModel::Syscalls: case ProcessModel::SyscallsRate: d->mModel.setIoInformation((showIoRate)?ProcessModel::SyscallsRate:ProcessModel::Syscalls); break; case ProcessModel::ActualBytes: case ProcessModel::ActualBytesRate: d->mModel.setIoInformation((showIoRate)?ProcessModel::ActualBytesRate:ProcessModel::ActualBytes); break; default: break; } } int i = result->data().toInt(); //We set data to be negative to hide a column, and positive to show a column if(i < 0) d->mUi->treeView->hideColumn(-1-i); else { d->mUi->treeView->showColumn(i); updateList(); d->mUi->treeView->resizeColumnToContents(i); d->mUi->treeView->resizeColumnToContents(d->mFilterModel.columnCount()); } menu.deleteLater(); } void KSysGuardProcessList::expandAllChildren(const QModelIndex &parent) { //This is called when the user expands a node. This then expands all of its //children. This will trigger this function again recursively. QModelIndex sourceParent = d->mFilterModel.mapToSource(parent); for(int i = 0; i < d->mModel.rowCount(sourceParent); i++) { d->mUi->treeView->expand(d->mFilterModel.mapFromSource(d->mModel.index(i,0, sourceParent))); } } void KSysGuardProcessList::rowsInserted(const QModelIndex & parent, int start, int end ) { if(d->mModel.isSimpleMode() || parent.isValid()) { emit processListChanged(); return; //No tree or not a root node - no need to expand init } disconnect(&d->mFilterModel, &QAbstractItemModel::rowsInserted, this, &KSysGuardProcessList::rowsInserted); //It is a root node that we just inserted - expand it bool expanded = false; for(int i = start; i <= end; i++) { QModelIndex index = d->mFilterModel.index(i, 0, QModelIndex()); if(!d->mUi->treeView->isExpanded(index)) { if(!expanded) { disconnect(d->mUi->treeView, &QTreeView::expanded, this, &KSysGuardProcessList::expandAllChildren); expanded = true; } d->mUi->treeView->expand(index); d->mNeedToExpandInit = true; } } if(expanded) connect(d->mUi->treeView, &QTreeView::expanded, this, &KSysGuardProcessList::expandAllChildren); connect(&d->mFilterModel, &QAbstractItemModel::rowsInserted, this, &KSysGuardProcessList::rowsInserted); emit processListChanged(); } void KSysGuardProcessList::expandInit() { if(d->mModel.isSimpleMode()) return; //No tree - no need to expand init bool expanded = false; for(int i = 0; i < d->mFilterModel.rowCount(QModelIndex()); i++) { QModelIndex index = d->mFilterModel.index(i, 0, QModelIndex()); if(!d->mUi->treeView->isExpanded(index)) { if(!expanded) { disconnect(d->mUi->treeView, &QTreeView::expanded, this, &KSysGuardProcessList::expandAllChildren); expanded = true; } d->mUi->treeView->expand(index); } } if(expanded) connect(d->mUi->treeView, &QTreeView::expanded, this, &KSysGuardProcessList::expandAllChildren); } void KSysGuardProcessList::hideEvent ( QHideEvent * event ) //virtual protected from QWidget { //Stop updating the process list if we are hidden if(d->mUpdateTimer) d->mUpdateTimer->stop(); //stop any scripts running, to save on memory if(d->mScripting) d->mScripting->stopAllScripts(); QWidget::hideEvent(event); } void KSysGuardProcessList::showEvent ( QShowEvent * event ) //virtual protected from QWidget { //Start updating the process list again if we are shown again updateList(); QHeaderView *header = d->mUi->treeView->header(); d->mUi->treeView->sortByColumn(header->sortIndicatorSection(), header->sortIndicatorOrder()); QWidget::showEvent(event); } void KSysGuardProcessList::changeEvent( QEvent * event ) { if (event->type() == QEvent::LanguageChange) { d->mModel.retranslateUi(); d->mUi->retranslateUi(this); retranslateUi(); } QWidget::changeEvent(event); } void KSysGuardProcessList::retranslateUi() { d->mUi->cmbFilter->setItemIcon(ProcessFilter::AllProcesses, QIcon::fromTheme(QStringLiteral("view-process-all"))); d->mUi->cmbFilter->setItemIcon(ProcessFilter::AllProcessesInTreeForm, QIcon::fromTheme(QStringLiteral("view-process-all-tree"))); d->mUi->cmbFilter->setItemIcon(ProcessFilter::SystemProcesses, QIcon::fromTheme(QStringLiteral("view-process-system"))); d->mUi->cmbFilter->setItemIcon(ProcessFilter::UserProcesses, QIcon::fromTheme(QStringLiteral("view-process-users"))); d->mUi->cmbFilter->setItemIcon(ProcessFilter::OwnProcesses, QIcon::fromTheme(QStringLiteral("view-process-own"))); d->mUi->cmbFilter->setItemIcon(ProcessFilter::ProgramsOnly, QIcon::fromTheme(QStringLiteral("view-process-all"))); } void KSysGuardProcessList::updateList() { if(isVisible()) { KSysGuard::Processes::UpdateFlags updateFlags = KSysGuard::Processes::StandardInformation; if(!d->mUi->treeView->isColumnHidden(ProcessModel::HeadingIoRead) || !d->mUi->treeView->isColumnHidden(ProcessModel::HeadingIoWrite)) updateFlags |= KSysGuard::Processes::IOStatistics; if(!d->mUi->treeView->isColumnHidden(ProcessModel::HeadingXMemory)) updateFlags |= KSysGuard::Processes::XMemory; d->mModel.update(d->mUpdateIntervalMSecs, updateFlags); if(d->mUpdateTimer) d->mUpdateTimer->start(d->mUpdateIntervalMSecs); emit updated(); if (QToolTip::isVisible() && qApp->topLevelAt(QCursor::pos()) == window()) { QWidget *w = d->mUi->treeView->viewport(); if(w->geometry().contains(d->mUi->treeView->mapFromGlobal( QCursor::pos() ))) { QHelpEvent event(QEvent::ToolTip, w->mapFromGlobal( QCursor::pos() ), QCursor::pos()); qApp->notify(w, &event); } } if(--d->mResortCountDown <= 0) { d->mResortCountDown = 2; //resort every second time //resort now QHeaderView *header= d->mUi->treeView->header(); d->mUi->treeView->sortByColumn(header->sortIndicatorSection(), header->sortIndicatorOrder()); } if( d->mNeedToExpandInit ) { expandInit(); d->mNeedToExpandInit = false; } } } int KSysGuardProcessList::updateIntervalMSecs() const { return d->mUpdateIntervalMSecs; } void KSysGuardProcessList::setUpdateIntervalMSecs(int intervalMSecs) { if(intervalMSecs == d->mUpdateIntervalMSecs) return; d->mUpdateIntervalMSecs = intervalMSecs; if(intervalMSecs <= 0) { //no point keep the timer around if we aren't updating automatically delete d->mUpdateTimer; d->mUpdateTimer = nullptr; return; } if(!d->mUpdateTimer) { //intervalMSecs is a valid time, so set up a timer d->mUpdateTimer = new QTimer(this); d->mUpdateTimer->setSingleShot(true); connect(d->mUpdateTimer, &QTimer::timeout, this, &KSysGuardProcessList::updateList); if(isVisible()) d->mUpdateTimer->start(d->mUpdateIntervalMSecs); } else d->mUpdateTimer->setInterval(d->mUpdateIntervalMSecs); } 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; } QList KSysGuardProcessList::selectedProcesses() const { QList processes; QModelIndexList selectedIndexes = d->mUi->treeView->selectionModel()->selectedRows(); for(int i = 0; i < selectedIndexes.size(); ++i) { KSysGuard::Process *process = reinterpret_cast (d->mFilterModel.mapToSource(selectedIndexes.at(i)).internalPointer()); processes << process; } return processes; } void KSysGuardProcessList::reniceSelectedProcesses() { QList pids; QPointer reniceDlg; { QList processes = selectedProcesses(); QStringList selectedAsStrings; if (processes.isEmpty()) { KMessageBox::sorry(this, i18n("You must select a process first.")); return; } int sched = -2; int iosched = -2; foreach(KSysGuard::Process *process, processes) { pids << process->pid(); selectedAsStrings << d->mModel.getStringForProcess(process); if(sched == -2) sched = (int)process->scheduler(); else if(sched != -1 && sched != (int)process->scheduler()) sched = -1; //If two processes have different schedulers, disable the cpu scheduler stuff if(iosched == -2) iosched = (int)process->ioPriorityClass(); else if(iosched != -1 && iosched != (int)process->ioPriorityClass()) iosched = -1; //If two processes have different schedulers, disable the cpu scheduler stuff } int firstPriority = processes.first()->niceLevel(); int firstIOPriority = processes.first()->ioniceLevel(); bool supportsIoNice = d->mModel.processController()->supportsIoNiceness(); if(!supportsIoNice) { iosched = -2; firstIOPriority = -2; } reniceDlg = new ReniceDlg(d->mUi->treeView, selectedAsStrings, firstPriority, sched, firstIOPriority, iosched); if(reniceDlg->exec() == QDialog::Rejected) { delete reniceDlg; return; } } //Because we've done into ReniceDlg, which calls processEvents etc, our processes list is no //longer valid QList renicePids; QList changeCPUSchedulerPids; QList changeIOSchedulerPids; foreach (long long pid, pids) { KSysGuard::Process *process = d->mModel.getProcess(pid); if (!process) continue; switch(reniceDlg->newCPUSched) { case -2: case -1: //Invalid, not changed etc. break; //So do nothing case KSysGuard::Process::Other: case KSysGuard::Process::Fifo: if(reniceDlg->newCPUSched != (int)process->scheduler()) { changeCPUSchedulerPids << pid; renicePids << pid; } else if(reniceDlg->newCPUPriority != process->niceLevel()) renicePids << pid; break; case KSysGuard::Process::RoundRobin: case KSysGuard::Process::Batch: if(reniceDlg->newCPUSched != (int)process->scheduler() || reniceDlg->newCPUPriority != process->niceLevel()) { changeCPUSchedulerPids << pid; } break; } switch(reniceDlg->newIOSched) { case -2: case -1: //Invalid, not changed etc. break; //So do nothing case KSysGuard::Process::None: if(reniceDlg->newIOSched != (int)process->ioPriorityClass()) { // Unfortunately linux doesn't actually let us set the ioniceness back to none after being set to something else if(process->ioPriorityClass() != KSysGuard::Process::BestEffort || reniceDlg->newIOPriority != process->ioniceLevel()) changeIOSchedulerPids << pid; } break; case KSysGuard::Process::Idle: if(reniceDlg->newIOSched != (int)process->ioPriorityClass()) { changeIOSchedulerPids << pid; } break; case KSysGuard::Process::BestEffort: if(process->ioPriorityClass() == KSysGuard::Process::None && reniceDlg->newIOPriority == (process->niceLevel() + 20)/5) break; //Don't set to BestEffort if it's on None and the nicelevel wouldn't change case KSysGuard::Process::RealTime: if(reniceDlg->newIOSched != (int)process->ioPriorityClass() || reniceDlg->newIOPriority != process->ioniceLevel()) { changeIOSchedulerPids << pid; } break; } } if(!changeCPUSchedulerPids.isEmpty()) { Q_ASSERT(reniceDlg->newCPUSched >= 0); if(!changeCpuScheduler(changeCPUSchedulerPids, (KSysGuard::Process::Scheduler) reniceDlg->newCPUSched, reniceDlg->newCPUPriority)) { delete reniceDlg; return; } } if(!renicePids.isEmpty()) { Q_ASSERT(reniceDlg->newCPUPriority <= 20 && reniceDlg->newCPUPriority >= -20); if(!reniceProcesses(renicePids, reniceDlg->newCPUPriority)) { delete reniceDlg; return; } } if(!changeIOSchedulerPids.isEmpty()) { if(!changeIoScheduler(changeIOSchedulerPids, (KSysGuard::Process::IoPriorityClass) reniceDlg->newIOSched, reniceDlg->newIOPriority)) { delete reniceDlg; return; } } delete reniceDlg; updateList(); } 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() { sendSignalToSelectedProcesses(SIGTERM, true); } void KSysGuardProcessList::sendSignalToSelectedProcesses(int sig, bool confirm) { QModelIndexList selectedIndexes = d->mUi->treeView->selectionModel()->selectedRows(); QStringList selectedAsStrings; QList< long long> selectedPids; QList processes = selectedProcesses(); foreach(KSysGuard::Process *process, processes) { selectedPids << process->pid(); if (!confirm) continue; QString name = d->mModel.getStringForProcess(process); selectedAsStrings << name; } if (selectedPids.isEmpty()) { if (confirm) KMessageBox::sorry(this, i18n("You must select a process first.")); return; } else if (confirm && (sig == SIGTERM || sig == SIGKILL)) { int count = selectedAsStrings.count(); QString msg; QString title; QString dontAskAgainKey; QString closeButton; if (sig == SIGTERM) { msg = i18np("Are you sure you want to end this process? Any unsaved work may be lost.", "Are you sure you want to end these %1 processes? Any unsaved work may be lost", count); title = i18ncp("Dialog title", "End Process", "End %1 Processes", count); dontAskAgainKey = QStringLiteral("endconfirmation"); closeButton = i18n("End"); } else if (sig == SIGKILL) { msg = i18np("Are you sure you want to immediately and forcibly kill this process? Any unsaved work may be lost.", "Are you sure you want to immediately and forcibly kill these %1 processes? Any unsaved work may be lost", count); title = i18ncp("Dialog title", "Forcibly Kill Process", "Forcibly Kill %1 Processes", count); dontAskAgainKey = QStringLiteral("killconfirmation"); closeButton = i18n("Kill"); } int res = KMessageBox::warningContinueCancelList(this, msg, selectedAsStrings, title, KGuiItem(closeButton, QStringLiteral("process-stop")), KStandardGuiItem::cancel(), dontAskAgainKey); if (res != KMessageBox::Continue) return; } //We have shown a GUI dialog box, which processes events etc. //So processes is NO LONGER VALID if (!killProcesses(selectedPids, sig)) return; if (sig == SIGTERM || sig == SIGKILL) { foreach (long long pid, selectedPids) { KSysGuard::Process *process = d->mModel.getProcess(pid); if (process) process->timeKillWasSent().start(); d->mUi->treeView->selectionModel()->clearSelection(); } } updateList(); } bool KSysGuardProcessList::showTotals() const { return d->mModel.showTotals(); } void KSysGuardProcessList::setShowTotals(bool showTotals) //slot { d->mModel.setShowTotals(showTotals); } ProcessModel::Units KSysGuardProcessList::units() const { return d->mModel.units(); } void KSysGuardProcessList::setUnits(ProcessModel::Units unit) { d->mModel.setUnits(unit); } void KSysGuardProcessList::saveSettings(KConfigGroup &cg) { /* Note that the ksysguard program does not use these functions. It saves the settings itself to an xml file instead */ cg.writeEntry("units", (int)(units())); cg.writeEntry("ioUnits", (int)(d->mModel.ioUnits())); cg.writeEntry("ioInformation", (int)(d->mModel.ioInformation())); cg.writeEntry("showCommandLineOptions", d->mModel.isShowCommandLineOptions()); cg.writeEntry("normalizeCPUUsage", d->mModel.isNormalizedCPUUsage()); cg.writeEntry("showTooltips", d->mModel.isShowingTooltips()); cg.writeEntry("showTotals", showTotals()); cg.writeEntry("filterState", (int)(state())); cg.writeEntry("updateIntervalMSecs", updateIntervalMSecs()); cg.writeEntry("headerState", d->mUi->treeView->header()->saveState()); //If we change, say, the header between versions of ksysguard, then the old headerState settings will not be valid. //The version property lets us keep track of which version we are cg.writeEntry("version", PROCESSHEADERVERSION); } void KSysGuardProcessList::loadSettings(const KConfigGroup &cg) { /* Note that the ksysguard program does not use these functions. It saves the settings itself to an xml file instead */ setUnits((ProcessModel::Units) cg.readEntry("units", (int)ProcessModel::UnitsKB)); d->mModel.setIoUnits((ProcessModel::Units) cg.readEntry("ioUnits", (int)ProcessModel::UnitsKB)); d->mModel.setIoInformation((ProcessModel::IoInformation) cg.readEntry("ioInformation", (int)ProcessModel::ActualBytesRate)); d->mModel.setShowCommandLineOptions(cg.readEntry("showCommandLineOptions", false)); d->mModel.setNormalizedCPUUsage(cg.readEntry("normalizeCPUUsage", true)); d->mModel.setShowingTooltips(cg.readEntry("showTooltips", true)); setShowTotals(cg.readEntry("showTotals", true)); setStateInt(cg.readEntry("filterState", (int)ProcessFilter::AllProcesses)); setUpdateIntervalMSecs(cg.readEntry("updateIntervalMSecs", 2000)); int version = cg.readEntry("version", 0); if(version == PROCESSHEADERVERSION) { //If the header has changed, the old settings are no longer valid. Only restore if version is the same restoreHeaderState(cg.readEntry("headerState", QByteArray())); } } void KSysGuardProcessList::restoreHeaderState(const QByteArray & state) { d->mUi->treeView->header()->restoreState(state); } bool KSysGuardProcessList::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); if(obj == d->mUi->treeView) { if(keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) { d->mUi->treeView->selectionModel()->select(d->mUi->treeView->currentIndex(), QItemSelectionModel::Select | QItemSelectionModel::Rows); showProcessContextMenu(d->mUi->treeView->currentIndex()); return true; } else if(keyEvent->matches(QKeySequence::MoveToPreviousLine) || keyEvent->matches(QKeySequence::SelectPreviousLine) || keyEvent->matches(QKeySequence::MoveToPreviousPage) || keyEvent->matches(QKeySequence::SelectPreviousPage)) { if (d->mUi->treeView->selectionModel()->selectedRows().size() == 1 && d->mUi->treeView->selectionModel()->selectedRows().first().row() == 0) { // when first row is selected, pressing up or pgup moves to the textfield d->mUi->txtFilter->setFocus(); return true; } } else if (!keyEvent->text().isEmpty() && keyEvent->key() != Qt::Key_Tab && (!keyEvent->modifiers() || keyEvent->modifiers() == Qt::ShiftModifier)) { // move to textfield and forward keyevent if user starts typing from treeview d->mUi->txtFilter->setFocus(); QApplication::sendEvent(d->mUi->txtFilter, event); return true; } } else { Q_ASSERT(obj == d->mUi->txtFilter); if (d->mUi->treeView->model()->rowCount() == 0) { // treeview is empty, do nothing return false; } if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) { // pressing enter will send enter to the first row in the list // the focusin eventfilter will make sure the first row is selected if there was // no previous selection d->mUi->treeView->setFocus(); QApplication::sendEvent(d->mUi->treeView, event); return true; } else if(keyEvent->matches(QKeySequence::MoveToNextLine) || keyEvent->matches(QKeySequence::SelectNextLine) || keyEvent->matches(QKeySequence::MoveToNextPage) || keyEvent->matches(QKeySequence::SelectNextPage)) { // attempting to move down by down-key or pgdown, or pressing enter will move focus // to the treeview d->mUi->treeView->setFocus(); return true; } } } return false; } ProcessModel *KSysGuardProcessList::processModel() { return &d->mModel; } void KSysGuardProcessList::setKillButtonVisible(bool visible) { d->mUi->btnKillProcess->setVisible(visible); } bool KSysGuardProcessList::isKillButtonVisible() const { return d->mUi->btnKillProcess->isVisible(); } bool KSysGuardProcessList::scriptingEnabled() const { return !!d->mScripting; } void KSysGuardProcessList::setScriptingEnabled(bool enabled) { if(!!d->mScripting == enabled) return; //Nothing changed if(!enabled) { delete d->mScripting; d->mScripting = nullptr; } else { d->mScripting = new Scripting(this); d->mScripting->hide(); } }