diff --git a/KF5KIOConfig.cmake.in b/KF5KIOConfig.cmake.in --- a/KF5KIOConfig.cmake.in +++ b/KF5KIOConfig.cmake.in @@ -17,6 +17,7 @@ find_dependency(KF5JobWidgets "@KF5_DEP_VERSION@") find_dependency(KF5Solid "@KF5_DEP_VERSION@") find_dependency(KF5XmlGui "@KF5_DEP_VERSION@") +find_dependency(KF5WindowSystem "@KF5_DEP_VERSION@") endif() find_dependency(Qt5Network "@REQUIRED_QT_VERSION@") diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -74,6 +74,7 @@ if (TARGET KF5::KIOGui) ecm_add_tests( favicontest.cpp + processlauncherjobtest.cpp NAME_PREFIX "kiogui-" LINK_LIBRARIES KF5::KIOCore KF5::KIOGui Qt5::Test ) diff --git a/autotests/kiotesthelper.h b/autotests/kiotesthelper.h --- a/autotests/kiotesthelper.h +++ b/autotests/kiotesthelper.h @@ -19,6 +19,8 @@ // This file can only be included once in a given binary +#include +#include #include #include #include diff --git a/autotests/processlauncherjobtest.h b/autotests/processlauncherjobtest.h new file mode 100644 --- /dev/null +++ b/autotests/processlauncherjobtest.h @@ -0,0 +1,53 @@ +/* + This file is part of the KDE libraries + Copyright (c) 2014, 2020 David Faure + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License or ( at + your option ) version 3 or, at the discretion of KDE e.V. ( which shall + act as a proxy as in section 14 of the GPLv3 ), 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 Lesser General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef PROCESSLAUNCHERJOBTEST_H +#define PROCESSLAUNCHERJOBTEST_H + +#include +#include + +class ProcessLauncherJobTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + + void startProcess_data(); + void startProcess(); + + void shouldFailOnNonExecutableDesktopFile(); + + void shouldFailOnNonExistingExecutable_data(); + void shouldFailOnNonExistingExecutable(); + +private: + QString createTempService(); + void writeTempServiceDesktopFile(const QString &filePath); + + QStringList m_filesToRemove; + +}; + +#endif /* PROCESSLAUNCHERJOBTEST_H */ + diff --git a/autotests/processlauncherjobtest.cpp b/autotests/processlauncherjobtest.cpp new file mode 100644 --- /dev/null +++ b/autotests/processlauncherjobtest.cpp @@ -0,0 +1,212 @@ +/* + This file is part of the KDE libraries + Copyright (c) 2014, 2020 David Faure + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License or ( at + your option ) version 3 or, at the discretion of KDE e.V. ( which shall + act as a proxy as in section 14 of the GPLv3 ), 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 Lesser 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 "processlauncherjobtest.h" +#include "processlauncherjob.h" + +#include "kiotesthelper.h" // createTestFile etc. + +#include +#include +#include + +#ifdef Q_OS_UNIX +#include // kill +#endif + +#include +#include +#include +#include + +QTEST_GUILESS_MAIN(ProcessLauncherJobTest) + +void ProcessLauncherJobTest::initTestCase() +{ + QStandardPaths::setTestModeEnabled(true); +} + +void ProcessLauncherJobTest::cleanupTestCase() +{ + std::for_each(m_filesToRemove.begin(), m_filesToRemove.end(), [](const QString & f) { + QFile::remove(f); + }); +} + +static const char s_tempServiceName[] = "processlauncherjobtest_service.desktop"; + +static void createSrcFile(const QString path) +{ + QFile srcFile(path); + QVERIFY2(srcFile.open(QFile::WriteOnly), qPrintable(srcFile.errorString())); + srcFile.write("Hello world\n"); +} + +void ProcessLauncherJobTest::startProcess_data() +{ + QTest::addColumn("tempFile"); + QTest::addColumn("useExec"); + QTest::addColumn("numFiles"); + + QTest::newRow("1_file_exec") << false << true << 1; + QTest::newRow("1_file_waitForStarted") << false << false << 1; + QTest::newRow("1_tempfile_exec") << true << true << 1; + QTest::newRow("1_tempfile_waitForStarted") << true << false << 1; + + QTest::newRow("2_files_exec") << false << true << 2; + QTest::newRow("2_files_waitForStarted") << false << false << 2; + QTest::newRow("2_tempfiles_exec") << true << true << 2; + QTest::newRow("2_tempfiles_waitForStarted") << true << false << 2; +} + +void ProcessLauncherJobTest::startProcess() +{ + QFETCH(bool, tempFile); + QFETCH(bool, useExec); + QFETCH(int, numFiles); + + // Given a service desktop file and a number of source files + const QString path = createTempService(); + QTemporaryDir tempDir; + const QString srcDir = tempDir.path(); + QList urls; + for (int i = 0; i < numFiles; ++i) { + const QString srcFile = srcDir + "/srcfile" + QString::number(i + 1); + createSrcFile(srcFile); + QVERIFY(QFile::exists(srcFile)); + urls.append(QUrl::fromLocalFile(srcFile)); + } + + // When running a ProcessLauncherJob + KService::Ptr servicePtr(new KService(path)); + KIO::ProcessLauncherJob *job = new KIO::ProcessLauncherJob(servicePtr, WId{}, this); + job->setUrls(urls); + if (tempFile) { + job->setRunFlags(KIO::ProcessLauncherJob::DeleteTemporaryFiles); + } + if (useExec) { + QVERIFY(job->exec()); + } else { + job->start(); + QVERIFY(job->waitForStarted()); + } + const QVector pids = job->pids(); + + // Then the service should be executed (which copies the source file to "dest") + QCOMPARE(pids.count(), numFiles); + QVERIFY(!pids.contains(0)); + for (int i = 0; i < numFiles; ++i) { + const QString dest = srcDir + "/dest_srcfile" + QString::number(i + 1); + QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest)); + QVERIFY(QFile::exists(srcDir + "/srcfile" + QString::number(i + 1))); // if tempfile is true, kioexec will delete it... in 3 minutes. + QVERIFY(QFile::remove(dest)); // cleanup + } + +#ifdef Q_OS_UNIX + // Kill the running kioexec processes + for (qint64 pid : pids) { + ::kill(pid, SIGTERM); + } +#endif + + // The kioexec processes that are waiting for 3 minutes and got killed above, + // will now trigger KProcessRunner::slotProcessError, KProcessRunner::slotProcessExited and delete the KProcessRunner. + // We wait for that to happen otherwise it gets confusing to see that output from later tests. + QTRY_COMPARE(KProcessRunner::instanceCount(), 0); +} + +void ProcessLauncherJobTest::shouldFailOnNonExecutableDesktopFile() +{ + // Given a .desktop file in a temporary directory (outside the trusted paths) + QTemporaryDir tempDir; + const QString srcDir = tempDir.path(); + const QString desktopFilePath = srcDir + "/shouldfail.desktop"; + writeTempServiceDesktopFile(desktopFilePath); + m_filesToRemove.append(desktopFilePath); + + const QString srcFile = srcDir + "/srcfile"; + createSrcFile(srcFile); + const QList urls{QUrl::fromLocalFile(srcFile)}; + KService::Ptr servicePtr(new KService(desktopFilePath)); + KIO::ProcessLauncherJob *job = new KIO::ProcessLauncherJob(servicePtr, WId{}, this); + job->setUrls(urls); + QVERIFY(!job->exec()); + QCOMPARE(job->error(), KJob::UserDefinedError); + QCOMPARE(job->errorString(), QStringLiteral("You are not authorized to execute this file.")); +} + +void ProcessLauncherJobTest::shouldFailOnNonExistingExecutable_data() +{ + QTest::addColumn("tempFile"); + + QTest::newRow("file") << false; + QTest::newRow("tempFile") << true; +} + +void ProcessLauncherJobTest::shouldFailOnNonExistingExecutable() +{ + QFETCH(bool, tempFile); + + const QString desktopFilePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/non_existing_executable.desktop"); + KDesktopFile file(desktopFilePath); + KConfigGroup group = file.desktopGroup(); + group.writeEntry("Name", "KRunUnittestService"); + group.writeEntry("Type", "Service"); + group.writeEntry("Exec", "does_not_exist %f %d/dest_%n"); + file.sync(); + + KService::Ptr servicePtr(new KService(desktopFilePath)); + KIO::ProcessLauncherJob *job = new KIO::ProcessLauncherJob(servicePtr, WId{}, this); + job->setUrls({QUrl::fromLocalFile(desktopFilePath)}); // just to have one URL as argument, as the desktop file expects + if (tempFile) { + job->setRunFlags(KIO::ProcessLauncherJob::DeleteTemporaryFiles); + } + QVERIFY(!job->exec()); + QCOMPARE(job->error(), KJob::UserDefinedError); + QCOMPARE(job->errorString(), QStringLiteral("Could not find the program 'does_not_exist'")); + + QFile::remove(desktopFilePath); +} + +void ProcessLauncherJobTest::writeTempServiceDesktopFile(const QString &filePath) +{ + if (!QFile::exists(filePath)) { + KDesktopFile file(filePath); + KConfigGroup group = file.desktopGroup(); + group.writeEntry("Name", "KRunUnittestService"); + group.writeEntry("Type", "Service"); +#ifdef Q_OS_WIN + group.writeEntry("Exec", "copy.exe %f %d/dest_%n"); +#else + group.writeEntry("Exec", "cd %d ; cp %f %d/dest_%n"); // cd is just to show that we can't do QFile::exists(binary) +#endif + file.sync(); + } +} + +QString ProcessLauncherJobTest::createTempService() +{ + const QString fileName = s_tempServiceName; + const QString fakeService = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/") + fileName; + writeTempServiceDesktopFile(fakeService); + m_filesToRemove.append(fakeService); + return fakeService; +} diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -1,5 +1,17 @@ +configure_file(config-kiogui.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kiogui.h) + set(kiogui_SRCS faviconrequestjob.cpp + processlauncherjob.cpp + kprocessrunner.cpp +) + +ecm_qt_declare_logging_category(kiogui_SRCS + HEADER kiogui_debug.h + IDENTIFIER KIO_GUI + CATEGORY_NAME kf5.kio.gui + DESCRIPTION "KIOGui (KIO)" + EXPORT KIO ) ecm_qt_declare_logging_category(kiogui_SRCS @@ -19,6 +31,7 @@ target_link_libraries(KF5KIOGui PUBLIC KF5::KIOCore + KF5::WindowSystem Qt5::Gui PRIVATE KF5::I18n @@ -33,6 +46,7 @@ ecm_generate_headers(KIOGui_CamelCase_HEADERS HEADER_NAMES FavIconRequestJob + ProcessLauncherJob PREFIX KIO REQUIRED_HEADERS KIO_namespaced_gui_HEADERS diff --git a/src/gui/config-kiogui.h.cmake b/src/gui/config-kiogui.h.cmake new file mode 100644 --- /dev/null +++ b/src/gui/config-kiogui.h.cmake @@ -0,0 +1 @@ +#cmakedefine01 HAVE_X11 diff --git a/src/gui/kprocessrunner.cpp b/src/gui/kprocessrunner.cpp new file mode 100644 --- /dev/null +++ b/src/gui/kprocessrunner.cpp @@ -0,0 +1,304 @@ +/* This file is part of the KDE libraries + Copyright (c) 2020 David Faure + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License or ( at + your option ) version 3 or, at the discretion of KDE e.V. ( which shall + act as a proxy as in section 14 of the GPLv3 ), 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 Lesser 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 "kprocessrunner_p.h" + +#include "kiogui_debug.h" +#include "config-kiogui.h" + +#include "desktopexecparser.h" +#include "krecentdocument.h" +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +static int s_instanceCount = 0; // for the unittest + +KProcessRunner::KProcessRunner(const KService::Ptr &service, const QList &urls, WId windowId, + KIO::ProcessLauncherJob::RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) + : m_process{new KProcess}, + m_executable(KIO::DesktopExecParser::executablePath(service->exec())) +{ + ++s_instanceCount; + KIO::DesktopExecParser execParser(*service, urls); + + const QString realExecutable = execParser.resultingArguments().at(0); + if (!QFileInfo::exists(realExecutable) && QStandardPaths::findExecutable(realExecutable).isEmpty()) { + emitDelayedError(i18n("Could not find the program '%1'", realExecutable)); + return; + } + + execParser.setUrlsAreTempFiles(flags & KIO::ProcessLauncherJob::DeleteTemporaryFiles); + execParser.setSuggestedFileName(suggestedFileName); + const QStringList args = execParser.resultingArguments(); + if (args.isEmpty()) { + emitDelayedError(i18n("Error processing Exec field in %1", service->entryPath())); + return; + } + //qDebug() << "KProcess args=" << args; + *m_process << args; + + enum DiscreteGpuCheck { NotChecked, Present, Absent }; + static DiscreteGpuCheck s_gpuCheck = NotChecked; + + if (service->runOnDiscreteGpu() && s_gpuCheck == NotChecked) { + // Check whether we have a discrete gpu + bool hasDiscreteGpu = false; + QDBusInterface iface(QStringLiteral("org.kde.Solid.PowerManagement"), + QStringLiteral("/org/kde/Solid/PowerManagement"), + QStringLiteral("org.kde.Solid.PowerManagement"), + QDBusConnection::sessionBus()); + if (iface.isValid()) { + QDBusReply reply = iface.call(QStringLiteral("hasDualGpu")); + if (reply.isValid()) { + hasDiscreteGpu = reply.value(); + } + } + + s_gpuCheck = hasDiscreteGpu ? Present : Absent; + } + + if (service->runOnDiscreteGpu() && s_gpuCheck == Present) { + m_process->setEnv(QStringLiteral("DRI_PRIME"), QStringLiteral("1")); + } + + QString workingDir(service->workingDirectory()); + if (workingDir.isEmpty() && !urls.isEmpty() && urls.first().isLocalFile()) { + workingDir = urls.first().adjusted(QUrl::RemoveFilename).toLocalFile(); + } + m_process->setWorkingDirectory(workingDir); + + if ((flags & KIO::ProcessLauncherJob::DeleteTemporaryFiles) == 0) { + // Remember we opened those urls, for the "recent documents" menu in kicker + for (const QUrl &url : urls) { + KRecentDocument::add(url, service->desktopEntryName()); + } + } + + // m_executable can be a full shell command, so here is not 100% reliable. + // E.g. it could be "cd", which isn't an existing binary. It's just a heuristic anyway. + const QString bin = KIO::DesktopExecParser::executableName(m_executable); + init(service, bin, service->name(), service->icon(), windowId, asn); +} + +KProcessRunner::KProcessRunner(const QString &cmd, const QString &execName, const QString &iconName, WId windowId, const QByteArray &asn, const QString &workingDirectory) + : m_process{new KProcess}, + m_executable(execName) +{ + ++s_instanceCount; + m_process->setShellCommand(cmd); + if (!workingDirectory.isEmpty()) { + m_process->setWorkingDirectory(workingDirectory); + } + QString bin = KIO::DesktopExecParser::executableName(m_executable); + KService::Ptr service = KService::serviceByDesktopName(bin); + init(service, bin, + execName /*user-visible name*/, + iconName, windowId, asn); +} + +void KProcessRunner::init(const KService::Ptr &service, const QString &bin, const QString &userVisibleName, const QString &iconName, WId windowId, const QByteArray &asn) +{ + if (service && !service->entryPath().isEmpty() + && !KDesktopFile::isAuthorizedDesktopFile(service->entryPath())) { + qCWarning(KIO_GUI) << "No authorization to execute " << service->entryPath(); + emitDelayedError(i18n("You are not authorized to execute this file.")); + return; + } + +#if HAVE_X11 + static bool isX11 = QGuiApplication::platformName() == QLatin1String("xcb"); + if (isX11) { + bool silent; + QByteArray wmclass; + const bool startup_notify = (asn != "0" && KIOGuiPrivate::checkStartupNotify(service.data(), &silent, &wmclass)); + if (startup_notify) { + m_startupId.initId(asn); + m_startupId.setupStartupEnv(); + KStartupInfoData data; + data.setHostname(); + data.setBin(bin); + if (!userVisibleName.isEmpty()) { + data.setName(userVisibleName); + } else if (service && !service->name().isEmpty()) { + data.setName(service->name()); + } + data.setDescription(i18n("Launching %1", data.name())); + if (!iconName.isEmpty()) { + data.setIcon(iconName); + } else if (service && !service->icon().isEmpty()) { + data.setIcon(service->icon()); + } + if (!wmclass.isEmpty()) { + data.setWMClass(wmclass); + } + if (silent) { + data.setSilent(KStartupInfoData::Yes); + } + data.setDesktop(KWindowSystem::currentDesktop()); + if (windowId) { + data.setLaunchedBy(windowId); + } + if (service && !service->entryPath().isEmpty()) { + data.setApplicationId(service->entryPath()); + } + KStartupInfo::sendStartup(m_startupId, data); + } + } +#else + Q_UNUSED(bin); + Q_UNUSED(userVisibleName); + Q_UNUSED(iconName); +#endif + startProcess(); +} + +void KProcessRunner::startProcess() +{ + connect(m_process.get(), QOverload::of(&QProcess::finished), + this, &KProcessRunner::slotProcessExited); + connect(m_process.get(), &QProcess::started, + this, &KProcessRunner::slotProcessStarted, Qt::QueuedConnection); + connect(m_process.get(), &QProcess::errorOccurred, + this, &KProcessRunner::slotProcessError); + + m_process->start(); +} + +bool KProcessRunner::waitForStarted() +{ + return m_process->waitForStarted(); +} + +void KProcessRunner::slotProcessError(QProcess::ProcessError errorCode) +{ + // E.g. the process crashed. + // This is unlikely to happen while the ProcessLauncherJob is still connected to the KProcessRunner. + // So the emit does nothing, this is really just for debugging. + qCDebug(KIO_GUI) << m_executable << "error=" << errorCode << m_process->errorString(); + Q_EMIT error(m_process->errorString()); +} + +void KProcessRunner::slotProcessStarted() +{ + m_pid = m_process->processId(); + +#if HAVE_X11 + if (!m_startupId.isNull() && m_pid) { + KStartupInfoData data; + data.addPid(m_pid); + KStartupInfo::sendChange(m_startupId, data); + KStartupInfo::resetStartupEnv(); + } +#endif + emit processStarted(); +} + +KProcessRunner::~KProcessRunner() +{ + // This destructor deletes m_process, since it's a unique_ptr. + --s_instanceCount; +} + +int KProcessRunner::instanceCount() +{ + return s_instanceCount; +} + +qint64 KProcessRunner::pid() const +{ + return m_pid; +} + +void KProcessRunner::terminateStartupNotification() +{ +#if HAVE_X11 + if (!m_startupId.isNull()) { + KStartupInfoData data; + data.addPid(m_pid); // announce this pid for the startup notification has finished + data.setHostname(); + KStartupInfo::sendFinish(m_startupId, data); + } +#endif +} + +void KProcessRunner::emitDelayedError(const QString &errorMsg) +{ + terminateStartupNotification(); + // Use delayed invocation so the caller has time to connect to the signal + QMetaObject::invokeMethod(this, [this, errorMsg]() { + emit error(errorMsg); + deleteLater(); + }, Qt::QueuedConnection); +} + +void KProcessRunner::slotProcessExited(int exitCode, QProcess::ExitStatus exitStatus) +{ + qCDebug(KIO_GUI) << m_executable << "exitCode=" << exitCode << "exitStatus=" << exitStatus; + terminateStartupNotification(); + deleteLater(); +} + +// This code is also used in klauncher (and KRun). +bool KIOGuiPrivate::checkStartupNotify(const KService *service, bool *silent_arg, QByteArray *wmclass_arg) +{ + bool silent = false; + QByteArray wmclass; + if (service && service->property(QStringLiteral("StartupNotify")).isValid()) { + silent = !service->property(QStringLiteral("StartupNotify")).toBool(); + wmclass = service->property(QStringLiteral("StartupWMClass")).toString().toLatin1(); + } else if (service && service->property(QStringLiteral("X-KDE-StartupNotify")).isValid()) { + silent = !service->property(QStringLiteral("X-KDE-StartupNotify")).toBool(); + wmclass = service->property(QStringLiteral("X-KDE-WMClass")).toString().toLatin1(); + } else { // non-compliant app + if (service) { + if (service->isApplication()) { // doesn't have .desktop entries needed, start as non-compliant + wmclass = "0"; // krazy:exclude=doublequote_chars + } else { + return false; // no startup notification at all + } + } else { +#if 0 + // Create startup notification even for apps for which there shouldn't be any, + // just without any visual feedback. This will ensure they'll be positioned on the proper + // virtual desktop, and will get user timestamp from the ASN ID. + wmclass = '0'; + silent = true; +#else // That unfortunately doesn't work, when the launched non-compliant application + // launches another one that is compliant and there is any delay inbetween (bnc:#343359) + return false; +#endif + } + } + if (silent_arg) { + *silent_arg = silent; + } + if (wmclass_arg) { + *wmclass_arg = wmclass; + } + return true; +} diff --git a/src/gui/kprocessrunner_p.h b/src/gui/kprocessrunner_p.h new file mode 100644 --- /dev/null +++ b/src/gui/kprocessrunner_p.h @@ -0,0 +1,124 @@ +/* This file is part of the KDE libraries + Copyright (c) 2020 David Faure + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License or ( at + your option ) version 3 or, at the discretion of KDE e.V. ( which shall + act as a proxy as in section 14 of the GPLv3 ), 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 Lesser General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KPROCESSRUNNER_P_H +#define KPROCESSRUNNER_P_H + +#include "processlauncherjob.h" +#include "kiogui_export.h" + +#include + +#include +#include // WId +#include +#include + +namespace KIOGuiPrivate { +/** + * @internal DO NOT USE + */ +bool KIOGUI_EXPORT checkStartupNotify(const KService *service, bool *silent_arg, + QByteArray *wmclass_arg); +} + +/** + * @internal (exported for KRun, currently) + * This class runs a KService or a shell command, using QProcess internally. + * It creates a startup notification and finishes it on success or on error (for the taskbar) + * It also shows an error message if necessary (e.g. "program not found"). + */ +class KIOGUI_EXPORT KProcessRunner : public QObject +{ + Q_OBJECT + +public: + /** + * Run a KService (application desktop file) to open @p urls. + * @param service the service to run + * @param urls the list of URLs, can be empty + * @param windowId the identifier of window of the app that invoked this class. + * @param flags various flags + * @param suggestedFileName see KRun::setSuggestedFileName + * @param asn Application startup notification id, if any (otherwise ""). + + */ + KProcessRunner(const KService::Ptr &service, const QList &urls, WId windowId, + KIO::ProcessLauncherJob::RunFlags flags = {}, const QString &suggestedFileName = {}, const QByteArray &asn = {}); + + /** + * Run a shell command + * @param cmd must be a shell command. No need to append "&" to it. + * @param execName the name of the executable, if known. This improves startup notification, + * as well as honoring various flags coming from the desktop file for this executable, if there's one. + * @param iconName icon for the startup notification + * @param windowId the identifier of window of the app that invoked this class. + * @param asn Application startup notification id, if any (otherwise ""). + * @param workingDirectory the working directory for the started process. The default + * (if passing an empty string) is the user's document path. + * This allows a command like "kwrite file.txt" to find file.txt from the right place. + */ + KProcessRunner(const QString &cmd, const QString &execName, const QString &iconName, + WId windowId, const QByteArray &asn = {}, const QString &workingDirectory = {}); + + /** + * @return the PID of the process that was started, on success + */ + qint64 pid() const; + + bool waitForStarted(); + + virtual ~KProcessRunner(); + + static int instanceCount(); // for the unittest + +Q_SIGNALS: + /** + * @brief Emitted on error. In that case, finished() is not emitted. + * @param errorString the error message + */ + void error(const QString &errorString); + + /** + * @brief emitted when the process was successfully started + */ + void processStarted(); + +private Q_SLOTS: + void slotProcessExited(int, QProcess::ExitStatus); + void slotProcessError(QProcess::ProcessError error); + void slotProcessStarted(); + +private: + void init(const KService::Ptr &service, const QString &bin, const QString &userVisibleName, + const QString &iconName, WId windowId, const QByteArray &asn); + void startProcess(); + void terminateStartupNotification(); + void emitDelayedError(const QString &errorMsg); + + std::unique_ptr m_process; + const QString m_executable; // can be a full path + KStartupInfoId m_startupId; + qint64 m_pid = 0; + + Q_DISABLE_COPY(KProcessRunner) +}; + +#endif diff --git a/src/gui/processlauncherjob.h b/src/gui/processlauncherjob.h new file mode 100644 --- /dev/null +++ b/src/gui/processlauncherjob.h @@ -0,0 +1,138 @@ +/* + This file is part of the KDE libraries + Copyright (c) 2020 David Faure + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License or ( at + your option ) version 3 or, at the discretion of KDE e.V. ( which shall + act as a proxy as in section 14 of the GPLv3 ), 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 Lesser General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KIO_PROCESSLAUNCHERJOB_H +#define KIO_PROCESSLAUNCHERJOB_H + +#include "kiogui_export.h" +#include +#include +#include // WId +#include + +namespace KIO { + +class ProcessLauncherJobPrivate; + +/** + * @brief ProcessLauncherJob runs a process (application) and watches it while running. + * + * It creates a startup notification and finishes it on success or on error (for the taskbar). + * It also emits an error message if necessary (e.g. "program not found"). + * + * Note that this class doesn't support warning the user if a desktop file or a binary + * does not have the executable bit set and offering to make it so. Therefore file managers + * should use KRun::runApplication rather than using ProcessLauncherJob directly. + * + * The @p pid() will be available immediately after start(), but the job will only finish + * when the application exits. + * + * Deleting the job while the application is running, will leave it running, but this means + * there won't be any chance to terminate startup notification if the application crashes + * on startup before it gets a chance to do that on its own. + * + * @since 5.69 + */ +class KIOGUI_EXPORT ProcessLauncherJob : public KJob +{ +public: + /** + * @brief Creates a ProcessLauncherJob + * @param service the service (application desktop file) to run + * @param windowId the identifier of the window requesting this. Used for KStartupInfo::setLaunchedBy. ### TODO: launchedBy is unused? Remove? + * @param parent the parent QObject + */ + explicit ProcessLauncherJob(const KService::Ptr &service, WId windowId, QObject *parent = nullptr); + + /** + * Destructor + * Note that jobs auto-delete themselves after emitting result + */ + ~ProcessLauncherJob() override; + + /** + * @brief setUrls specifies the URLs to be passed to the application + * @param urls list of files (local or remote) to open + * + * Note that when passing multiple URLs to an application that doesn't support opening + * multiple files, the application will be launched once for each URL. + */ + void setUrls(const QList &urls); + + enum RunFlag { + DeleteTemporaryFiles = 0x1, ///< the URLs passed to the service will be deleted when it exits (if the URLs are local files) + }; + Q_DECLARE_FLAGS(RunFlags, RunFlag) + + /** + * @brief setRunFlags specifies various flags + * @param runFlags the flags to be set. For instance, whether the URLs are temporary files that should be deleted after execution. + */ + void setRunFlags(RunFlags runFlags); + + /** + * Sets the file name to use in the case of downloading the file to a tempfile + * in order to give to a non-url-aware application. Some apps rely on the extension + * to determine the mimetype of the file. Usually the file name comes from the URL, + * but in the case of the HTTP Content-Disposition header, we need to override the + * file name. + * @param suggestedFileName the file name + */ + void setSuggestedFileName(const QString &suggestedFileName); + + /** + * @brief setStartupId sets the startupId of the new application + * @param startupId Application startup notification id, if any (otherwise ""). + */ + void setStartupId(const QByteArray &startupId); + + /** + * @brief start starts the job. You must call this, after all the setters. + */ + void start() override; + + /** + * Blocks until the process has started. + */ + bool waitForStarted(); + + /** + * @return the PID of the application that was started. + * Convenience method for pids().at(0). You should only use this when specifying zero or one URL, + * or when you are sure that the application supports opening multiple files. Otherwise use pids(). + * Available after the job emits result(). + */ + qint64 pid() const; + + /** + * @return the PIDs of the applications that were started. + * Available after the job emits result(). + */ + QVector pids() const; + +private: + friend class ProcessLauncherJobPrivate; + QScopedPointer d; +}; + +} // namespace KIO + +#endif diff --git a/src/gui/processlauncherjob.cpp b/src/gui/processlauncherjob.cpp new file mode 100644 --- /dev/null +++ b/src/gui/processlauncherjob.cpp @@ -0,0 +1,134 @@ +/* + This file is part of the KDE libraries + Copyright (c) 2020 David Faure + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License or ( at + your option ) version 3 or, at the discretion of KDE e.V. ( which shall + act as a proxy as in section 14 of the GPLv3 ), 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 Lesser 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 "processlauncherjob.h" +#include "kprocessrunner_p.h" +#include "kiogui_debug.h" + +class KIO::ProcessLauncherJobPrivate +{ +public: + ProcessLauncherJobPrivate(const KService::Ptr &service, WId windowId) + : m_service(service), m_windowId(windowId) {} + + void slotStarted(KIO::ProcessLauncherJob *q, KProcessRunner *processRunner) { + m_pids.append(processRunner->pid()); + if (--m_numProcessesPending == 0) { + q->emitResult(); + } + } + const KService::Ptr m_service; + const WId m_windowId; + QList m_urls; + KIO::ProcessLauncherJob::RunFlags m_runFlags; + QString m_suggestedFileName; + QByteArray m_startupId; + QVector m_pids; + QVector m_processRunners; + int m_numProcessesPending = 0; +}; + +KIO::ProcessLauncherJob::ProcessLauncherJob(const KService::Ptr &service, WId windowId, QObject *parent) + : KJob(parent), d(new ProcessLauncherJobPrivate(service, windowId)) +{ +} + +KIO::ProcessLauncherJob::~ProcessLauncherJob() +{ + // Do *NOT* delete the KProcessRunner instances here. + // We need it to keep running so it can do terminate startup notification on process exit. +} + +void KIO::ProcessLauncherJob::setUrls(const QList &urls) +{ + d->m_urls = urls; +} + +void KIO::ProcessLauncherJob::setRunFlags(RunFlags runFlags) +{ + d->m_runFlags = runFlags; +} + +void KIO::ProcessLauncherJob::setSuggestedFileName(const QString &suggestedFileName) +{ + d->m_suggestedFileName = suggestedFileName; +} + +void KIO::ProcessLauncherJob::setStartupId(const QByteArray &startupId) +{ + d->m_startupId = startupId; +} + +void KIO::ProcessLauncherJob::start() +{ + if (d->m_urls.count() > 1 && !d->m_service->allowMultipleFiles()) { + // We need to launch the application N times. + // We ignore the result for application 2 to N. + // For the first file we launch the application in the + // usual way. The reported result is based on this application. + d->m_numProcessesPending = d->m_urls.count(); + d->m_processRunners.reserve(d->m_numProcessesPending); + for (int i = 1; i < d->m_urls.count(); ++i) { + auto *processRunner = new KProcessRunner(d->m_service, { d->m_urls.at(i) }, d->m_windowId, + d->m_runFlags, d->m_suggestedFileName, QByteArray()); + d->m_processRunners.push_back(processRunner); + connect(processRunner, &KProcessRunner::processStarted, this, [this, processRunner]() { + d->slotStarted(this, processRunner); + }); + } + d->m_urls = { d->m_urls.at(0) }; + } else { + d->m_numProcessesPending = 1; + } + + auto *processRunner = new KProcessRunner(d->m_service, d->m_urls, d->m_windowId, + d->m_runFlags, d->m_suggestedFileName, d->m_startupId); + d->m_processRunners.push_back(processRunner); + connect(processRunner, &KProcessRunner::error, this, [this](const QString &errorText) { + setError(KJob::UserDefinedError); + setErrorText(errorText); + emitResult(); + }); + connect(processRunner, &KProcessRunner::processStarted, this, [this, processRunner]() { + d->slotStarted(this, processRunner); + }); +} + +bool KIO::ProcessLauncherJob::waitForStarted() +{ + const bool ret = std::all_of(d->m_processRunners.cbegin(), + d->m_processRunners.cend(), + [](KProcessRunner *r) { return r->waitForStarted(); }); + for (KProcessRunner *r : qAsConst(d->m_processRunners)) { + qApp->sendPostedEvents(r); // so slotStarted gets called + } + return ret; +} + +qint64 KIO::ProcessLauncherJob::pid() const +{ + return d->m_pids.at(0); +} + +QVector KIO::ProcessLauncherJob::pids() const +{ + return d->m_pids; +} diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -116,6 +116,7 @@ target_link_libraries(KF5KIOWidgets PUBLIC + KF5::KIOGui KF5::KIOCore KF5::JobWidgets KF5::Service @@ -223,5 +224,5 @@ set(KIOWidgets_QCH_SOURCES ${KIOWidgets_HEADERS} ${KIO_namespaced_widgets_HEADERS} PARENT_SCOPE) include(ECMGeneratePriFile) -ecm_generate_pri_file(BASE_NAME KIOWidgets LIB_NAME KF5KIOWidgets DEPS "KIOCore KBookmarks KXmlGui Solid" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KIOWidgets) +ecm_generate_pri_file(BASE_NAME KIOWidgets LIB_NAME KF5KIOWidgets DEPS "KIOGui KIOCore KBookmarks KXmlGui Solid" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KIOWidgets) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) diff --git a/src/widgets/krun.h b/src/widgets/krun.h --- a/src/widgets/krun.h +++ b/src/widgets/krun.h @@ -252,8 +252,10 @@ /** * Run an application (known from its .desktop file, i.e. as a KService) * - * Unlike runService, this does not wait for the application to register to D-Bus - * before returning. Such behavior is better done with D-Bus activation anyway. + * If you need to wait for the application to register to D-Bus, use D-Bus activation instead. + * + * If you don't need the prompt for asking the user whether to add the executable bit for + * desktop files or binaries that don't have it, you can use KIO::ProcessLauncherJob from KIOGui directly. * * @param service the service to run * @param urls the list of URLs, can be empty (app launched diff --git a/src/widgets/krun.cpp b/src/widgets/krun.cpp --- a/src/widgets/krun.cpp +++ b/src/widgets/krun.cpp @@ -41,10 +41,6 @@ #include #include #include -#include -#include -#include -#include #include #include #include @@ -59,6 +55,8 @@ #include "krecentdocument.h" #include "kdesktopfileactions.h" #include +#include "kprocessrunner_p.h" +#include "processlauncherjob.h" #include #include @@ -115,9 +113,24 @@ QEventLoopLocker locker; KMessageBox::sorry(widget, errorString); }); + processRunner->waitForStarted(); return processRunner->pid(); } +static qint64 runProcessLauncherJob(KIO::ProcessLauncherJob *job, QWidget *widget) +{ + QObject *receiver = widget ? static_cast(widget) : static_cast(qApp); + QObject::connect(job, &KJob::result, receiver, [widget](KJob *job) { + if (job->error()) { + QEventLoopLocker locker; + KMessageBox::sorry(widget, job->errorString()); + } + }); + job->start(); + job->waitForStarted(); + return job->pid(); +} + // --------------------------------------------------------------------------- // Helper function that returns whether a file has the execute bit set or not. @@ -474,77 +487,11 @@ } // This code is also used in klauncher. -// TODO: move this to KProcessRunner +// TODO: port klauncher to KIOGuiPrivate::checkStartupNotify once this lands +// TODO: then deprecate this method, and remove in KF6 bool KRun::checkStartupNotify(const QString & /*binName*/, const KService *service, bool *silent_arg, QByteArray *wmclass_arg) { - bool silent = false; - QByteArray wmclass; - if (service && service->property(QStringLiteral("StartupNotify")).isValid()) { - silent = !service->property(QStringLiteral("StartupNotify")).toBool(); - wmclass = service->property(QStringLiteral("StartupWMClass")).toString().toLatin1(); - } else if (service && service->property(QStringLiteral("X-KDE-StartupNotify")).isValid()) { - silent = !service->property(QStringLiteral("X-KDE-StartupNotify")).toBool(); - wmclass = service->property(QStringLiteral("X-KDE-WMClass")).toString().toLatin1(); - } else { // non-compliant app - if (service) { - if (service->isApplication()) { // doesn't have .desktop entries needed, start as non-compliant - wmclass = "0"; // krazy:exclude=doublequote_chars - } else { - return false; // no startup notification at all - } - } else { -#if 0 - // Create startup notification even for apps for which there shouldn't be any, - // just without any visual feedback. This will ensure they'll be positioned on the proper - // virtual desktop, and will get user timestamp from the ASN ID. - wmclass = '0'; - silent = true; -#else // That unfortunately doesn't work, when the launched non-compliant application - // launches another one that is compliant and there is any delay inbetween (bnc:#343359) - return false; -#endif - } - } - if (silent_arg) { - *silent_arg = silent; - } - if (wmclass_arg) { - *wmclass_arg = wmclass; - } - return true; -} - -static qint64 runApplicationImpl(const KService::Ptr &service, const QList &_urls, QWidget *window, - KRun::RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) -{ - QList urlsToRun = _urls; - if ((_urls.count() > 1) && !service->allowMultipleFiles()) { - // We need to launch the application N times. That sucks. - // We ignore the result for application 2 to N. - // For the first file we launch the application in the - // usual way. The reported result is based on this - // application. - QList::ConstIterator it = _urls.begin(); - while (++it != _urls.end()) { - QList singleUrl; - singleUrl.append(*it); - runApplicationImpl(service, singleUrl, window, flags, suggestedFileName, QByteArray()); - } - urlsToRun.clear(); - urlsToRun.append(_urls.first()); - } - // QTBUG-59017 Calling winId() on an embedded widget will break interaction - // with it on high-dpi multi-screen setups (cf. also Bug 363548), hence using - // its parent window instead - auto windowId = WId{}; - if (window) { - window = window->window(); - windowId = window ? window->winId() : WId{}; - } - - auto *processRunner = new KProcessRunner(service, urlsToRun, - windowId, flags, suggestedFileName, asn); - return runProcessRunner(processRunner, window); + return KIOGuiPrivate::checkStartupNotify(service, silent_arg, wmclass_arg); } // WARNING: don't call this from DesktopExecParser, since klauncher uses that too... @@ -712,15 +659,24 @@ return 0; } - if (!flags.testFlag(DeleteTemporaryFiles)) { - // Remember we opened those urls, for the "recent documents" menu in kicker - for (const QUrl &url : urls) { - KRecentDocument::add(url, service.desktopEntryName()); - } + KService::Ptr servicePtr(new KService(service)); // clone + // QTBUG-59017 Calling winId() on an embedded widget will break interaction + // with it on high-dpi multi-screen setups (cf. also Bug 363548), hence using + // its parent window instead + auto windowId = WId{}; + if (window) { + window = window->window(); + windowId = window ? window->winId() : WId{}; } - KService::Ptr servicePtr(new KService(service)); // clone - return runApplicationImpl(servicePtr, urls, window, flags, suggestedFileName, asn); + KIO::ProcessLauncherJob *job = new KIO::ProcessLauncherJob(servicePtr, windowId); + job->setUrls(urls); + if (flags & DeleteTemporaryFiles) { + job->setRunFlags(KIO::ProcessLauncherJob::DeleteTemporaryFiles); + } + job->setSuggestedFileName(suggestedFileName); + job->setStartupId(asn); + return runProcessLauncherJob(job, window); } qint64 KRun::runService(const KService &_service, const QList &_urls, QWidget *window, @@ -1489,224 +1445,6 @@ return d->m_strURL.isLocalFile(); } -/****************/ - -KProcessRunner::KProcessRunner(const KService::Ptr &service, const QList &urls, WId windowId, - KRun::RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) - : m_process{new KProcess}, - m_executable(KIO::DesktopExecParser::executablePath(service->exec())) -{ - KIO::DesktopExecParser execParser(*service, urls); - execParser.setUrlsAreTempFiles(flags & KRun::DeleteTemporaryFiles); - execParser.setSuggestedFileName(suggestedFileName); - const QStringList args = execParser.resultingArguments(); - if (args.isEmpty()) { - emitDelayedError(i18n("Error processing Exec field in %1", service->entryPath())); - return; - } - //qDebug() << "runTempService: KProcess args=" << args; - *m_process << args; - - enum DiscreteGpuCheck { NotChecked, Present, Absent }; - static DiscreteGpuCheck s_gpuCheck = NotChecked; - - if (service->runOnDiscreteGpu() && s_gpuCheck == NotChecked) { - // Check whether we have a discrete gpu - bool hasDiscreteGpu = false; - QDBusInterface iface(QStringLiteral("org.kde.Solid.PowerManagement"), - QStringLiteral("/org/kde/Solid/PowerManagement"), - QStringLiteral("org.kde.Solid.PowerManagement"), - QDBusConnection::sessionBus()); - if (iface.isValid()) { - QDBusReply reply = iface.call(QStringLiteral("hasDualGpu")); - if (reply.isValid()) { - hasDiscreteGpu = reply.value(); - } - } - - s_gpuCheck = hasDiscreteGpu ? Present : Absent; - } - - if (service->runOnDiscreteGpu() && s_gpuCheck == Present) { - m_process->setEnv(QStringLiteral("DRI_PRIME"), QStringLiteral("1")); - } - - QString workingDir(service->workingDirectory()); - if (workingDir.isEmpty() && !urls.isEmpty() && urls.first().isLocalFile()) { - workingDir = urls.first().adjusted(QUrl::RemoveFilename).toLocalFile(); - } - m_process->setWorkingDirectory(workingDir); - - if ((flags & KRun::DeleteTemporaryFiles) == 0) { - // Remember we opened those urls, for the "recent documents" menu in kicker - for (const QUrl &url : urls) { - KRecentDocument::add(url, service->desktopEntryName()); - } - } - - const QString bin = KIO::DesktopExecParser::executableName(m_executable); - init(service, bin, service->name(), service->icon(), windowId, asn); -} - -KProcessRunner::KProcessRunner(const QString &cmd, const QString &execName, const QString &iconName, WId windowId, const QByteArray &asn, const QString &workingDirectory) - : m_process{new KProcess}, - m_executable(execName) -{ - m_process->setShellCommand(cmd); - if (!workingDirectory.isEmpty()) { - m_process->setWorkingDirectory(workingDirectory); - } - QString bin = KIO::DesktopExecParser::executableName(m_executable); - KService::Ptr service = KService::serviceByDesktopName(bin); - init(service, bin, - execName /*user-visible name*/, - iconName, windowId, asn); -} - -void KProcessRunner::init(const KService::Ptr &service, const QString &bin, const QString &userVisibleName, const QString &iconName, WId windowId, const QByteArray &asn) -{ - if (service && !service->entryPath().isEmpty() - && !KDesktopFile::isAuthorizedDesktopFile(service->entryPath())) { - qCWarning(KIO_WIDGETS) << "No authorization to execute " << service->entryPath(); - emitDelayedError(i18n("You are not authorized to execute this file.")); - return; - } - -#if HAVE_X11 - static bool isX11 = QGuiApplication::platformName() == QLatin1String("xcb"); - if (isX11) { - bool silent; - QByteArray wmclass; - const bool startup_notify = (asn != "0" && KRun::checkStartupNotify(QString() /*unused*/, service.data(), &silent, &wmclass)); - if (startup_notify) { - m_startupId.initId(asn); - m_startupId.setupStartupEnv(); - KStartupInfoData data; - data.setHostname(); - data.setBin(bin); - if (!userVisibleName.isEmpty()) { - data.setName(userVisibleName); - } else if (service && !service->name().isEmpty()) { - data.setName(service->name()); - } - data.setDescription(i18n("Launching %1", data.name())); - if (!iconName.isEmpty()) { - data.setIcon(iconName); - } else if (service && !service->icon().isEmpty()) { - data.setIcon(service->icon()); - } - if (!wmclass.isEmpty()) { - data.setWMClass(wmclass); - } - if (silent) { - data.setSilent(KStartupInfoData::Yes); - } - data.setDesktop(KWindowSystem::currentDesktop()); - if (windowId) { - data.setLaunchedBy(windowId); - } - if (service && !service->entryPath().isEmpty()) { - data.setApplicationId(service->entryPath()); - } - KStartupInfo::sendStartup(m_startupId, data); - } - } -#else - Q_UNUSED(bin); - Q_UNUSED(userVisibleName); - Q_UNUSED(iconName); -#endif - startProcess(); -} - -void KProcessRunner::startProcess() -{ - connect(m_process.get(), QOverload::of(&QProcess::finished), - this, &KProcessRunner::slotProcessExited); - - m_process->start(); - if (!m_process->waitForStarted()) { - //qDebug() << "wait for started failed, exitCode=" << process->exitCode() - // << "exitStatus=" << process->exitStatus(); - // Note that exitCode is 255 here (the first time), and 0 later on (bug?). - - // Use delayed invocation so the caller has time to connect to the signal - QMetaObject::invokeMethod(this, [this]() { - slotProcessExited(255, m_process->exitStatus()); - }, Qt::QueuedConnection); - } else { - m_pid = m_process->processId(); - -#if HAVE_X11 - if (!m_startupId.isNull() && m_pid) { - KStartupInfoData data; - data.addPid(m_pid); - KStartupInfo::sendChange(m_startupId, data); - KStartupInfo::resetStartupEnv(); - } -#endif - } -} - -KProcessRunner::~KProcessRunner() -{ - // This destructor deletes m_process, since it's a unique_ptr. -} - -qint64 KProcessRunner::pid() const -{ - return m_pid; -} - -void KProcessRunner::terminateStartupNotification() -{ -#if HAVE_X11 - if (!m_startupId.isNull()) { - KStartupInfoData data; - data.addPid(m_pid); // announce this pid for the startup notification has finished - data.setHostname(); - KStartupInfo::sendFinish(m_startupId, data); - } -#endif -} - -void KProcessRunner::emitDelayedError(const QString &errorMsg) -{ - terminateStartupNotification(); - // Use delayed invocation so the caller has time to connect to the signal - QMetaObject::invokeMethod(this, [this, errorMsg]() { - emit error(errorMsg); - deleteLater(); - }, Qt::QueuedConnection); -} - -void -KProcessRunner::slotProcessExited(int exitCode, QProcess::ExitStatus exitStatus) -{ - //qDebug() << m_executable << "exitCode=" << exitCode << "exitStatus=" << exitStatus; - Q_UNUSED(exitStatus) - - terminateStartupNotification(); // do this before the messagebox - - if (exitCode != 0 && !m_executable.isEmpty()) { - // Let's see if the error is because the exe doesn't exist. - // When this happens, waitForStarted returns false, but not if kioexec - // was involved, then we come here, that's why the code is here. - // - // We'll try to find the executable relatively to current directory, - // (or with a full path, if m_executable is absolute), and then in the PATH. - if (!QFile(m_executable).exists() && QStandardPaths::findExecutable(m_executable).isEmpty()) { - const QString &errorString = i18n("Could not find the program '%1'", m_executable); - qWarning() << errorString; - emit error(errorString); - } else { - //qDebug() << process->readAllStandardError(); - } - } - - deleteLater(); -} - #include "moc_krun.cpp" #include "moc_krun_p.cpp" #include "krun.moc" diff --git a/src/widgets/krun_p.h b/src/widgets/krun_p.h --- a/src/widgets/krun_p.h +++ b/src/widgets/krun_p.h @@ -33,77 +33,6 @@ #include "executablefileopendialog_p.h" #include "kstartupinfo.h" -/** - * @internal - * This class runs a KService or a shell command, using QProcess internally. - * It creates a startup notification and finishes it on success or on error (for the taskbar) - * It also shows an error message if necessary (e.g. "program not found"). - */ -class KProcessRunner : public QObject -{ - Q_OBJECT - -public: - /** - * Run a KService (application desktop file) to open @p urls. - * @param service the service to run - * @param urls the list of URLs, can be empty - * @param windowId the identifier of window of the app that invoked this class. - * @param flags various flags - * @param suggestedFileName see KRun::setSuggestedFileName - * @param asn Application startup notification id, if any (otherwise ""). - - */ - KProcessRunner(const KService::Ptr &service, const QList &urls, WId windowId, - KRun::RunFlags flags = {}, const QString &suggestedFileName = {}, const QByteArray &asn = {}); - - /** - * Run a shell command - * @param cmd must be a shell command. No need to append "&" to it. - * @param execName the name of the executable, if known. This improves startup notification, - * as well as honoring various flags coming from the desktop file for this executable, if there's one. - * @param iconName icon for the startup notification - * @param windowId the identifier of window of the app that invoked this class. - * @param asn Application startup notification id, if any (otherwise ""). - * @param workingDirectory the working directory for the started process. The default - * (if passing an empty string) is the user's document path. - * This allows a command like "kwrite file.txt" to find file.txt from the right place. - */ - KProcessRunner(const QString &cmd, const QString &execName, const QString &iconName, - WId windowId, const QByteArray &asn = {}, const QString &workingDirectory = {}); - - virtual ~KProcessRunner(); - - /** - * @return the PID of the process that was started, on success - */ - qint64 pid() const; - -Q_SIGNALS: - /** - * @brief Emitted on error - * @param errorString the error message - */ - void error(const QString &errorString); - -private Q_SLOTS: - void slotProcessExited(int, QProcess::ExitStatus); - -private: - void init(const KService::Ptr &service, const QString &bin, const QString &userVisibleName, - const QString &iconName, WId windowId, const QByteArray &asn); - void startProcess(); - void terminateStartupNotification(); - void emitDelayedError(const QString &errorMsg); - - std::unique_ptr m_process; - const QString m_executable; // can be a full path - KStartupInfoId m_startupId; - qint64 m_pid = 0; - - Q_DISABLE_COPY(KProcessRunner) -}; - /** * @internal */