1/*
2 This file is part of libkdbusaddons
3
4 SPDX-FileCopyrightText: 2011 David Faure <faure@kde.org>
5 SPDX-FileCopyrightText: 2011 Kevin Ottens <ervin@kde.org>
6 SPDX-FileCopyrightText: 2019 Harald Sitter <sitter@kde.org>
7
8 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
9*/
10
11#include "kdbusservice.h"
12
13#include <QCoreApplication>
14#include <QDebug>
15
16#include <QDBusConnection>
17#include <QDBusConnectionInterface>
18#include <QDBusReply>
19
20#include "FreeDesktopApplpicationIface.h"
21#include "KDBusServiceIface.h"
22
23#include "config-kdbusaddons.h"
24
25#if HAVE_X11
26#include <private/qtx11extras_p.h>
27#endif
28
29#include "kdbusaddons_debug.h"
30#include "kdbusservice_adaptor.h"
31#include "kdbusserviceextensions_adaptor.h"
32
33class KDBusServicePrivate
34{
35public:
36 KDBusServicePrivate()
37 : registered(false)
38 , exitValue(0)
39 {
40 }
41
42 QString generateServiceName()
43 {
44 const QCoreApplication *app = QCoreApplication::instance();
45 const QString domain = app->organizationDomain();
46 const QStringList parts = domain.split(sep: QLatin1Char('.'), behavior: Qt::SkipEmptyParts);
47
48 QString reversedDomain;
49 if (parts.isEmpty()) {
50 reversedDomain = QStringLiteral("local.");
51 } else {
52 for (const QString &part : parts) {
53 reversedDomain.prepend(c: QLatin1Char('.'));
54 reversedDomain.prepend(s: part);
55 }
56 }
57
58 return reversedDomain + app->applicationName();
59 }
60
61 static void handlePlatformData(const QVariantMap &platformData)
62 {
63 #if HAVE_X11
64 if (QX11Info::isPlatformX11()) {
65 QByteArray desktopStartupId = platformData.value(QStringLiteral("desktop-startup-id")).toByteArray();
66 if (!desktopStartupId.isEmpty()) {
67 QX11Info::setNextStartupId(desktopStartupId);
68 }
69 }
70 #endif
71
72 const auto xdgActivationToken = platformData.value(key: QLatin1String("activation-token")).toByteArray();
73 if (!xdgActivationToken.isEmpty()) {
74 qputenv(varName: "XDG_ACTIVATION_TOKEN", value: xdgActivationToken);
75 }
76 }
77
78 bool registered;
79 QString serviceName;
80 QString errorMessage;
81 int exitValue;
82};
83
84// Wraps a serviceName registration.
85class Registration : public QObject
86{
87 Q_OBJECT
88public:
89 enum class Register {
90 RegisterWitoutQueue,
91 RegisterWithQueue,
92 };
93
94 Registration(KDBusService *s_, KDBusServicePrivate *d_, KDBusService::StartupOptions options_)
95 : s(s_)
96 , d(d_)
97 , options(options_)
98 {
99 if (!QDBusConnection::sessionBus().isConnected() || !(bus = QDBusConnection::sessionBus().interface())) {
100 d->errorMessage = QLatin1String(
101 "DBus session bus not found. To circumvent this problem try the following command (with bash):\n"
102 " export $(dbus-launch)");
103 } else {
104 generateServiceName();
105 }
106 }
107
108 void run()
109 {
110 if (bus) {
111 registerOnBus();
112 }
113
114 if (!d->registered && ((options & KDBusService::NoExitOnFailure) == 0)) {
115 qCCritical(KDBUSADDONS_LOG) << qPrintable(d->errorMessage);
116 exit(status: 1);
117 }
118 }
119
120private:
121 void generateServiceName()
122 {
123 d->serviceName = d->generateServiceName();
124 objectPath = QLatin1Char('/') + d->serviceName;
125 objectPath.replace(before: QLatin1Char('.'), after: QLatin1Char('/'));
126 objectPath.replace(before: QLatin1Char('-'), after: QLatin1Char('_')); // see spec change at https://bugs.freedesktop.org/show_bug.cgi?id=95129
127
128 if (options & KDBusService::Multiple) {
129 const bool inSandbox = QFileInfo::exists(QStringLiteral("/.flatpak-info"));
130 if (inSandbox) {
131 d->serviceName += QStringLiteral(".kdbus-")
132 + QDBusConnection::sessionBus().baseService().replace(re: QRegularExpression(QStringLiteral("[\\.:]")), QStringLiteral("_"));
133 } else {
134 d->serviceName += QLatin1Char('-') + QString::number(QCoreApplication::applicationPid());
135 }
136 }
137 }
138
139 void registerOnBus()
140 {
141 auto bus = QDBusConnection::sessionBus();
142 bool objectRegistered = false;
143 objectRegistered = bus.registerObject(QStringLiteral("/MainApplication"),
144 object: QCoreApplication::instance(),
145 options: QDBusConnection::ExportAllSlots //
146 | QDBusConnection::ExportScriptableProperties //
147 | QDBusConnection::ExportAdaptors);
148 if (!objectRegistered) {
149 qCWarning(KDBUSADDONS_LOG) << "Failed to register /MainApplication on DBus";
150 return;
151 }
152
153 objectRegistered = bus.registerObject(path: objectPath, object: s, options: QDBusConnection::ExportAdaptors);
154 if (!objectRegistered) {
155 qCWarning(KDBUSADDONS_LOG) << "Failed to register" << objectPath << "on DBus";
156 return;
157 }
158
159 attemptRegistration();
160 }
161
162 void attemptRegistration()
163 {
164 Q_ASSERT(!d->registered);
165
166 auto queueOption = QDBusConnectionInterface::DontQueueService;
167
168 if (options & KDBusService::Unique) {
169 // When a process crashes and gets auto-restarted by KCrash we may
170 // be in this code path "too early". There is a bit of a delay
171 // between the restart and the previous process dropping off of the
172 // bus and thus releasing its registered names. As a result there
173 // is a good chance that if we wait a bit the name will shortly
174 // become registered.
175
176 queueOption = QDBusConnectionInterface::QueueService;
177
178 connect(sender: bus, signal: &QDBusConnectionInterface::serviceRegistered, context: this, slot: [this](const QString &service) {
179 if (service != d->serviceName) {
180 return;
181 }
182
183 d->registered = true;
184 registrationLoop.quit();
185 });
186 }
187
188 d->registered = (bus->registerService(serviceName: d->serviceName, qoption: queueOption) == QDBusConnectionInterface::ServiceRegistered);
189
190 if (d->registered) {
191 return;
192 }
193
194 if (options & KDBusService::Replace) {
195 auto message = QDBusMessage::createMethodCall(destination: d->serviceName,
196 QStringLiteral("/MainApplication"),
197 QStringLiteral("org.qtproject.Qt.QCoreApplication"),
198 QStringLiteral("quit"));
199 QDBusConnection::sessionBus().asyncCall(message);
200 waitForRegistration();
201 } else if (options & KDBusService::Unique) {
202 // Already running so it's ok!
203 QVariantMap platform_data;
204#if HAVE_X11
205 if (QX11Info::isPlatformX11()) {
206 QString startupId = QString::fromUtf8(ba: qgetenv(varName: "DESKTOP_STARTUP_ID"));
207 if (startupId.isEmpty()) {
208 startupId = QString::fromUtf8(ba: QX11Info::nextStartupId());
209 }
210 if (!startupId.isEmpty()) {
211 platform_data.insert(QStringLiteral("desktop-startup-id"), value: startupId);
212 }
213 }
214#endif
215
216 if (qEnvironmentVariableIsSet(varName: "XDG_ACTIVATION_TOKEN")) {
217 platform_data.insert(QStringLiteral("activation-token"), value: qgetenv(varName: "XDG_ACTIVATION_TOKEN"));
218 }
219
220 if (QCoreApplication::arguments().count() > 1) {
221 OrgKdeKDBusServiceInterface iface(d->serviceName, objectPath, QDBusConnection::sessionBus());
222 iface.setTimeout(5 * 60 * 1000); // Application can take time to answer
223 QDBusReply<int> reply = iface.CommandLine(arguments: QCoreApplication::arguments(), working_dir: QDir::currentPath(), platform_data);
224 if (reply.isValid()) {
225 exit(status: reply.value());
226 } else {
227 d->errorMessage = reply.error().message();
228 }
229 } else {
230 OrgFreedesktopApplicationInterface iface(d->serviceName, objectPath, QDBusConnection::sessionBus());
231 iface.setTimeout(5 * 60 * 1000); // Application can take time to answer
232 QDBusReply<void> reply = iface.Activate(platform_data);
233 if (reply.isValid()) {
234 exit(status: 0);
235 } else {
236 d->errorMessage = reply.error().message();
237 }
238 }
239
240 // service did not respond in a valid way....
241 // let's wait to see if our queued registration finishes perhaps.
242 waitForRegistration();
243 }
244
245 if (!d->registered) { // either multi service or failed to reclaim name
246 d->errorMessage = QLatin1String("Failed to register name '") + d->serviceName + QLatin1String("' with DBUS - does this process have permission to use the name, and do no other processes own it already?");
247 }
248 }
249
250 void waitForRegistration()
251 {
252 QTimer quitTimer;
253 // We have to wait for the other application to quit completely which could take a while
254 quitTimer.start(msec: 8000);
255 connect(sender: &quitTimer, signal: &QTimer::timeout, context: &registrationLoop, slot: &QEventLoop::quit);
256 registrationLoop.exec();
257 }
258
259 QDBusConnectionInterface *bus = nullptr;
260 KDBusService *s = nullptr;
261 KDBusServicePrivate *d = nullptr;
262 KDBusService::StartupOptions options;
263 QEventLoop registrationLoop;
264 QString objectPath;
265};
266
267KDBusService::KDBusService(StartupOptions options, QObject *parent)
268 : QObject(parent)
269 , d(new KDBusServicePrivate)
270{
271 new KDBusServiceAdaptor(this);
272 new KDBusServiceExtensionsAdaptor(this);
273
274 Registration registration(this, d.get(), options);
275 registration.run();
276}
277
278KDBusService::~KDBusService() = default;
279
280bool KDBusService::isRegistered() const
281{
282 return d->registered;
283}
284
285QString KDBusService::errorMessage() const
286{
287 return d->errorMessage;
288}
289
290void KDBusService::setExitValue(int value)
291{
292 d->exitValue = value;
293}
294
295QString KDBusService::serviceName() const
296{
297 return d->serviceName;
298}
299
300void KDBusService::unregister()
301{
302 QDBusConnectionInterface *bus = nullptr;
303 if (!d->registered || !QDBusConnection::sessionBus().isConnected() || !(bus = QDBusConnection::sessionBus().interface())) {
304 return;
305 }
306 bus->unregisterService(serviceName: d->serviceName);
307}
308
309void KDBusService::Activate(const QVariantMap &platform_data)
310{
311 d->handlePlatformData(platformData: platform_data);
312 Q_EMIT activateRequested(arguments: QStringList(QCoreApplication::arguments()[0]), workingDirectory: QDir::currentPath());
313 qunsetenv(varName: "XDG_ACTIVATION_TOKEN");
314}
315
316void KDBusService::Open(const QStringList &uris, const QVariantMap &platform_data)
317{
318 d->handlePlatformData(platformData: platform_data);
319 Q_EMIT openRequested(uris: QUrl::fromStringList(uris));
320 qunsetenv(varName: "XDG_ACTIVATION_TOKEN");
321}
322
323void KDBusService::ActivateAction(const QString &action_name, const QVariantList &maybeParameter, const QVariantMap &platform_data)
324{
325 d->handlePlatformData(platformData: platform_data);
326
327 // This is a workaround for D-Bus not supporting null variants.
328 const QVariant param = maybeParameter.count() == 1 ? maybeParameter.first() : QVariant();
329
330 Q_EMIT activateActionRequested(actionName: action_name, parameter: param);
331 qunsetenv(varName: "XDG_ACTIVATION_TOKEN");
332}
333
334int KDBusService::CommandLine(const QStringList &arguments, const QString &workingDirectory, const QVariantMap &platform_data)
335{
336 d->exitValue = 0;
337 d->handlePlatformData(platformData: platform_data);
338 // The TODOs here only make sense if this method can be called from the GUI.
339 // If it's for pure "usage in the terminal" then no startup notification got started.
340 // But maybe one day the workspace wants to call this for the Exec key of a .desktop file?
341 Q_EMIT activateRequested(arguments, workingDirectory);
342 qunsetenv(varName: "XDG_ACTIVATION_TOKEN");
343 return d->exitValue;
344}
345
346#include "kdbusservice.moc"
347#include "moc_kdbusservice.cpp"
348

source code of kdbusaddons/src/kdbusservice.cpp