1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2020 Henri Chain <henri.chain@enioka.com>
4
5 SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6*/
7
8#include "kiogui_debug.h"
9#include "systemdprocessrunner_p.h"
10
11#include "managerinterface.h"
12#include "propertiesinterface.h"
13#include "unitinterface.h"
14
15#include <QTimer>
16
17#include <mutex>
18#include <signal.h>
19
20using namespace org::freedesktop;
21using namespace Qt::Literals::StringLiterals;
22
23KProcessRunner::LaunchMode calculateLaunchMode()
24{
25 // overrides for unit test purposes
26 if (Q_UNLIKELY(qEnvironmentVariableIntValue("KDE_APPLICATIONS_AS_SERVICE"))) {
27 return KProcessRunner::SystemdAsService;
28 }
29 if (Q_UNLIKELY(qEnvironmentVariableIntValue("KDE_APPLICATIONS_AS_SCOPE"))) {
30 return KProcessRunner::SystemdAsScope;
31 }
32 if (Q_UNLIKELY(qEnvironmentVariableIntValue("KDE_APPLICATIONS_AS_FORKING"))) {
33 return KProcessRunner::Forking;
34 }
35
36 QDBusConnection bus = QDBusConnection::sessionBus();
37 auto queryVersionMessage =
38 QDBusMessage::createMethodCall(destination: u"org.freedesktop.systemd1"_s, path: u"/org/freedesktop/systemd1"_s, interface: u"org.freedesktop.DBus.Properties"_s, method: u"Get"_s);
39 queryVersionMessage << u"org.freedesktop.systemd1.Manager"_s << u"Version"_s;
40 QDBusReply<QDBusVariant> reply = bus.call(message: queryVersionMessage);
41 QVersionNumber systemdVersion = QVersionNumber::fromString(string: reply.value().variant().toString());
42 if (systemdVersion.isNull()) {
43 return KProcessRunner::Forking;
44 }
45 if (systemdVersion.majorVersion() >= 250) { // first version with ExitType=cgroup, which won't cleanup when the first process exits
46 return KProcessRunner::SystemdAsService;
47 } else {
48 return KProcessRunner::SystemdAsScope;
49 }
50}
51
52KProcessRunner::LaunchMode SystemdProcessRunner::modeAvailable()
53{
54 static std::once_flag launchModeCalculated;
55 static KProcessRunner::LaunchMode launchMode = Forking;
56 std::call_once(once&: launchModeCalculated, f: [] {
57 launchMode = calculateLaunchMode();
58 qCDebug(KIO_GUI) << "Launching processes via" << launchMode;
59 qDBusRegisterMetaType<QVariantMultiItem>();
60 qDBusRegisterMetaType<QVariantMultiMap>();
61 qDBusRegisterMetaType<TransientAux>();
62 qDBusRegisterMetaType<TransientAuxList>();
63 qDBusRegisterMetaType<ExecCommand>();
64 qDBusRegisterMetaType<ExecCommandList>();
65 });
66 return launchMode;
67}
68
69SystemdProcessRunner::SystemdProcessRunner()
70 : KProcessRunner()
71{
72}
73
74bool SystemdProcessRunner::waitForStarted(int timeout)
75{
76 if (m_pid || m_exited) {
77 return true;
78 }
79 QEventLoop loop;
80 bool success = false;
81 loop.connect(sender: this, signal: &KProcessRunner::processStarted, slot: [&loop, &success]() {
82 loop.quit();
83 success = true;
84 });
85 QTimer::singleShot(interval: timeout, receiver: &loop, slot: &QEventLoop::quit);
86 QObject::connect(sender: this, signal: &KProcessRunner::error, context: &loop, slot: &QEventLoop::quit);
87 loop.exec();
88 return success;
89}
90
91void SystemdProcessRunner::startProcess()
92{
93 // As specified in "XDG standardization for applications" in https://systemd.io/DESKTOP_ENVIRONMENTS/
94 m_serviceName = QStringLiteral("app-%1@%2.service").arg(args: escapeUnitName(input: resolveServiceAlias()), args: QUuid::createUuid().toString(mode: QUuid::Id128));
95
96 // Watch for new services
97 m_manager = new systemd1::Manager(systemdService, systemdPath, QDBusConnection::sessionBus(), this);
98 m_manager->Subscribe();
99 connect(sender: m_manager, signal: &systemd1::Manager::UnitNew, context: this, slot: &SystemdProcessRunner::handleUnitNew);
100
101 // Watch for service creation job error
102 connect(sender: m_manager,
103 signal: &systemd1::Manager::JobRemoved,
104 context: this,
105 slot: [this](uint jobId, const QDBusObjectPath &jobPath, const QString &unitName, const QString &result) {
106 Q_UNUSED(jobId)
107 if (jobPath.path() == m_jobPath && unitName == m_serviceName && result != QLatin1String("done")) {
108 qCWarning(KIO_GUI) << "Failed to launch process as service:" << m_serviceName << ", result " << result;
109 // result=failed is not a fatal error, service is actually created in this case
110 if (result != QLatin1String("failed")) {
111 systemdError(error: result);
112 }
113 }
114 });
115
116 // Ask systemd for a new transient service
117 const auto startReply = m_manager->StartTransientUnit(
118 name: m_serviceName,
119 QStringLiteral("fail"), // mode defines what to do in the case of a name conflict, in this case, just do nothing
120 properties: {
121 // Properties of the transient service unit
122 {QStringLiteral("Type"), QStringLiteral("simple")},
123 {QStringLiteral("ExitType"), QStringLiteral("cgroup")},
124 {QStringLiteral("Slice"), QStringLiteral("app.slice")},
125 {QStringLiteral("Description"), .value: m_description},
126 {QStringLiteral("SourcePath"), .value: m_desktopFilePath},
127 {QStringLiteral("AddRef"), .value: true}, // Asks systemd to avoid garbage collecting the service if it immediately crashes,
128 // so we can be notified (see https://github.com/systemd/systemd/pull/3984)
129 {QStringLiteral("Environment"), .value: m_process->environment()},
130 {QStringLiteral("WorkingDirectory"), .value: m_process->workingDirectory()},
131 {QStringLiteral("ExecStart"), .value: QVariant::fromValue(value: ExecCommandList{{.path: m_process->program().first(), .argv: m_process->program(), .ignoreFailure: false}})},
132 },
133 aux: {} // aux is currently unused and should be passed as empty array.
134 );
135 connect(sender: new QDBusPendingCallWatcher(startReply, this), signal: &QDBusPendingCallWatcher::finished, context: this, slot: [this](QDBusPendingCallWatcher *watcher) {
136 QDBusPendingReply<QDBusObjectPath> reply = *watcher;
137 watcher->deleteLater();
138 if (reply.isError()) {
139 qCWarning(KIO_GUI) << "Failed to launch process as service:" << m_serviceName << reply.error().name() << reply.error().message();
140 return systemdError(error: reply.error().message());
141 }
142 qCDebug(KIO_GUI) << "Successfully asked systemd to launch process as service:" << m_serviceName;
143 m_jobPath = reply.argumentAt<0>().path();
144 });
145}
146
147void SystemdProcessRunner::handleProperties(QDBusPendingCallWatcher *watcher)
148{
149 const QDBusPendingReply<QVariantMap> reply = *watcher;
150 watcher->deleteLater();
151 if (reply.isError()) {
152 qCWarning(KIO_GUI) << "Failed to get properties for service:" << m_serviceName << reply.error().name() << reply.error().message();
153 return systemdError(error: reply.error().message());
154 }
155 qCDebug(KIO_GUI) << "Successfully retrieved properties for service:" << m_serviceName;
156 if (m_exited) {
157 return;
158 }
159 const auto properties = reply.argumentAt<0>();
160 if (!m_pid) {
161 setPid(properties[QStringLiteral("ExecMainPID")].value<quint32>());
162 return;
163 }
164 const auto activeState = properties[QStringLiteral("ActiveState")].toString();
165 if (activeState != QLatin1String("inactive") && activeState != QLatin1String("failed")) {
166 return;
167 }
168 m_exited = true;
169
170 // ExecMainCode/Status correspond to si_code/si_status in the siginfo_t structure
171 // ExecMainCode is the signal code: CLD_EXITED (1) means normal exit
172 // ExecMainStatus is the process exit code in case of normal exit, otherwise it is the signal number
173 const auto signalCode = properties[QStringLiteral("ExecMainCode")].value<qint32>();
174 const auto exitCodeOrSignalNumber = properties[QStringLiteral("ExecMainStatus")].value<qint32>();
175 const auto exitStatus = signalCode == CLD_EXITED ? QProcess::ExitStatus::NormalExit : QProcess::ExitStatus::CrashExit;
176
177 qCDebug(KIO_GUI) << m_serviceName << "pid=" << m_pid << "exitCode=" << exitCodeOrSignalNumber << "exitStatus=" << exitStatus;
178 terminateStartupNotification();
179 deleteLater();
180
181 systemd1::Unit unitInterface(systemdService, m_servicePath, QDBusConnection::sessionBus(), this);
182 connect(sender: new QDBusPendingCallWatcher(unitInterface.Unref(), this), signal: &QDBusPendingCallWatcher::finished, context: this, slot: [this](QDBusPendingCallWatcher *watcher) {
183 QDBusPendingReply<> reply = *watcher;
184 watcher->deleteLater();
185 if (reply.isError()) {
186 qCWarning(KIO_GUI) << "Failed to unref service:" << m_serviceName << reply.error().name() << reply.error().message();
187 return systemdError(error: reply.error().message());
188 }
189 qCDebug(KIO_GUI) << "Successfully unref'd service:" << m_serviceName;
190 });
191}
192
193void SystemdProcessRunner::handleUnitNew(const QString &newName, const QDBusObjectPath &newPath)
194{
195 if (newName != m_serviceName) {
196 return;
197 }
198 qCDebug(KIO_GUI) << "Successfully launched process as service:" << m_serviceName;
199
200 // Get PID (and possibly exit code) from systemd service properties
201 m_servicePath = newPath.path();
202 m_serviceProperties = new DBus::Properties(systemdService, m_servicePath, QDBusConnection::sessionBus(), this);
203 auto propReply = m_serviceProperties->GetAll(interface: QString());
204 connect(sender: new QDBusPendingCallWatcher(propReply, this), signal: &QDBusPendingCallWatcher::finished, context: this, slot: &SystemdProcessRunner::handleProperties);
205
206 // Watch for status change
207 connect(sender: m_serviceProperties, signal: &DBus::Properties::PropertiesChanged, context: this, slot: [this]() {
208 if (m_exited) {
209 return;
210 }
211 qCDebug(KIO_GUI) << "Got PropertiesChanged signal:" << m_serviceName;
212 // We need to look at the full list of properties rather than only those which changed
213 auto reply = m_serviceProperties->GetAll(interface: QString());
214 connect(sender: new QDBusPendingCallWatcher(reply, this), signal: &QDBusPendingCallWatcher::finished, context: this, slot: &SystemdProcessRunner::handleProperties);
215 });
216}
217
218void SystemdProcessRunner::systemdError(const QString &message)
219{
220 Q_EMIT error(errorString: message);
221 deleteLater();
222}
223
224#include "moc_systemdprocessrunner_p.cpp"
225

source code of kio/src/gui/systemd/systemdprocessrunner.cpp