File indexing completed on 2024-04-28 15:26:52

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include "kprocessrunner_p.h"
0009 
0010 #if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
0011 #include "systemd/scopedprocessrunner_p.h"
0012 #include "systemd/systemdprocessrunner_p.h"
0013 #endif
0014 
0015 #include "config-kiogui.h"
0016 #include "dbusactivationrunner_p.h"
0017 #include "kiogui_debug.h"
0018 
0019 #include "desktopexecparser.h"
0020 #include "gpudetection_p.h"
0021 #include "krecentdocument.h"
0022 #include <KDesktopFile>
0023 #include <KLocalizedString>
0024 #include <KWindowSystem>
0025 
0026 #ifndef Q_OS_ANDROID
0027 #include <QDBusConnection>
0028 #include <QDBusInterface>
0029 #include <QDBusReply>
0030 #endif
0031 #include <QDir>
0032 #include <QFileInfo>
0033 #include <QGuiApplication>
0034 #include <QProcess>
0035 #include <QStandardPaths>
0036 #include <QString>
0037 #include <QTimer>
0038 #include <QUuid>
0039 
0040 #ifdef Q_OS_WIN
0041 #include "windows.h"
0042 
0043 #include "shellapi.h" // Must be included after "windows.h"
0044 #endif
0045 
0046 static int s_instanceCount = 0; // for the unittest
0047 
0048 KProcessRunner::KProcessRunner()
0049     : m_process{new KProcess}
0050 {
0051     ++s_instanceCount;
0052 }
0053 
0054 static KProcessRunner *makeInstance()
0055 {
0056 #if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
0057     if (SystemdProcessRunner::isAvailable()) {
0058         if (qEnvironmentVariableIntValue("KDE_APPLICATIONS_AS_SERVICE")) {
0059             return new SystemdProcessRunner();
0060         }
0061         if (qEnvironmentVariableIntValue("KDE_APPLICATIONS_AS_SCOPE")) {
0062             return new ScopedProcessRunner();
0063         }
0064     }
0065 #endif
0066     return new ForkingProcessRunner();
0067 }
0068 
0069 static void modifyEnv(KProcess &process, QProcessEnvironment mod)
0070 {
0071     QProcessEnvironment env = process.processEnvironment();
0072     if (env.isEmpty()) {
0073         env = QProcessEnvironment::systemEnvironment();
0074     }
0075     env.insert(mod);
0076     process.setProcessEnvironment(env);
0077 }
0078 
0079 KProcessRunner *KProcessRunner::fromApplication(const KService::Ptr &service,
0080                                                 const QString &serviceEntryPath,
0081                                                 const QList<QUrl> &urls,
0082                                                 KIO::ApplicationLauncherJob::RunFlags flags,
0083                                                 const QString &suggestedFileName,
0084                                                 const QByteArray &asn)
0085 {
0086     KProcessRunner *instance;
0087     // special case for applicationlauncherjob
0088     // FIXME: KProcessRunner is currently broken and fails to prepare the m_urls member
0089     // DBusActivationRunner uses, which then only calls "Activate", not "Open".
0090     // Possibly will need some special mode of DesktopExecParser
0091     // for the D-Bus activation call scenario to handle URLs with protocols
0092     // the invoked service/executable might not support.
0093     const bool notYetSupportedOpenActivationNeeded = !urls.isEmpty();
0094     if (!notYetSupportedOpenActivationNeeded && DBusActivationRunner::activationPossible(service, flags, suggestedFileName)) {
0095         const auto actions = service->actions();
0096         auto action = std::find_if(actions.cbegin(), actions.cend(), [service](const KServiceAction &action) {
0097             return action.exec() == service->exec();
0098         });
0099         instance = new DBusActivationRunner(action != actions.cend() ? action->name() : QString());
0100     } else {
0101         instance = makeInstance();
0102     }
0103 
0104     if (!service->isValid()) {
0105         instance->emitDelayedError(i18n("The desktop entry file\n%1\nis not valid.", serviceEntryPath));
0106         return instance;
0107     }
0108     instance->m_executable = KIO::DesktopExecParser::executablePath(service->exec());
0109 
0110     KIO::DesktopExecParser execParser(*service, urls);
0111     execParser.setUrlsAreTempFiles(flags & KIO::ApplicationLauncherJob::DeleteTemporaryFiles);
0112     execParser.setSuggestedFileName(suggestedFileName);
0113     const QStringList args = execParser.resultingArguments();
0114     if (args.isEmpty()) {
0115         instance->emitDelayedError(execParser.errorMessage());
0116         return instance;
0117     }
0118 
0119     qCDebug(KIO_GUI) << "Starting process:" << args;
0120     *instance->m_process << args;
0121 
0122 #ifndef Q_OS_ANDROID
0123     if (service->runOnDiscreteGpu()) {
0124         modifyEnv(*instance->m_process, KIO::discreteGpuEnvironment());
0125     }
0126 #endif
0127 
0128     QString workingDir(service->workingDirectory());
0129     if (workingDir.isEmpty() && !urls.isEmpty() && urls.first().isLocalFile()) {
0130         workingDir = urls.first().adjusted(QUrl::RemoveFilename).toLocalFile();
0131     }
0132     instance->m_process->setWorkingDirectory(workingDir);
0133 
0134     if ((flags & KIO::ApplicationLauncherJob::DeleteTemporaryFiles) == 0) {
0135         // Remember we opened those urls, for the "recent documents" menu in kicker
0136         for (const QUrl &url : urls) {
0137             KRecentDocument::add(url, service->desktopEntryName());
0138         }
0139     }
0140 
0141     instance->init(service, serviceEntryPath, service->name(), service->icon(), asn);
0142     return instance;
0143 }
0144 
0145 KProcessRunner *KProcessRunner::fromCommand(const QString &cmd,
0146                                             const QString &desktopName,
0147                                             const QString &execName,
0148                                             const QString &iconName,
0149                                             const QByteArray &asn,
0150                                             const QString &workingDirectory,
0151                                             const QProcessEnvironment &environment)
0152 {
0153     auto instance = makeInstance();
0154 
0155     instance->m_executable = KIO::DesktopExecParser::executablePath(execName);
0156     instance->m_cmd = cmd;
0157 #ifdef Q_OS_WIN
0158     if (cmd.startsWith(QLatin1String("wt.exe")) || cmd.startsWith(QLatin1String("pwsh.exe")) || cmd.startsWith(QLatin1String("powershell.exe"))) {
0159         instance->m_process->setCreateProcessArgumentsModifier([](QProcess::CreateProcessArguments *args) {
0160             args->flags |= CREATE_NEW_CONSOLE;
0161             args->startupInfo->dwFlags &= ~STARTF_USESTDHANDLES;
0162         });
0163         const int firstSpace = cmd.indexOf(QLatin1Char(' '));
0164         instance->m_process->setProgram(cmd.left(firstSpace));
0165         instance->m_process->setNativeArguments(cmd.mid(firstSpace + 1));
0166     } else
0167 #endif
0168         instance->m_process->setShellCommand(cmd);
0169 
0170     instance->initFromDesktopName(desktopName, execName, iconName, asn, workingDirectory, environment);
0171     return instance;
0172 }
0173 
0174 KProcessRunner *KProcessRunner::fromExecutable(const QString &executable,
0175                                                const QStringList &args,
0176                                                const QString &desktopName,
0177                                                const QString &iconName,
0178                                                const QByteArray &asn,
0179                                                const QString &workingDirectory,
0180                                                const QProcessEnvironment &environment)
0181 {
0182     const QString actualExec = QStandardPaths::findExecutable(executable);
0183     if (actualExec.isEmpty()) {
0184         qCWarning(KIO_GUI) << "Could not find an executable named:" << executable;
0185         return {};
0186     }
0187 
0188     auto instance = makeInstance();
0189 
0190     instance->m_executable = KIO::DesktopExecParser::executablePath(executable);
0191     instance->m_process->setProgram(executable, args);
0192     instance->initFromDesktopName(desktopName, executable, iconName, asn, workingDirectory, environment);
0193     return instance;
0194 }
0195 
0196 void KProcessRunner::initFromDesktopName(const QString &desktopName,
0197                                          const QString &execName,
0198                                          const QString &iconName,
0199                                          const QByteArray &asn,
0200                                          const QString &workingDirectory,
0201                                          const QProcessEnvironment &environment)
0202 {
0203     if (!workingDirectory.isEmpty()) {
0204         m_process->setWorkingDirectory(workingDirectory);
0205     }
0206     m_process->setProcessEnvironment(environment);
0207     if (!desktopName.isEmpty()) {
0208         KService::Ptr service = KService::serviceByDesktopName(desktopName);
0209         if (service) {
0210             if (m_executable.isEmpty()) {
0211                 m_executable = KIO::DesktopExecParser::executablePath(service->exec());
0212             }
0213             init(service, service->entryPath(), service->name(), service->icon(), asn);
0214             return;
0215         }
0216     }
0217     init(KService::Ptr(), QString{}, execName /*user-visible name*/, iconName, asn);
0218 }
0219 
0220 void KProcessRunner::init(const KService::Ptr &service,
0221                           const QString &serviceEntryPath,
0222                           const QString &userVisibleName,
0223                           const QString &iconName,
0224                           const QByteArray &asn)
0225 {
0226     m_serviceEntryPath = serviceEntryPath;
0227     if (service && !serviceEntryPath.isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(serviceEntryPath)) {
0228         qCWarning(KIO_GUI) << "No authorization to execute" << serviceEntryPath;
0229         emitDelayedError(i18n("You are not authorized to execute this file."));
0230         return;
0231     }
0232 
0233 #if HAVE_X11
0234     static bool isX11 = QGuiApplication::platformName() == QLatin1String("xcb");
0235     if (isX11) {
0236         bool silent;
0237         QByteArray wmclass;
0238         const bool startup_notify = (asn != "0" && KIOGuiPrivate::checkStartupNotify(service.data(), &silent, &wmclass));
0239         if (startup_notify) {
0240             m_startupId.initId(asn);
0241             m_startupId.setupStartupEnv();
0242             KStartupInfoData data;
0243             data.setHostname();
0244             // When it comes from a desktop file, m_executable can be a full shell command, so <bin> here is not 100% reliable.
0245             // E.g. it could be "cd", which isn't an existing binary. It's just a heuristic anyway.
0246             const QString bin = KIO::DesktopExecParser::executableName(m_executable);
0247             data.setBin(bin);
0248             if (!userVisibleName.isEmpty()) {
0249                 data.setName(userVisibleName);
0250             } else if (service && !service->name().isEmpty()) {
0251                 data.setName(service->name());
0252             }
0253             data.setDescription(i18n("Launching %1", data.name()));
0254             if (!iconName.isEmpty()) {
0255                 data.setIcon(iconName);
0256             } else if (service && !service->icon().isEmpty()) {
0257                 data.setIcon(service->icon());
0258             }
0259             if (!wmclass.isEmpty()) {
0260                 data.setWMClass(wmclass);
0261             }
0262             if (silent) {
0263                 data.setSilent(KStartupInfoData::Yes);
0264             }
0265             if (service && !serviceEntryPath.isEmpty()) {
0266                 data.setApplicationId(serviceEntryPath);
0267             }
0268             KStartupInfo::sendStartup(m_startupId, data);
0269         }
0270     }
0271 #else
0272     Q_UNUSED(userVisibleName);
0273     Q_UNUSED(iconName);
0274 #endif
0275 
0276     if (KWindowSystem::isPlatformWayland()) {
0277         if (!asn.isEmpty()) {
0278             m_process->setEnv(QStringLiteral("XDG_ACTIVATION_TOKEN"), QString::fromUtf8(asn));
0279         } else {
0280             bool silent;
0281             QByteArray wmclass;
0282             const bool startup_notify = service && KIOGuiPrivate::checkStartupNotify(service.data(), &silent, &wmclass);
0283             if (startup_notify && !silent) {
0284                 auto window = qGuiApp->focusWindow();
0285                 if (!window && !qGuiApp->allWindows().isEmpty()) {
0286                     window = qGuiApp->allWindows().constFirst();
0287                 }
0288                 if (window) {
0289                     const int launchedSerial = KWindowSystem::lastInputSerial(window);
0290                     m_waitingForXdgToken = true;
0291                     connect(this, &KProcessRunner::xdgActivationTokenArrived, m_process.get(), [this] {
0292                         startProcess();
0293                     });
0294                     connect(KWindowSystem::self(),
0295                             &KWindowSystem::xdgActivationTokenArrived,
0296                             m_process.get(),
0297                             [this, launchedSerial](int tokenSerial, const QString &token) {
0298                                 if (tokenSerial == launchedSerial) {
0299                                     m_process->setEnv(QStringLiteral("XDG_ACTIVATION_TOKEN"), token);
0300                                     Q_EMIT xdgActivationTokenArrived();
0301                                     m_waitingForXdgToken = false;
0302                                 }
0303                             });
0304                     KWindowSystem::requestXdgActivationToken(window, launchedSerial, maybeAliasedName(QFileInfo(m_serviceEntryPath).completeBaseName()));
0305                 }
0306             }
0307         }
0308     }
0309 
0310     if (service) {
0311         m_service = service;
0312         // Store the desktop name, used by debug output and for the systemd unit name
0313         m_desktopName = service->menuId();
0314         if (m_desktopName.isEmpty() && m_executable == QLatin1String("systemsettings5") && m_service->hasServiceType(QLatin1String("KCModule"))) {
0315             m_desktopName = QStringLiteral("systemsettings.desktop");
0316         }
0317         if (m_desktopName.endsWith(QLatin1String(".desktop"))) { // always true, in theory
0318             m_desktopName.chop(strlen(".desktop"));
0319         }
0320         if (m_desktopName.isEmpty()) { // desktop files not in the menu
0321             // desktopEntryName is lowercase so this is only a fallback
0322             m_desktopName = service->desktopEntryName();
0323         }
0324         m_desktopFilePath = QFileInfo(serviceEntryPath).absoluteFilePath();
0325         m_description = service->name();
0326         if (!service->genericName().isEmpty()) {
0327             m_description.append(QStringLiteral(" - %1").arg(service->genericName()));
0328         }
0329     } else {
0330         m_description = userVisibleName;
0331     }
0332 
0333     if (!m_waitingForXdgToken) {
0334         startProcess();
0335     }
0336 }
0337 
0338 void ForkingProcessRunner::startProcess()
0339 {
0340     connect(m_process.get(), qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, &ForkingProcessRunner::slotProcessExited);
0341     connect(m_process.get(), &QProcess::started, this, &ForkingProcessRunner::slotProcessStarted, Qt::QueuedConnection);
0342     connect(m_process.get(), &QProcess::errorOccurred, this, &ForkingProcessRunner::slotProcessError);
0343     m_process->start();
0344 }
0345 
0346 bool ForkingProcessRunner::waitForStarted(int timeout)
0347 {
0348     if (m_process->state() == QProcess::NotRunning && m_waitingForXdgToken) {
0349         QEventLoop loop;
0350         QObject::connect(m_process.get(), &QProcess::stateChanged, &loop, &QEventLoop::quit);
0351         QTimer::singleShot(timeout, &loop, &QEventLoop::quit);
0352         loop.exec();
0353     }
0354     return m_process->waitForStarted(timeout);
0355 }
0356 
0357 void ForkingProcessRunner::slotProcessError(QProcess::ProcessError errorCode)
0358 {
0359     // E.g. the process crashed.
0360     // This is unlikely to happen while the ApplicationLauncherJob is still connected to the KProcessRunner.
0361     // So the emit does nothing, this is really just for debugging.
0362     qCDebug(KIO_GUI) << name() << "error=" << errorCode << m_process->errorString();
0363     Q_EMIT error(m_process->errorString());
0364 }
0365 
0366 void ForkingProcessRunner::slotProcessStarted()
0367 {
0368     setPid(m_process->processId());
0369 }
0370 
0371 void KProcessRunner::setPid(qint64 pid)
0372 {
0373     if (!m_pid && pid) {
0374         qCDebug(KIO_GUI) << "Setting PID" << pid << "for:" << name();
0375         m_pid = pid;
0376 #if HAVE_X11
0377         if (!m_startupId.isNull()) {
0378             KStartupInfoData data;
0379             data.addPid(static_cast<int>(m_pid));
0380             KStartupInfo::sendChange(m_startupId, data);
0381             KStartupInfo::resetStartupEnv();
0382         }
0383 #endif
0384         Q_EMIT processStarted(pid);
0385     }
0386 }
0387 
0388 KProcessRunner::~KProcessRunner()
0389 {
0390     // This destructor deletes m_process, since it's a unique_ptr.
0391     --s_instanceCount;
0392 }
0393 
0394 int KProcessRunner::instanceCount()
0395 {
0396     return s_instanceCount;
0397 }
0398 
0399 void KProcessRunner::terminateStartupNotification()
0400 {
0401 #if HAVE_X11
0402     if (!m_startupId.isNull()) {
0403         KStartupInfoData data;
0404         data.addPid(static_cast<int>(m_pid)); // announce this pid for the startup notification has finished
0405         data.setHostname();
0406         KStartupInfo::sendFinish(m_startupId, data);
0407     }
0408 #endif
0409 }
0410 
0411 QString KProcessRunner::name() const
0412 {
0413     return !m_desktopName.isEmpty() ? m_desktopName : m_executable;
0414 }
0415 
0416 // Only alphanum, ':' and '_' allowed in systemd unit names
0417 QString KProcessRunner::escapeUnitName(const QString &input)
0418 {
0419     QString res;
0420     const QByteArray bytes = input.toUtf8();
0421     for (const auto &c : bytes) {
0422         if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == ':' || c == '_' || c == '.') {
0423             res += QLatin1Char(c);
0424         } else {
0425             res += QStringLiteral("\\x%1").arg(c, 2, 16, QLatin1Char('0'));
0426         }
0427     }
0428     return res;
0429 }
0430 
0431 QString KProcessRunner::maybeAliasedName(const QString &pattern) const
0432 {
0433     // Don't actually load aliased desktop file to avoid having to deal with recursion
0434     QString servName = m_service ? m_service->aliasFor() : QString{};
0435     if (servName.isEmpty()) {
0436         servName = name();
0437     }
0438 
0439     // As specified in "XDG standardization for applications" in https://systemd.io/DESKTOP_ENVIRONMENTS/
0440     return pattern.arg(escapeUnitName(servName), QUuid::createUuid().toString(QUuid::Id128));
0441 }
0442 
0443 void KProcessRunner::emitDelayedError(const QString &errorMsg)
0444 {
0445     qCWarning(KIO_GUI) << errorMsg;
0446     terminateStartupNotification();
0447     // Use delayed invocation so the caller has time to connect to the signal
0448     auto func = [this, errorMsg]() {
0449         Q_EMIT error(errorMsg);
0450         deleteLater();
0451     };
0452     QMetaObject::invokeMethod(this, func, Qt::QueuedConnection);
0453 }
0454 
0455 void ForkingProcessRunner::slotProcessExited(int exitCode, QProcess::ExitStatus exitStatus)
0456 {
0457     qCDebug(KIO_GUI) << name() << "exitCode=" << exitCode << "exitStatus=" << exitStatus;
0458     terminateStartupNotification();
0459     deleteLater();
0460 #ifdef Q_OS_UNIX
0461     if (exitCode == 127) {
0462 #else
0463     if (exitCode == 9009) {
0464 #endif
0465         const QStringList args = m_cmd.split(QLatin1Char(' '));
0466         emitDelayedError(xi18nc("@info", "The command <command>%1</command> could not be found.", args[0]));
0467     }
0468 }
0469 
0470 // This code is also used in klauncher (and KRun).
0471 bool KIOGuiPrivate::checkStartupNotify(const KService *service, bool *silent_arg, QByteArray *wmclass_arg)
0472 {
0473     bool silent = false;
0474     QByteArray wmclass;
0475     if (service && service->property(QStringLiteral("StartupNotify")).isValid()) {
0476         silent = !service->property(QStringLiteral("StartupNotify")).toBool();
0477         wmclass = service->property(QStringLiteral("StartupWMClass")).toString().toLatin1();
0478     } else if (service && service->property(QStringLiteral("X-KDE-StartupNotify")).isValid()) {
0479         silent = !service->property(QStringLiteral("X-KDE-StartupNotify")).toBool();
0480         wmclass = service->property(QStringLiteral("X-KDE-WMClass")).toString().toLatin1();
0481     } else { // non-compliant app
0482         if (service) {
0483             if (service->isApplication()) { // doesn't have .desktop entries needed, start as non-compliant
0484                 wmclass = "0"; // krazy:exclude=doublequote_chars
0485             } else {
0486                 return false; // no startup notification at all
0487             }
0488         } else {
0489 #if 0
0490             // Create startup notification even for apps for which there shouldn't be any,
0491             // just without any visual feedback. This will ensure they'll be positioned on the proper
0492             // virtual desktop, and will get user timestamp from the ASN ID.
0493             wmclass = '0';
0494             silent = true;
0495 #else // That unfortunately doesn't work, when the launched non-compliant application
0496       // launches another one that is compliant and there is any delay in between (bnc:#343359)
0497             return false;
0498 #endif
0499         }
0500     }
0501     if (silent_arg) {
0502         *silent_arg = silent;
0503     }
0504     if (wmclass_arg) {
0505         *wmclass_arg = wmclass;
0506     }
0507     return true;
0508 }
0509 
0510 ForkingProcessRunner::ForkingProcessRunner()
0511     : KProcessRunner()
0512 {
0513 }
0514 
0515 #include "moc_kprocessrunner_p.cpp"