File indexing completed on 2024-05-12 11:54:13

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 
0022 bool SystemdProcessRunner::isAvailable()
0023 {
0024     static std::once_flag dbusRegistered;
0025     static bool runAsService = false;
0026     std::call_once(dbusRegistered, []() {
0027         if (QDBusConnection::sessionBus().interface()->isServiceRegistered(systemdService)) {
0028             runAsService = true;
0029             qDBusRegisterMetaType<QVariantMultiItem>();
0030             qDBusRegisterMetaType<QVariantMultiMap>();
0031             qDBusRegisterMetaType<TransientAux>();
0032             qDBusRegisterMetaType<TransientAuxList>();
0033             qDBusRegisterMetaType<ExecCommand>();
0034             qDBusRegisterMetaType<ExecCommandList>();
0035         }
0036     });
0037     return runAsService;
0038 }
0039 
0040 SystemdProcessRunner::SystemdProcessRunner()
0041     : KProcessRunner()
0042 {
0043 }
0044 
0045 bool SystemdProcessRunner::waitForStarted(int timeout)
0046 {
0047     if (m_pid || m_exited) {
0048         return true;
0049     }
0050     QEventLoop loop;
0051     bool success = false;
0052     loop.connect(this, &KProcessRunner::processStarted, [&loop, &success]() {
0053         loop.quit();
0054         success = true;
0055     });
0056     QTimer::singleShot(timeout, &loop, &QEventLoop::quit);
0057     QObject::connect(this, &KProcessRunner::error, &loop, &QEventLoop::quit);
0058     loop.exec();
0059     return success;
0060 }
0061 
0062 void SystemdProcessRunner::startProcess()
0063 {
0064     m_serviceName = SystemdProcessRunner::maybeAliasedName(QStringLiteral("app-%1@%2.service"));
0065 
0066     // Watch for new services
0067     m_manager = new systemd1::Manager(systemdService, systemdPath, QDBusConnection::sessionBus(), this);
0068     m_manager->Subscribe();
0069     connect(m_manager, &systemd1::Manager::UnitNew, this, &SystemdProcessRunner::handleUnitNew);
0070 
0071     // Watch for service creation job error
0072     connect(m_manager,
0073             &systemd1::Manager::JobRemoved,
0074             this,
0075             [this](uint jobId, const QDBusObjectPath &jobPath, const QString &unitName, const QString &result) {
0076                 Q_UNUSED(jobId)
0077                 if (jobPath.path() == m_jobPath && unitName == m_serviceName && result != QLatin1String("done")) {
0078                     qCWarning(KIO_GUI) << "Failed to launch process as service:" << m_serviceName << ", result " << result;
0079                     // result=failed is not a fatal error, service is actually created in this case
0080                     if (result != QLatin1String("failed")) {
0081                         systemdError(result);
0082                     }
0083                 }
0084             });
0085 
0086     // Ask systemd for a new transient service
0087     const auto startReply = m_manager->StartTransientUnit(
0088         m_serviceName,
0089         QStringLiteral("fail"), // mode defines what to do in the case of a name conflict, in this case, just do nothing
0090         {
0091             // Properties of the transient service unit
0092             {QStringLiteral("Type"), QStringLiteral("simple")},
0093             {QStringLiteral("ExitType"), QStringLiteral("cgroup")},
0094             {QStringLiteral("Slice"), QStringLiteral("app.slice")},
0095             {QStringLiteral("Description"), m_description},
0096             {QStringLiteral("SourcePath"), m_desktopFilePath},
0097             {QStringLiteral("AddRef"), true}, // Asks systemd to avoid garbage collecting the service if it immediately crashes,
0098                                               // so we can be notified (see https://github.com/systemd/systemd/pull/3984)
0099             {QStringLiteral("Environment"), m_process->environment()},
0100             {QStringLiteral("WorkingDirectory"), m_process->workingDirectory()},
0101             {QStringLiteral("ExecStart"), QVariant::fromValue(ExecCommandList{{m_process->program().first(), m_process->program(), false}})},
0102         },
0103         {} // aux is currently unused and should be passed as empty array.
0104     );
0105     connect(new QDBusPendingCallWatcher(startReply, this), &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
0106         QDBusPendingReply<QDBusObjectPath> reply = *watcher;
0107         watcher->deleteLater();
0108         if (reply.isError()) {
0109             qCWarning(KIO_GUI) << "Failed to launch process as service:" << m_serviceName << reply.error().name() << reply.error().message();
0110             return systemdError(reply.error().message());
0111         }
0112         qCDebug(KIO_GUI) << "Successfully asked systemd to launch process as service:" << m_serviceName;
0113         m_jobPath = reply.argumentAt<0>().path();
0114     });
0115 }
0116 
0117 void SystemdProcessRunner::handleProperties(QDBusPendingCallWatcher *watcher)
0118 {
0119     const QDBusPendingReply<QVariantMap> reply = *watcher;
0120     watcher->deleteLater();
0121     if (reply.isError()) {
0122         qCWarning(KIO_GUI) << "Failed to get properties for service:" << m_serviceName << reply.error().name() << reply.error().message();
0123         return systemdError(reply.error().message());
0124     }
0125     qCDebug(KIO_GUI) << "Successfully retrieved properties for service:" << m_serviceName;
0126     if (m_exited) {
0127         return;
0128     }
0129     const auto properties = reply.argumentAt<0>();
0130     if (!m_pid) {
0131         setPid(properties[QStringLiteral("ExecMainPID")].value<quint32>());
0132         return;
0133     }
0134     const auto activeState = properties[QStringLiteral("ActiveState")].toString();
0135     if (activeState != QLatin1String("inactive") && activeState != QLatin1String("failed")) {
0136         return;
0137     }
0138     m_exited = true;
0139 
0140     // ExecMainCode/Status correspond to si_code/si_status in the siginfo_t structure
0141     // ExecMainCode is the signal code: CLD_EXITED (1) means normal exit
0142     // ExecMainStatus is the process exit code in case of normal exit, otherwise it is the signal number
0143     const auto signalCode = properties[QStringLiteral("ExecMainCode")].value<qint32>();
0144     const auto exitCodeOrSignalNumber = properties[QStringLiteral("ExecMainStatus")].value<qint32>();
0145     const auto exitStatus = signalCode == CLD_EXITED ? QProcess::ExitStatus::NormalExit : QProcess::ExitStatus::CrashExit;
0146 
0147     qCDebug(KIO_GUI) << m_serviceName << "pid=" << m_pid << "exitCode=" << exitCodeOrSignalNumber << "exitStatus=" << exitStatus;
0148     terminateStartupNotification();
0149     deleteLater();
0150 
0151     systemd1::Unit unitInterface(systemdService, m_servicePath, QDBusConnection::sessionBus(), this);
0152     connect(new QDBusPendingCallWatcher(unitInterface.Unref(), this), &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
0153         QDBusPendingReply<> reply = *watcher;
0154         watcher->deleteLater();
0155         if (reply.isError()) {
0156             qCWarning(KIO_GUI) << "Failed to unref service:" << m_serviceName << reply.error().name() << reply.error().message();
0157             return systemdError(reply.error().message());
0158         }
0159         qCDebug(KIO_GUI) << "Successfully unref'd service:" << m_serviceName;
0160     });
0161 }
0162 
0163 void SystemdProcessRunner::handleUnitNew(const QString &newName, const QDBusObjectPath &newPath)
0164 {
0165     if (newName != m_serviceName) {
0166         return;
0167     }
0168     qCDebug(KIO_GUI) << "Successfully launched process as service:" << m_serviceName;
0169 
0170     // Get PID (and possibly exit code) from systemd service properties
0171     m_servicePath = newPath.path();
0172     m_serviceProperties = new DBus::Properties(systemdService, m_servicePath, QDBusConnection::sessionBus(), this);
0173     auto propReply = m_serviceProperties->GetAll(QString());
0174     connect(new QDBusPendingCallWatcher(propReply, this), &QDBusPendingCallWatcher::finished, this, &SystemdProcessRunner::handleProperties);
0175 
0176     // Watch for status change
0177     connect(m_serviceProperties, &DBus::Properties::PropertiesChanged, this, [this]() {
0178         if (m_exited) {
0179             return;
0180         }
0181         qCDebug(KIO_GUI) << "Got PropertiesChanged signal:" << m_serviceName;
0182         // We need to look at the full list of properties rather than only those which changed
0183         auto reply = m_serviceProperties->GetAll(QString());
0184         connect(new QDBusPendingCallWatcher(reply, this), &QDBusPendingCallWatcher::finished, this, &SystemdProcessRunner::handleProperties);
0185     });
0186 }
0187 
0188 void SystemdProcessRunner::systemdError(const QString &message)
0189 {
0190     Q_EMIT error(message);
0191     deleteLater();
0192 }
0193 
0194 #include "moc_systemdprocessrunner_p.cpp"