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

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2020 Henri Chain <henri.chain@enioka.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include "kiogui_debug.h"
0009 #include "systemdprocessrunner_p.h"
0010 
0011 #include "managerinterface.h"
0012 #include "propertiesinterface.h"
0013 #include "unitinterface.h"
0014 
0015 #include <QTimer>
0016 
0017 #include <mutex>
0018 #include <signal.h>
0019 
0020 using namespace org::freedesktop;
0021 using namespace Qt::Literals::StringLiterals;
0022 
0023 KProcessRunner::LaunchMode calculateLaunchMode()
0024 {
0025     // overrides for unit test purposes
0026     if (Q_UNLIKELY(qEnvironmentVariableIntValue("KDE_APPLICATIONS_AS_SERVICE"))) {
0027         return KProcessRunner::SystemdAsService;
0028     }
0029     if (Q_UNLIKELY(qEnvironmentVariableIntValue("KDE_APPLICATIONS_AS_SCOPE"))) {
0030         return KProcessRunner::SystemdAsScope;
0031     }
0032     if (Q_UNLIKELY(qEnvironmentVariableIntValue("KDE_APPLICATIONS_AS_FORKING"))) {
0033         return KProcessRunner::Forking;
0034     }
0035 
0036     QDBusConnection bus = QDBusConnection::sessionBus();
0037     auto queryVersionMessage =
0038         QDBusMessage::createMethodCall(u"org.freedesktop.systemd1"_s, u"/org/freedesktop/systemd1"_s, u"org.freedesktop.DBus.Properties"_s, u"Get"_s);
0039     queryVersionMessage << u"org.freedesktop.systemd1.Manager"_s << u"Version"_s;
0040     QDBusReply<QDBusVariant> reply = bus.call(queryVersionMessage);
0041     QVersionNumber systemdVersion = QVersionNumber::fromString(reply.value().variant().toString());
0042     if (systemdVersion.isNull()) {
0043         return KProcessRunner::Forking;
0044     }
0045     if (systemdVersion.majorVersion() >= 250) { // first version with ExitType=cgroup, which won't cleanup when the first process exits
0046         return KProcessRunner::SystemdAsService;
0047     } else {
0048         return KProcessRunner::SystemdAsScope;
0049     }
0050 }
0051 
0052 KProcessRunner::LaunchMode SystemdProcessRunner::modeAvailable()
0053 {
0054     static std::once_flag launchModeCalculated;
0055     static KProcessRunner::LaunchMode launchMode = Forking;
0056     std::call_once(launchModeCalculated, [] {
0057         launchMode = calculateLaunchMode();
0058         qCDebug(KIO_GUI) << "Launching processes via" << launchMode;
0059         qDBusRegisterMetaType<QVariantMultiItem>();
0060         qDBusRegisterMetaType<QVariantMultiMap>();
0061         qDBusRegisterMetaType<TransientAux>();
0062         qDBusRegisterMetaType<TransientAuxList>();
0063         qDBusRegisterMetaType<ExecCommand>();
0064         qDBusRegisterMetaType<ExecCommandList>();
0065     });
0066     return launchMode;
0067 }
0068 
0069 SystemdProcessRunner::SystemdProcessRunner()
0070     : KProcessRunner()
0071 {
0072 }
0073 
0074 bool SystemdProcessRunner::waitForStarted(int timeout)
0075 {
0076     if (m_pid || m_exited) {
0077         return true;
0078     }
0079     QEventLoop loop;
0080     bool success = false;
0081     loop.connect(this, &KProcessRunner::processStarted, [&loop, &success]() {
0082         loop.quit();
0083         success = true;
0084     });
0085     QTimer::singleShot(timeout, &loop, &QEventLoop::quit);
0086     QObject::connect(this, &KProcessRunner::error, &loop, &QEventLoop::quit);
0087     loop.exec();
0088     return success;
0089 }
0090 
0091 void SystemdProcessRunner::startProcess()
0092 {
0093     // As specified in "XDG standardization for applications" in https://systemd.io/DESKTOP_ENVIRONMENTS/
0094     m_serviceName = QStringLiteral("app-%1@%2.service").arg(escapeUnitName(resolveServiceAlias()), QUuid::createUuid().toString(QUuid::Id128));
0095 
0096     // Watch for new services
0097     m_manager = new systemd1::Manager(systemdService, systemdPath, QDBusConnection::sessionBus(), this);
0098     m_manager->Subscribe();
0099     connect(m_manager, &systemd1::Manager::UnitNew, this, &SystemdProcessRunner::handleUnitNew);
0100 
0101     // Watch for service creation job error
0102     connect(m_manager,
0103             &systemd1::Manager::JobRemoved,
0104             this,
0105             [this](uint jobId, const QDBusObjectPath &jobPath, const QString &unitName, const QString &result) {
0106                 Q_UNUSED(jobId)
0107                 if (jobPath.path() == m_jobPath && unitName == m_serviceName && result != QLatin1String("done")) {
0108                     qCWarning(KIO_GUI) << "Failed to launch process as service:" << m_serviceName << ", result " << result;
0109                     // result=failed is not a fatal error, service is actually created in this case
0110                     if (result != QLatin1String("failed")) {
0111                         systemdError(result);
0112                     }
0113                 }
0114             });
0115 
0116     // Ask systemd for a new transient service
0117     const auto startReply = m_manager->StartTransientUnit(
0118         m_serviceName,
0119         QStringLiteral("fail"), // mode defines what to do in the case of a name conflict, in this case, just do nothing
0120         {
0121             // Properties of the transient service unit
0122             {QStringLiteral("Type"), QStringLiteral("simple")},
0123             {QStringLiteral("ExitType"), QStringLiteral("cgroup")},
0124             {QStringLiteral("Slice"), QStringLiteral("app.slice")},
0125             {QStringLiteral("Description"), m_description},
0126             {QStringLiteral("SourcePath"), m_desktopFilePath},
0127             {QStringLiteral("AddRef"), true}, // Asks systemd to avoid garbage collecting the service if it immediately crashes,
0128                                               // so we can be notified (see https://github.com/systemd/systemd/pull/3984)
0129             {QStringLiteral("Environment"), m_process->environment()},
0130             {QStringLiteral("WorkingDirectory"), m_process->workingDirectory()},
0131             {QStringLiteral("ExecStart"), QVariant::fromValue(ExecCommandList{{m_process->program().first(), m_process->program(), false}})},
0132         },
0133         {} // aux is currently unused and should be passed as empty array.
0134     );
0135     connect(new QDBusPendingCallWatcher(startReply, this), &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
0136         QDBusPendingReply<QDBusObjectPath> reply = *watcher;
0137         watcher->deleteLater();
0138         if (reply.isError()) {
0139             qCWarning(KIO_GUI) << "Failed to launch process as service:" << m_serviceName << reply.error().name() << reply.error().message();
0140             return systemdError(reply.error().message());
0141         }
0142         qCDebug(KIO_GUI) << "Successfully asked systemd to launch process as service:" << m_serviceName;
0143         m_jobPath = reply.argumentAt<0>().path();
0144     });
0145 }
0146 
0147 void SystemdProcessRunner::handleProperties(QDBusPendingCallWatcher *watcher)
0148 {
0149     const QDBusPendingReply<QVariantMap> reply = *watcher;
0150     watcher->deleteLater();
0151     if (reply.isError()) {
0152         qCWarning(KIO_GUI) << "Failed to get properties for service:" << m_serviceName << reply.error().name() << reply.error().message();
0153         return systemdError(reply.error().message());
0154     }
0155     qCDebug(KIO_GUI) << "Successfully retrieved properties for service:" << m_serviceName;
0156     if (m_exited) {
0157         return;
0158     }
0159     const auto properties = reply.argumentAt<0>();
0160     if (!m_pid) {
0161         setPid(properties[QStringLiteral("ExecMainPID")].value<quint32>());
0162         return;
0163     }
0164     const auto activeState = properties[QStringLiteral("ActiveState")].toString();
0165     if (activeState != QLatin1String("inactive") && activeState != QLatin1String("failed")) {
0166         return;
0167     }
0168     m_exited = true;
0169 
0170     // ExecMainCode/Status correspond to si_code/si_status in the siginfo_t structure
0171     // ExecMainCode is the signal code: CLD_EXITED (1) means normal exit
0172     // ExecMainStatus is the process exit code in case of normal exit, otherwise it is the signal number
0173     const auto signalCode = properties[QStringLiteral("ExecMainCode")].value<qint32>();
0174     const auto exitCodeOrSignalNumber = properties[QStringLiteral("ExecMainStatus")].value<qint32>();
0175     const auto exitStatus = signalCode == CLD_EXITED ? QProcess::ExitStatus::NormalExit : QProcess::ExitStatus::CrashExit;
0176 
0177     qCDebug(KIO_GUI) << m_serviceName << "pid=" << m_pid << "exitCode=" << exitCodeOrSignalNumber << "exitStatus=" << exitStatus;
0178     terminateStartupNotification();
0179     deleteLater();
0180 
0181     systemd1::Unit unitInterface(systemdService, m_servicePath, QDBusConnection::sessionBus(), this);
0182     connect(new QDBusPendingCallWatcher(unitInterface.Unref(), this), &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
0183         QDBusPendingReply<> reply = *watcher;
0184         watcher->deleteLater();
0185         if (reply.isError()) {
0186             qCWarning(KIO_GUI) << "Failed to unref service:" << m_serviceName << reply.error().name() << reply.error().message();
0187             return systemdError(reply.error().message());
0188         }
0189         qCDebug(KIO_GUI) << "Successfully unref'd service:" << m_serviceName;
0190     });
0191 }
0192 
0193 void SystemdProcessRunner::handleUnitNew(const QString &newName, const QDBusObjectPath &newPath)
0194 {
0195     if (newName != m_serviceName) {
0196         return;
0197     }
0198     qCDebug(KIO_GUI) << "Successfully launched process as service:" << m_serviceName;
0199 
0200     // Get PID (and possibly exit code) from systemd service properties
0201     m_servicePath = newPath.path();
0202     m_serviceProperties = new DBus::Properties(systemdService, m_servicePath, QDBusConnection::sessionBus(), this);
0203     auto propReply = m_serviceProperties->GetAll(QString());
0204     connect(new QDBusPendingCallWatcher(propReply, this), &QDBusPendingCallWatcher::finished, this, &SystemdProcessRunner::handleProperties);
0205 
0206     // Watch for status change
0207     connect(m_serviceProperties, &DBus::Properties::PropertiesChanged, this, [this]() {
0208         if (m_exited) {
0209             return;
0210         }
0211         qCDebug(KIO_GUI) << "Got PropertiesChanged signal:" << m_serviceName;
0212         // We need to look at the full list of properties rather than only those which changed
0213         auto reply = m_serviceProperties->GetAll(QString());
0214         connect(new QDBusPendingCallWatcher(reply, this), &QDBusPendingCallWatcher::finished, this, &SystemdProcessRunner::handleProperties);
0215     });
0216 }
0217 
0218 void SystemdProcessRunner::systemdError(const QString &message)
0219 {
0220     Q_EMIT error(message);
0221     deleteLater();
0222 }
0223 
0224 #include "moc_systemdprocessrunner_p.cpp"