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"