diff --git a/src/core/desktopexecparser.cpp b/src/core/desktopexecparser.cpp index a3b52192..fcf83d9e 100644 --- a/src/core/desktopexecparser.cpp +++ b/src/core/desktopexecparser.cpp @@ -1,513 +1,521 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Torben Weis Copyright (C) 2006-2013 David Faure Copyright (C) 2009 Michael Pyne 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 "desktopexecparser.h" #include "kiofuse_interface.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 #include "kiocoredebug.h" class KRunMX1 : public KMacroExpanderBase { public: explicit KRunMX1(const KService &_service) : KMacroExpanderBase(QLatin1Char('%')) , hasUrls(false) , hasSpec(false) , service(_service) {} bool hasUrls; bool hasSpec; protected: int expandEscapedMacro(const QString &str, int pos, QStringList &ret) override; private: const KService &service; }; int KRunMX1::expandEscapedMacro(const QString &str, int pos, QStringList &ret) { uint option = str[pos + 1].unicode(); switch (option) { case 'c': ret << service.name().replace(QLatin1Char('%'), QLatin1String("%%")); break; case 'k': ret << service.entryPath().replace(QLatin1Char('%'), QLatin1String("%%")); break; case 'i': ret << QStringLiteral("--icon") << service.icon().replace(QLatin1Char('%'), QLatin1String("%%")); break; case 'm': // ret << "-miniicon" << service.icon().replace( '%', "%%" ); qCWarning(KIO_CORE) << "-miniicon isn't supported anymore (service" << service.name() << ')'; break; case 'u': case 'U': hasUrls = true; Q_FALLTHROUGH(); /* fallthrough */ case 'f': case 'F': case 'n': case 'N': case 'd': case 'D': case 'v': hasSpec = true; Q_FALLTHROUGH(); /* fallthrough */ default: return -2; // subst with same and skip } return 2; } class KRunMX2 : public KMacroExpanderBase { public: explicit KRunMX2(const QList &_urls) : KMacroExpanderBase(QLatin1Char('%')) , ignFile(false), urls(_urls) {} bool ignFile; protected: int expandEscapedMacro(const QString &str, int pos, QStringList &ret) override; private: void subst(int option, const QUrl &url, QStringList &ret); const QList &urls; }; void KRunMX2::subst(int option, const QUrl &url, QStringList &ret) { switch (option) { case 'u': ret << ((url.isLocalFile() && url.fragment().isNull() && url.query().isNull()) ? QDir::toNativeSeparators(url.toLocalFile()) : url.toString()); break; case 'd': ret << url.adjusted(QUrl::RemoveFilename).path(); break; case 'f': ret << QDir::toNativeSeparators(url.toLocalFile()); break; case 'n': ret << url.fileName(); break; case 'v': if (url.isLocalFile() && QFile::exists(url.toLocalFile())) { ret << KDesktopFile(url.toLocalFile()).desktopGroup().readEntry("Dev"); } break; } return; } int KRunMX2::expandEscapedMacro(const QString &str, int pos, QStringList &ret) { uint option = str[pos + 1].unicode(); switch (option) { case 'f': case 'u': case 'n': case 'd': case 'v': if (urls.isEmpty()) { if (!ignFile) { //qCDebug(KIO_CORE) << "No URLs supplied to single-URL service" << str; } } else if (urls.count() > 1) { qCWarning(KIO_CORE) << urls.count() << "URLs supplied to single-URL service" << str; } else { subst(option, urls.first(), ret); } break; case 'F': case 'U': case 'N': case 'D': option += 'a' - 'A'; for (const QUrl &url : urls) { subst(option, url, ret); } break; case '%': ret = QStringList(QStringLiteral("%")); break; default: return -2; // subst with same and skip } return 2; } QStringList KIO::DesktopExecParser::supportedProtocols(const KService &service) { QStringList supportedProtocols = service.property(QStringLiteral("X-KDE-Protocols")).toStringList(); KRunMX1 mx1(service); QString exec = service.exec(); if (mx1.expandMacrosShellQuote(exec) && !mx1.hasUrls) { if (!supportedProtocols.isEmpty()) { qCWarning(KIO_CORE) << service.entryPath() << "contains a X-KDE-Protocols line but doesn't use %u or %U in its Exec line! This is inconsistent."; } return QStringList(); } else { if (supportedProtocols.isEmpty()) { // compat mode: assume KIO if not set and it's a KDE app (or a KDE service) const QStringList categories = service.property(QStringLiteral("Categories")).toStringList(); if (categories.contains(QLatin1String("KDE")) || !service.isApplication() || service.entryPath().isEmpty() /*temp service*/) { supportedProtocols.append(QStringLiteral("KIO")); } else { // if no KDE app, be a bit over-generic supportedProtocols.append(QStringLiteral("http")); supportedProtocols.append(QStringLiteral("https")); // #253294 supportedProtocols.append(QStringLiteral("ftp")); } } } // add x-scheme-handler/ const auto servicesTypes = service.serviceTypes(); for (const auto &mimeType : servicesTypes) { if (mimeType.startsWith(QLatin1String("x-scheme-handler/"))) { supportedProtocols << mimeType.mid(17); } } //qCDebug(KIO_CORE) << "supportedProtocols:" << supportedProtocols; return supportedProtocols; } bool KIO::DesktopExecParser::isProtocolInSupportedList(const QUrl &url, const QStringList &supportedProtocols) { if (supportedProtocols.contains(QLatin1String("KIO"))) { return true; } return url.isLocalFile() || supportedProtocols.contains(url.scheme().toLower()); } // We have up to two sources of data, for protocols not handled by kioslaves (so called "helper") : // 1) the exec line of the .protocol file, if there's one // 2) the application associated with x-scheme-handler/ if there's one // If both exist, then: // A) if the .protocol file says "launch an application", then the new-style handler-app has priority // B) but if the .protocol file is for a kioslave (e.g. kio_http) then this has priority over // firefox or chromium saying x-scheme-handler/http. Gnome people want to send all HTTP urls // to a webbrowser, but we want mimetype-determination-in-calling-application by default // (the user can configure a BrowserApplication though) bool KIO::DesktopExecParser::hasSchemeHandler(const QUrl &url) { if (KProtocolInfo::isHelperProtocol(url)) { return true; } if (KProtocolInfo::isKnownProtocol(url)) { return false; // this is case B, we prefer kioslaves over the competition } const KService::Ptr service = KMimeTypeTrader::self()->preferredService(QLatin1String("x-scheme-handler/") + url.scheme()); if (service) { qCDebug(KIO_CORE) << QLatin1String("preferred service for x-scheme-handler/") + url.scheme() << service->desktopEntryName(); } return service; } class KIO::DesktopExecParserPrivate { public: DesktopExecParserPrivate(const KService &_service, const QList &_urls) : service(_service), urls(_urls), tempFiles(false) {} const KService &service; QList urls; bool tempFiles; QString suggestedFileName; }; KIO::DesktopExecParser::DesktopExecParser(const KService &service, const QList &urls) : d(new DesktopExecParserPrivate(service, urls)) { } KIO::DesktopExecParser::~DesktopExecParser() { } void KIO::DesktopExecParser::setUrlsAreTempFiles(bool tempFiles) { d->tempFiles = tempFiles; } void KIO::DesktopExecParser::setSuggestedFileName(const QString &suggestedFileName) { d->suggestedFileName = suggestedFileName; } static const QString kioexecPath() { QString kioexec = QCoreApplication::applicationDirPath() + QLatin1String("/kioexec"); if (!QFileInfo::exists(kioexec)) kioexec = QStringLiteral(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/kioexec"); Q_ASSERT(QFileInfo::exists(kioexec)); return kioexec; } QStringList KIO::DesktopExecParser::resultingArguments() const { QString exec = d->service.exec(); if (exec.isEmpty()) { qCWarning(KIO_CORE) << "No Exec field in `" << d->service.entryPath() << "' !"; return QStringList(); } QStringList result; bool appHasTempFileOption; KRunMX1 mx1(d->service); KRunMX2 mx2(d->urls); if (!mx1.expandMacrosShellQuote(exec)) { // Error in shell syntax qCWarning(KIO_CORE) << "Syntax error in command" << d->service.exec() << ", service" << d->service.name(); return QStringList(); } // FIXME: the current way of invoking kioexec disables term and su use // Check if we need "tempexec" (kioexec in fact) appHasTempFileOption = d->tempFiles && d->service.property(QStringLiteral("X-KDE-HasTempFileOption")).toBool(); if (d->tempFiles && !appHasTempFileOption && d->urls.size()) { result << kioexecPath() << QStringLiteral("--tempfiles") << exec; if (!d->suggestedFileName.isEmpty()) { result << QStringLiteral("--suggestedfilename"); result << d->suggestedFileName; } result += QUrl::toStringList(d->urls); return result; } // Return true for non-KIO desktop files with explicit X-KDE-Protocols list, like vlc, for the special case below auto isNonKIO = [this]() { const QStringList protocols = d->service.property(QStringLiteral("X-KDE-Protocols")).toStringList(); return !protocols.isEmpty() && !protocols.contains(QLatin1String("KIO")); }; // Check if we need kioexec, or KIOFuse bool useKioexec = false; org::kde::KIOFuse::VFS kiofuse_iface(QStringLiteral("org.kde.KIOFuse"), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus()); struct MountRequest { QDBusPendingReply reply; int urlIndex; }; QVector requests; requests.reserve(d->urls.count()); const QStringList appSupportedProtocols = supportedProtocols(d->service); for (int i = 0; i < d->urls.count(); ++i) { const QUrl url = d->urls.at(i); const bool supported = mx1.hasUrls ? isProtocolInSupportedList(url, appSupportedProtocols) : url.isLocalFile(); if (!supported) { // if FUSE fails, we'll have to fallback to kioexec useKioexec = true; } // NOTE: Some non-KIO apps may support the URLs (e.g. VLC supports smb://) // but will not have the password if they are not in the URL itself. // Hence convert URL to KIOFuse equivalent in case there is a password. // @see https://pointieststick.com/2018/01/17/videos-on-samba-shares/ // @see https://bugs.kde.org/show_bug.cgi?id=330192 if (!supported || (!url.userName().isEmpty() && url.password().isEmpty() && isNonKIO())) { requests.push_back({ kiofuse_iface.mountUrl(url.toString()), i }); } } for (auto &request : requests) { request.reply.waitForFinished(); } const bool fuseError = std::any_of(requests.cbegin(), requests.cend(), [](const MountRequest &request) { return request.reply.isError(); }); if (fuseError && useKioexec) { // We need to run the app through kioexec result << kioexecPath(); if (d->tempFiles) { result << QStringLiteral("--tempfiles"); } if (!d->suggestedFileName.isEmpty()) { result << QStringLiteral("--suggestedfilename"); result << d->suggestedFileName; } result << exec; result += QUrl::toStringList(d->urls); return result; } // At this point we know we're not using kioexec, so feel free to replace // KIO URLs with their KIOFuse local path. for (const auto &request : qAsConst(requests)) { if (!request.reply.isError()) { d->urls[request.urlIndex] = QUrl::fromLocalFile(request.reply.value()); } } if (appHasTempFileOption) { exec += QLatin1String(" --tempfile"); } // Did the user forget to append something like '%f'? // If so, then assume that '%f' is the right choice => the application // accepts only local files. if (!mx1.hasSpec) { exec += QLatin1String(" %f"); mx2.ignFile = true; } mx2.expandMacrosShellQuote(exec); // syntax was already checked, so don't check return value /* 1 = need_shell, 2 = terminal, 4 = su 0 << split(cmd) 1 << "sh" << "-c" << cmd 2 << split(term) << "-e" << split(cmd) 3 << split(term) << "-e" << "sh" << "-c" << cmd 4 << "kdesu" << "-u" << user << "-c" << cmd 5 << "kdesu" << "-u" << user << "-c" << ("sh -c " + quote(cmd)) 6 << split(term) << "-e" << "su" << user << "-c" << cmd 7 << split(term) << "-e" << "su" << user << "-c" << ("sh -c " + quote(cmd)) "sh -c" is needed in the "su" case, too, as su uses the user's login shell, not sh. this could be optimized with the -s switch of some su versions (e.g., debian linux). */ if (d->service.terminal()) { KConfigGroup cg(KSharedConfig::openConfig(), "General"); QString terminal = cg.readPathEntry("TerminalApplication", QStringLiteral("konsole")); - if (terminal == QLatin1String("konsole")) { + const bool isKonsole = (terminal == QLatin1String("konsole")); + + QString terminalPath = QStandardPaths::findExecutable(terminal); + if (terminalPath.isEmpty()) { + qCWarning(KIO_CORE) << "Terminal" << terminal << "not found, service" << d->service.name(); + return QStringList(); + } + terminal = terminalPath; + if (isKonsole) { if (!d->service.workingDirectory().isEmpty()) { terminal += QLatin1String(" --workdir ") + KShell::quoteArg(d->service.workingDirectory()); } terminal += QLatin1String(" -qwindowtitle '%c'"); if(!d->service.icon().isEmpty()) { terminal += QLatin1String(" -qwindowicon ") + KShell::quoteArg(d->service.icon().replace(QLatin1Char('%'), QLatin1String("%%"))); } } terminal += QLatin1Char(' ') + d->service.terminalOptions(); if (!mx1.expandMacrosShellQuote(terminal)) { qCWarning(KIO_CORE) << "Syntax error in command" << terminal << ", service" << d->service.name(); return QStringList(); } mx2.expandMacrosShellQuote(terminal); result = KShell::splitArgs(terminal); // assuming that the term spec never needs a shell! result << QStringLiteral("-e"); } KShell::Errors err; QStringList execlist = KShell::splitArgs(exec, KShell::AbortOnMeta | KShell::TildeExpand, &err); if (err == KShell::NoError && !execlist.isEmpty()) { // mx1 checked for syntax errors already const QString executable = execlist.at(0); if (QDir::isRelativePath(executable)) { // Resolve the executable to ensure that helpers in libexec are found. // Too bad for commands that need a shell - they must reside in $PATH. QString exePath = QStandardPaths::findExecutable(executable); if (exePath.isEmpty()) { exePath = QFile::decodeName(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/") + executable; } if (QFile::exists(exePath)) { execlist[0] = exePath; } } } if (d->service.substituteUid()) { if (d->service.terminal()) { result << QStringLiteral("su"); } else { QString kdesu = QFile::decodeName(CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "/kdesu"); if (!QFile::exists(kdesu)) { kdesu = QStandardPaths::findExecutable(QStringLiteral("kdesu")); } if (!QFile::exists(kdesu)) { // Insert kdesu as string so we show a nice warning: 'Could not launch kdesu' result << QStringLiteral("kdesu"); return result; } else { result << kdesu << QStringLiteral("-u"); } } result << d->service.username() << QStringLiteral("-c"); if (err == KShell::FoundMeta) { exec = QLatin1String("/bin/sh -c ") + KShell::quoteArg(exec); } else { exec = KShell::joinArgs(execlist); } result << exec; } else { if (err == KShell::FoundMeta) { result << QStringLiteral("/bin/sh") << QStringLiteral("-c") << exec; } else { result += execlist; } } return result; } //static QString KIO::DesktopExecParser::executableName(const QString &execLine) { const QString bin = executablePath(execLine); return bin.mid(bin.lastIndexOf(QLatin1Char('/')) + 1); } //static QString KIO::DesktopExecParser::executablePath(const QString &execLine) { // Remove parameters and/or trailing spaces. const QStringList args = KShell::splitArgs(execLine); for (const QString &arg : args) { if (!arg.contains(QLatin1Char('='))) { return arg; } } return QString(); } diff --git a/src/gui/kprocessrunner.cpp b/src/gui/kprocessrunner.cpp index a4701a7e..cc57b541 100644 --- a/src/gui/kprocessrunner.cpp +++ b/src/gui/kprocessrunner.cpp @@ -1,423 +1,424 @@ /* 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 #include #include #include #include #include #include #include static int s_instanceCount = 0; // for the unittest static QString findNonExecutableProgram(const QString &executable) { // Relative to current dir, or absolute path const QFileInfo fi(executable); if (fi.exists() && !fi.isExecutable()) { return executable; } #ifdef Q_OS_UNIX // This is a *very* simplified version of QStandardPaths::findExecutable #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) const auto skipEmptyParts = QString::SkipEmptyParts; #else const auto skipEmptyParts = Qt::SkipEmptyParts; #endif const QStringList searchPaths = QString::fromLocal8Bit(qgetenv("PATH")).split(QDir::listSeparator(), skipEmptyParts); for (const QString &searchPath : searchPaths) { const QString candidate = searchPath + QLatin1Char('/') + executable; const QFileInfo fileInfo(candidate); if (fileInfo.exists()) { if (fileInfo.isExecutable()) { qWarning() << "Internal program error. QStandardPaths::findExecutable couldn't find" << executable << "but our own logic found it at" << candidate << ". Please report a bug at https://bugs.kde.org"; } else { return candidate; } } } #endif return QString(); } KProcessRunner::KProcessRunner(const KService::Ptr &service, const QList &urls, KIO::ApplicationLauncherJob::RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) : m_process{new KProcess}, m_executable(KIO::DesktopExecParser::executablePath(service->exec())) { ++s_instanceCount; if (!service->isValid()) { emitDelayedError(i18n("The desktop entry file\n%1\nis not valid.", service->entryPath())); return; } + KIO::DesktopExecParser execParser(*service, urls); + execParser.setUrlsAreTempFiles(flags & KIO::ApplicationLauncherJob::DeleteTemporaryFiles); + execParser.setSuggestedFileName(suggestedFileName); + const QStringList args = execParser.resultingArguments(); + if (args.isEmpty()) { + emitDelayedError(i18n("Error processing Exec field in %1", service->entryPath())); + return; + } - const QString realExecutable = execParser.resultingArguments().at(0); + const QString realExecutable = args.at(0); // realExecutable is a full path if DesktopExecParser was able to locate it. Otherwise it's still relative, which is a bad sign. if (QDir::isRelativePath(realExecutable) || !QFileInfo(realExecutable).isExecutable()) { // Does it really not exist, or is it non-executable? (bug #415567) const QString nonExecutable = findNonExecutableProgram(realExecutable); if (nonExecutable.isEmpty()) { emitDelayedError(i18n("Could not find the program '%1'", realExecutable)); } else { emitDelayedError(i18n("The program '%1' was found at '%2' but it is missing executable permissions.", realExecutable, nonExecutable)); } return; } - execParser.setUrlsAreTempFiles(flags & KIO::ApplicationLauncherJob::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::ApplicationLauncherJob::DeleteTemporaryFiles) == 0) { // Remember we opened those urls, for the "recent documents" menu in kicker for (const QUrl &url : urls) { KRecentDocument::add(url, service->desktopEntryName()); } } init(service, service->name(), service->icon(), asn); } KProcessRunner::KProcessRunner(const QString &cmd, const QString &desktopName, const QString &execName, const QString &iconName, 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); } if (!desktopName.isEmpty()) { KService::Ptr service = KService::serviceByDesktopName(desktopName); if (service) { if (m_executable.isEmpty()) { m_executable = KIO::DesktopExecParser::executablePath(service->exec()); } init(service, service->name(), service->icon(), asn); return; } } init(KService::Ptr(), execName /*user-visible name*/, iconName, asn); } void KProcessRunner::init(const KService::Ptr &service, const QString &userVisibleName, const QString &iconName, 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(); // When it comes from a desktop file, 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); 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 (service && !service->entryPath().isEmpty()) { data.setApplicationId(service->entryPath()); } KStartupInfo::sendStartup(m_startupId, data); } } #else Q_UNUSED(bin); Q_UNUSED(userVisibleName); Q_UNUSED(iconName); #endif if (service) { m_scopeId = service->desktopEntryName(); } if (m_scopeId.isEmpty()) { m_scopeId = m_executable; } 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 ApplicationLauncherJob 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(); registerCGroup(); #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(); } void KProcessRunner::registerCGroup() { // As specified in "XDG standardization for applications" in https://systemd.io/DESKTOP_ENVIRONMENTS/ #ifdef Q_OS_LINUX if (!qEnvironmentVariableIsSet("KDE_APPLICATIONS_AS_SCOPE")) { return; } if (!QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.freedesktop.systemd1"))) { return; } typedef QPair NamedVariant; typedef QList NamedVariantList; static std::once_flag dbusTypesRegistered; std::call_once(dbusTypesRegistered, []() { qDBusRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType>(); qDBusRegisterMetaType>>(); }); QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"), QStringLiteral("/org/freedesktop/systemd1"), QStringLiteral("org.freedesktop.systemd1.Manager"), QStringLiteral("StartTransientUnit")); // "-" is a special character in systemd representing a heirachical level. It should be escaped. const QString escapedScopeId = m_scopeId.replace(QLatin1Char('-'), QStringLiteral("\\x2d")); const QString name = QStringLiteral("apps-%1-%2.scope").arg(escapedScopeId, QUuid::createUuid().toString(QUuid::Id128)); // mode defines what to do in the case of a name conflict, in this case, just do nothing const QString mode = QStringLiteral("fail"); const QList pidList = {static_cast(m_process->pid())}; NamedVariantList properties = {NamedVariant({QStringLiteral("PIDs"), QDBusVariant(QVariant::fromValue(pidList))})}; QList> aux; message.setArguments({name, mode, QVariant::fromValue(properties), QVariant::fromValue(aux)}); QDBusPendingCall reply = QDBusConnection::sessionBus().asyncCall(message); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); QObject::connect(watcher, &QDBusPendingCallWatcher::finished, qApp, [=]() { watcher->deleteLater(); if (reply.isError()) { qCWarning(KIO_GUI) << "Failed to register new cgroup:" << name; } else { qCDebug(KIO_GUI) << "Successfully registered new cgroup:" << name; } }); #endif } // 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; }