KIO

systemdprocessrunner.cpp
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 <algorithm>
18#include <mutex>
19#include <signal.h>
20
21using namespace org::freedesktop;
22using namespace Qt::Literals::StringLiterals;
23
24KProcessRunner::LaunchMode calculateLaunchMode()
25{
26 // overrides for unit test purposes. These are considered internal, private and may change in the future.
27 if (Q_UNLIKELY(qEnvironmentVariableIntValue("_KDE_APPLICATIONS_AS_SERVICE"))) {
28 return KProcessRunner::SystemdAsService;
29 }
30 if (Q_UNLIKELY(qEnvironmentVariableIntValue("_KDE_APPLICATIONS_AS_SCOPE"))) {
31 return KProcessRunner::SystemdAsScope;
32 }
33 if (Q_UNLIKELY(qEnvironmentVariableIntValue("_KDE_APPLICATIONS_AS_FORKING"))) {
34 return KProcessRunner::Forking;
35 }
36
38 auto queryVersionMessage =
39 QDBusMessage::createMethodCall(u"org.freedesktop.systemd1"_s, u"/org/freedesktop/systemd1"_s, u"org.freedesktop.DBus.Properties"_s, u"Get"_s);
40 queryVersionMessage << u"org.freedesktop.systemd1.Manager"_s << u"Version"_s;
41 QDBusReply<QDBusVariant> reply = bus.call(queryVersionMessage);
42 QVersionNumber systemdVersion = QVersionNumber::fromString(reply.value().variant().toString());
43 if (systemdVersion.isNull()) {
44 qCWarning(KIO_GUI) << "Failed to determine systemd version, falling back to extremely legacy forking mode.";
45 return KProcessRunner::Forking;
46 }
47 if (systemdVersion.majorVersion() < 250) { // first version with ExitType=cgroup, which won't cleanup when the first process exits
48 return KProcessRunner::SystemdAsScope;
49 }
50 return KProcessRunner::SystemdAsService;
51}
52
53KProcessRunner::LaunchMode SystemdProcessRunner::modeAvailable()
54{
55 static std::once_flag launchModeCalculated;
56 static KProcessRunner::LaunchMode launchMode = Forking;
57 std::call_once(launchModeCalculated, [] {
58 launchMode = calculateLaunchMode();
59 qCDebug(KIO_GUI) << "Launching processes via" << launchMode;
60 qDBusRegisterMetaType<QVariantMultiItem>();
61 qDBusRegisterMetaType<QVariantMultiMap>();
62 qDBusRegisterMetaType<TransientAux>();
63 qDBusRegisterMetaType<TransientAuxList>();
64 qDBusRegisterMetaType<ExecCommand>();
65 qDBusRegisterMetaType<ExecCommandList>();
66 });
67 return launchMode;
68}
69
70SystemdProcessRunner::SystemdProcessRunner()
71 : KProcessRunner()
72{
73}
74
75bool SystemdProcessRunner::waitForStarted(int timeout)
76{
77 if (m_pid || m_exited) {
78 return true;
79 }
80 QEventLoop loop;
81 bool success = false;
82 loop.connect(this, &KProcessRunner::processStarted, this, [&loop, &success]() {
83 loop.quit();
84 success = true;
85 });
86 QTimer::singleShot(timeout, &loop, &QEventLoop::quit);
87 QObject::connect(this, &KProcessRunner::error, &loop, &QEventLoop::quit);
88 loop.exec();
89 return success;
90}
91
92static QStringList prepareEnvironment(const QProcessEnvironment &environment)
93{
94 QProcessEnvironment allowedEnvironment = environment.inheritsFromParent() ? QProcessEnvironment::systemEnvironment() : environment;
95 auto allowedBySystemd = [](const QChar c) {
96 return c.isDigit() || c.isLetter() || c == u'_';
97 };
98 for (const auto variables = allowedEnvironment.keys(); const auto &variable : variables) {
99 if (!std::ranges::all_of(variable, allowedBySystemd)) {
100 qCWarning(KIO_GUI) << "Not passing environment variable" << variable << "to systemd because its name contains illegal characters";
101 allowedEnvironment.remove(variable);
102 }
103 }
104 return allowedEnvironment.toStringList();
105}
106
107// systemd performs substitution of $ variables, we don't want this
108// $ should be replaced with $$
109static QStringList escapeArguments(const QStringList &in)
110{
111 QStringList escaped = in;
112 std::transform(escaped.begin(), escaped.end(), escaped.begin(), [](QString &item) {
113 return item.replace(QLatin1Char('$'), QLatin1String("$$"));
114 });
115 return escaped;
116}
117
118void SystemdProcessRunner::startProcess()
119{
120 // As specified in "XDG standardization for applications" in https://systemd.io/DESKTOP_ENVIRONMENTS/
121 m_serviceName = QStringLiteral("app-%1@%2.service").arg(escapeUnitName(resolveServiceAlias()), QUuid::createUuid().toString(QUuid::Id128));
122
123 // Watch for new services
124 m_manager = new systemd1::Manager(systemdService, systemdPath, QDBusConnection::sessionBus(), this);
125 m_manager->Subscribe();
126 connect(m_manager, &systemd1::Manager::UnitNew, this, &SystemdProcessRunner::handleUnitNew);
127
128 // Watch for service creation job error
129 connect(m_manager,
130 &systemd1::Manager::JobRemoved,
131 this,
132 [this](uint jobId, const QDBusObjectPath &jobPath, const QString &unitName, const QString &result) {
133 Q_UNUSED(jobId)
134 if (jobPath.path() == m_jobPath && unitName == m_serviceName && result != QLatin1String("done")) {
135 qCWarning(KIO_GUI) << "Failed to launch process as service:" << m_serviceName << ", result " << result;
136 // result=failed is not a fatal error, service is actually created in this case
137 if (result != QLatin1String("failed")) {
138 systemdError(result);
139 }
140 }
141 });
142
143 const QStringList argv = escapeArguments(m_process->program());
144
145 // Ask systemd for a new transient service
146 const auto startReply =
147 m_manager->StartTransientUnit(m_serviceName,
148 QStringLiteral("fail"), // mode defines what to do in the case of a name conflict, in this case, just do nothing
149 {
150 // Properties of the transient service unit
151 {QStringLiteral("Type"), QStringLiteral("simple")},
152 {QStringLiteral("ExitType"), QStringLiteral("cgroup")},
153 {QStringLiteral("Slice"), QStringLiteral("app.slice")},
154 {QStringLiteral("Description"), m_description},
155 {QStringLiteral("SourcePath"), m_desktopFilePath},
156 {QStringLiteral("AddRef"), true}, // Asks systemd to avoid garbage collecting the service if it immediately crashes,
157 // so we can be notified (see https://github.com/systemd/systemd/pull/3984)
158 {QStringLiteral("Environment"), prepareEnvironment(m_process->processEnvironment())},
159 {QStringLiteral("WorkingDirectory"), m_process->workingDirectory()},
160 {QStringLiteral("ExecStart"), QVariant::fromValue(ExecCommandList{{m_process->program().first(), argv, false}})},
161 },
162 {} // aux is currently unused and should be passed as empty array.
163 );
164 connect(new QDBusPendingCallWatcher(startReply, this), &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
165 QDBusPendingReply<QDBusObjectPath> reply = *watcher;
166 watcher->deleteLater();
167 if (reply.isError()) {
168 qCWarning(KIO_GUI) << "Failed to launch process as service:" << m_serviceName << reply.error().name() << reply.error().message();
169 return systemdError(reply.error().message());
170 }
171 qCDebug(KIO_GUI) << "Successfully asked systemd to launch process as service:" << m_serviceName;
172 m_jobPath = reply.argumentAt<0>().path();
173 });
174}
175
176void SystemdProcessRunner::handleProperties(QDBusPendingCallWatcher *watcher)
177{
178 const QDBusPendingReply<QVariantMap> reply = *watcher;
179 watcher->deleteLater();
180 if (reply.isError()) {
181 qCWarning(KIO_GUI) << "Failed to get properties for service:" << m_serviceName << reply.error().name() << reply.error().message();
182 return systemdError(reply.error().message());
183 }
184 qCDebug(KIO_GUI) << "Successfully retrieved properties for service:" << m_serviceName;
185 if (m_exited) {
186 return;
187 }
188 const auto properties = reply.argumentAt<0>();
189 if (!m_pid) {
190 setPid(properties[QStringLiteral("ExecMainPID")].value<quint32>());
191 return;
192 }
193 const auto activeState = properties[QStringLiteral("ActiveState")].toString();
194 if (activeState != QLatin1String("inactive") && activeState != QLatin1String("failed")) {
195 return;
196 }
197 m_exited = true;
198
199 // ExecMainCode/Status correspond to si_code/si_status in the siginfo_t structure
200 // ExecMainCode is the signal code: CLD_EXITED (1) means normal exit
201 // ExecMainStatus is the process exit code in case of normal exit, otherwise it is the signal number
202 const auto signalCode = properties[QStringLiteral("ExecMainCode")].value<qint32>();
203 const auto exitCodeOrSignalNumber = properties[QStringLiteral("ExecMainStatus")].value<qint32>();
204 const auto exitStatus = signalCode == CLD_EXITED ? QProcess::ExitStatus::NormalExit : QProcess::ExitStatus::CrashExit;
205
206 qCDebug(KIO_GUI) << m_serviceName << "pid=" << m_pid << "exitCode=" << exitCodeOrSignalNumber << "exitStatus=" << exitStatus;
207 terminateStartupNotification();
208 deleteLater();
209
210 systemd1::Unit unitInterface(systemdService, m_servicePath, QDBusConnection::sessionBus(), this);
211 connect(new QDBusPendingCallWatcher(unitInterface.Unref(), this), &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
212 QDBusPendingReply<> reply = *watcher;
213 watcher->deleteLater();
214 if (reply.isError()) {
215 qCWarning(KIO_GUI) << "Failed to unref service:" << m_serviceName << reply.error().name() << reply.error().message();
216 return systemdError(reply.error().message());
217 }
218 qCDebug(KIO_GUI) << "Successfully unref'd service:" << m_serviceName;
219 });
220}
221
222void SystemdProcessRunner::handleUnitNew(const QString &newName, const QDBusObjectPath &newPath)
223{
224 if (newName != m_serviceName) {
225 return;
226 }
227 qCDebug(KIO_GUI) << "Successfully launched process as service:" << m_serviceName;
228
229 // Get PID (and possibly exit code) from systemd service properties
230 m_servicePath = newPath.path();
231 m_serviceProperties = new DBus::Properties(systemdService, m_servicePath, QDBusConnection::sessionBus(), this);
232 auto propReply = m_serviceProperties->GetAll(QString());
233 connect(new QDBusPendingCallWatcher(propReply, this), &QDBusPendingCallWatcher::finished, this, &SystemdProcessRunner::handleProperties);
234
235 // Watch for status change
236 connect(m_serviceProperties, &DBus::Properties::PropertiesChanged, this, [this]() {
237 if (m_exited) {
238 return;
239 }
240 qCDebug(KIO_GUI) << "Got PropertiesChanged signal:" << m_serviceName;
241 // We need to look at the full list of properties rather than only those which changed
242 auto reply = m_serviceProperties->GetAll(QString());
243 connect(new QDBusPendingCallWatcher(reply, this), &QDBusPendingCallWatcher::finished, this, &SystemdProcessRunner::handleProperties);
244 });
245}
246
247void SystemdProcessRunner::systemdError(const QString &message)
248{
249 Q_EMIT error(message);
250 deleteLater();
251}
252
253#include "moc_systemdprocessrunner_p.cpp"
char * toString(const EngineQuery &query)
QString path(const QString &relativePath)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
KGuiItem properties()
QDBusMessage call(const QDBusMessage &message, QDBus::CallMode mode, int timeout) const const
QDBusConnection sessionBus()
QString message() const const
QString name() const const
QDBusMessage createMethodCall(const QString &service, const QString &path, const QString &interface, const QString &method)
QString path() const const
void finished(QDBusPendingCallWatcher *self)
QVariant argumentAt(int index) const const
QDBusError error() const const
bool isError() const const
int exec(ProcessEventsFlags flags)
void quit()
iterator begin()
iterator end()
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
bool inheritsFromParent() const const
QStringList keys() const const
void remove(const QString &name)
QProcessEnvironment systemEnvironment()
QStringList toStringList() const const
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QUuid createUuid()
QVariant fromValue(T &&value)
QVersionNumber fromString(QAnyStringView string, qsizetype *suffixIndex)
bool isNull() const const
int majorVersion() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:54:08 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.