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 Registration(KDBusService *s_, KDBusServicePrivate *d_, KDBusService::StartupOptions options_)
90 : s(s_)
91 , d(d_)
92 , options(options_)
93 {
94 if (!QDBusConnection::sessionBus().isConnected() || !(bus = QDBusConnection::sessionBus().interface())) {
95 d->errorMessage = QLatin1String(
96 "DBus session bus not found. To circumvent this problem try the following command (with bash):\n"
97 " export $(dbus-launch)");
98 } else {
99 generateServiceName();
100 }
101 }
102
103 void run()
104 {
105 if (bus) {
106 registerOnBus();
107 }
108
109 if (!d->registered && ((options & KDBusService::NoExitOnFailure) == 0)) {
110 qCCritical(KDBUSADDONS_LOG) << qPrintable(d->errorMessage);
111 exit(status: 1);
112 }
113 }
114
115private:
116 void generateServiceName()
117 {
118 d->serviceName = d->generateServiceName();
119 objectPath = QLatin1Char('/') + d->serviceName;
120 objectPath.replace(before: QLatin1Char('.'), after: QLatin1Char('/'));
121 objectPath.replace(before: QLatin1Char('-'), after: QLatin1Char('_')); // see spec change at https://bugs.freedesktop.org/show_bug.cgi?id=95129
122
123 if (options & KDBusService::Multiple) {
124 const bool inSandbox = QFileInfo::exists(QStringLiteral("/.flatpak-info"));
125 if (inSandbox) {
126 d->serviceName += QStringLiteral(".kdbus-")
127 + QDBusConnection::sessionBus().baseService().replace(re: QRegularExpression(QStringLiteral("[\\.:]")), QStringLiteral("_"));
128 } else {
129 d->serviceName += QLatin1Char('-') + QString::number(QCoreApplication::applicationPid());
130 }
131 }
132 }
133
134 void registerOnBus()
135 {
136 auto bus = QDBusConnection::sessionBus();
137 bool objectRegistered = false;
138 objectRegistered = bus.registerObject(QStringLiteral("/MainApplication"),
139 object: QCoreApplication::instance(),
140 options: QDBusConnection::ExportAllSlots //
141 | QDBusConnection::ExportScriptableProperties //
142 | QDBusConnection::ExportAdaptors);
143 if (!objectRegistered) {
144 qCWarning(KDBUSADDONS_LOG) << "Failed to register /MainApplication on DBus";
145 return;
146 }
147
148 objectRegistered = bus.registerObject(path: objectPath, object: s, options: QDBusConnection::ExportAdaptors);
149 if (!objectRegistered) {
150 qCWarning(KDBUSADDONS_LOG) << "Failed to register" << objectPath << "on DBus";
151 return;
152 }
153
154 attemptRegistration();
155 }
156
157 void attemptRegistration()
158 {
159 Q_ASSERT(!d->registered);
160
161 auto queueOption = QDBusConnectionInterface::DontQueueService;
162
163 if (options & KDBusService::Unique) {
164 // When a process crashes and gets auto-restarted by KCrash we may
165 // be in this code path "too early". There is a bit of a delay
166 // between the restart and the previous process dropping off of the
167 // bus and thus releasing its registered names. As a result there
168 // is a good chance that if we wait a bit the name will shortly
169 // become registered.
170
171 queueOption = QDBusConnectionInterface::QueueService;
172
173 connect(sender: bus, signal: &QDBusConnectionInterface::serviceRegistered, context: this, slot: [this](const QString &service) {
174 if (service != d->serviceName) {
175 return;
176 }
177
178 d->registered = true;
179 registrationLoop.quit();
180 });
181 }
182
183 d->registered = (bus->registerService(serviceName: d->serviceName, qoption: queueOption) == QDBusConnectionInterface::ServiceRegistered);
184
185 if (d->registered) {
186 return;
187 }
188
189 if (options & KDBusService::Replace) {
190 auto message = QDBusMessage::createMethodCall(destination: d->serviceName,
191 QStringLiteral("/MainApplication"),
192 QStringLiteral("org.qtproject.Qt.QCoreApplication"),
193 QStringLiteral("quit"));
194 QDBusConnection::sessionBus().asyncCall(message);
195 waitForRegistration();
196 } else if (options & KDBusService::Unique) {
197 // Already running so it's ok!
198 QVariantMap platform_data;
199#if HAVE_X11
200 if (QX11Info::isPlatformX11()) {
201 QString startupId = QString::fromUtf8(ba: qgetenv(varName: "DESKTOP_STARTUP_ID"));
202 if (startupId.isEmpty()) {
203 startupId = QString::fromUtf8(ba: QX11Info::nextStartupId());
204 }
205 if (!startupId.isEmpty()) {
206 platform_data.insert(QStringLiteral("desktop-startup-id"), value: startupId);
207 }
208 }
209#endif
210
211 if (qEnvironmentVariableIsSet(varName: "XDG_ACTIVATION_TOKEN")) {
212 platform_data.insert(QStringLiteral("activation-token"), value: qgetenv(varName: "XDG_ACTIVATION_TOKEN"));
213 }
214
215 if (QCoreApplication::arguments().count() > 1) {
216 OrgKdeKDBusServiceInterface iface(d->serviceName, objectPath, QDBusConnection::sessionBus());
217 iface.setTimeout(5 * 60 * 1000); // Application can take time to answer
218 QDBusReply<int> reply = iface.CommandLine(arguments: QCoreApplication::arguments(), working_dir: QDir::currentPath(), platform_data);
219 if (reply.isValid()) {
220 exit(status: reply.value());
221 } else {
222 d->errorMessage = reply.error().message();
223 }
224 } else {
225 OrgFreedesktopApplicationInterface iface(d->serviceName, objectPath, QDBusConnection::sessionBus());
226 iface.setTimeout(5 * 60 * 1000); // Application can take time to answer
227 QDBusReply<void> reply = iface.Activate(platform_data);
228 if (reply.isValid()) {
229 exit(status: 0);
230 } else {
231 d->errorMessage = reply.error().message();
232 }
233 }
234
235 // service did not respond in a valid way....
236 // let's wait to see if our queued registration finishes perhaps.
237 waitForRegistration();
238 }
239
240 if (!d->registered) { // either multi service or failed to reclaim name
241 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?");
242 }
243 }
244
245 void waitForRegistration()
246 {
247 QTimer quitTimer;
248 // We have to wait for the other application to quit completely which could take a while
249 quitTimer.start(msec: 8000);
250 connect(sender: &quitTimer, signal: &QTimer::timeout, context: &registrationLoop, slot: &QEventLoop::quit);
251 registrationLoop.exec();
252 }
253
254 QDBusConnectionInterface *bus = nullptr;
255 KDBusService *s = nullptr;
256 KDBusServicePrivate *d = nullptr;
257 KDBusService::StartupOptions options;
258 QEventLoop registrationLoop;
259 QString objectPath;
260};
261
262KDBusService::KDBusService(StartupOptions options, QObject *parent)
263 : QObject(parent)
264 , d(new KDBusServicePrivate)
265{
266 new KDBusServiceAdaptor(this);
267 new KDBusServiceExtensionsAdaptor(this);
268
269 Registration registration(this, d.get(), options);
270 registration.run();
271}
272
273KDBusService::~KDBusService() = default;
274
275bool KDBusService::isRegistered() const
276{
277 return d->registered;
278}
279
280QString KDBusService::errorMessage() const
281{
282 return d->errorMessage;
283}
284
285void KDBusService::setExitValue(int value)
286{
287 d->exitValue = value;
288}
289
290QString KDBusService::serviceName() const
291{
292 return d->serviceName;
293}
294
295void KDBusService::unregister()
296{
297 QDBusConnectionInterface *bus = nullptr;
298 if (!d->registered || !QDBusConnection::sessionBus().isConnected() || !(bus = QDBusConnection::sessionBus().interface())) {
299 return;
300 }
301 bus->unregisterService(serviceName: d->serviceName);
302}
303
304void KDBusService::Activate(const QVariantMap &platform_data)
305{
306 d->handlePlatformData(platformData: platform_data);
307 Q_EMIT activateRequested(arguments: QStringList(QCoreApplication::arguments()[0]), workingDirectory: QDir::currentPath());
308 qunsetenv(varName: "XDG_ACTIVATION_TOKEN");
309}
310
311void KDBusService::Open(const QStringList &uris, const QVariantMap &platform_data)
312{
313 d->handlePlatformData(platformData: platform_data);
314 Q_EMIT openRequested(uris: QUrl::fromStringList(uris));
315 qunsetenv(varName: "XDG_ACTIVATION_TOKEN");
316}
317
318void KDBusService::ActivateAction(const QString &action_name, const QVariantList &maybeParameter, const QVariantMap &platform_data)
319{
320 d->handlePlatformData(platformData: platform_data);
321
322 // This is a workaround for D-Bus not supporting null variants.
323 const QVariant param = maybeParameter.count() == 1 ? maybeParameter.first() : QVariant();
324
325 Q_EMIT activateActionRequested(actionName: action_name, parameter: param);
326 qunsetenv(varName: "XDG_ACTIVATION_TOKEN");
327}
328
329int KDBusService::CommandLine(const QStringList &arguments, const QString &workingDirectory, const QVariantMap &platform_data)
330{
331 d->exitValue = 0;
332 d->handlePlatformData(platformData: platform_data);
333 // The TODOs here only make sense if this method can be called from the GUI.
334 // If it's for pure "usage in the terminal" then no startup notification got started.
335 // But maybe one day the workspace wants to call this for the Exec key of a .desktop file?
336 Q_EMIT activateRequested(arguments, workingDirectory);
337 qunsetenv(varName: "XDG_ACTIVATION_TOKEN");
338 return d->exitValue;
339}
340
341#include "kdbusservice.moc"
342#include "moc_kdbusservice.cpp"
343

source code of kdbusaddons/src/kdbusservice.cpp