File indexing completed on 2024-04-21 03:55:28

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