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 <QX11Info>
27 #endif
28 
29 #include "kdbusaddons_debug.h"
30 #include "kdbusservice_adaptor.h"
31 #include "kdbusserviceextensions_adaptor.h"
32 
33 class KDBusServicePrivate
34 {
35 public:
KDBusServicePrivate()36     KDBusServicePrivate()
37         : registered(false)
38         , exitValue(0)
39     {
40     }
41 
generateServiceName()42     QString generateServiceName()
43     {
44         const QCoreApplication *app = QCoreApplication::instance();
45         const QString domain = app->organizationDomain();
46         const QStringList parts = domain.split(QLatin1Char('.'), 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(QLatin1Char('.'));
54                 reversedDomain.prepend(part);
55             }
56         }
57 
58         return reversedDomain + app->applicationName();
59     }
60 
handlePlatformData(const QVariantMap & platformData)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(QLatin1String("activation-token")).toByteArray();
73         if (!xdgActivationToken.isEmpty()) {
74             qputenv("XDG_ACTIVATION_TOKEN", xdgActivationToken);
75         }
76     }
77 
78     bool registered;
79     QString serviceName;
80     QString errorMessage;
81     int exitValue;
82 };
83 
84 // Wraps a serviceName registration.
85 class Registration : public QObject
86 {
87     Q_OBJECT
88 public:
89     enum class Register {
90         RegisterWitoutQueue,
91         RegisterWithQueue,
92     };
93 
Registration(KDBusService * s_,KDBusServicePrivate * d_,KDBusService::StartupOptions options_)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 
run()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(1);
117         }
118     }
119 
120 private:
generateServiceName()121     void generateServiceName()
122     {
123         d->serviceName = d->generateServiceName();
124         objectPath = QLatin1Char('/') + d->serviceName;
125         objectPath.replace(QLatin1Char('.'), QLatin1Char('/'));
126         objectPath.replace(QLatin1Char('-'), 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(QRegularExpression(QStringLiteral("[\\.:]")), QStringLiteral("_"));
133             } else {
134                 d->serviceName += QLatin1Char('-') + QString::number(QCoreApplication::applicationPid());
135             }
136         } else if (options & KDBusService::Unique) {
137             auto reply = bus->registeredServiceNames();
138             if (!reply.isValid()) {
139                 return;
140             }
141 
142             for (const auto& serviceName : reply.value()) {
143                 if (serviceName.startsWith(d->serviceName)) {
144                     d->serviceName = serviceName;
145                     return;
146                 }
147             }
148         }
149     }
150 
registerOnBus()151     void registerOnBus()
152     {
153         auto bus = QDBusConnection::sessionBus();
154         bool objectRegistered = false;
155         objectRegistered = bus.registerObject(QStringLiteral("/MainApplication"),
156                                               QCoreApplication::instance(),
157                                               QDBusConnection::ExportAllSlots //
158                                                   | QDBusConnection::ExportScriptableProperties //
159                                                   | QDBusConnection::ExportAdaptors);
160         if (!objectRegistered) {
161             qCWarning(KDBUSADDONS_LOG) << "Failed to register /MainApplication on DBus";
162             return;
163         }
164 
165         objectRegistered = bus.registerObject(objectPath, s, QDBusConnection::ExportAdaptors);
166         if (!objectRegistered) {
167             qCWarning(KDBUSADDONS_LOG) << "Failed to register" << objectPath << "on DBus";
168             return;
169         }
170 
171         attemptRegistration();
172 
173         if (d->registered) {
174             if (QCoreApplication *app = QCoreApplication::instance()) {
175                 connect(app, &QCoreApplication::aboutToQuit, s, &KDBusService::unregister);
176             }
177         }
178     }
179 
attemptRegistration()180     void attemptRegistration()
181     {
182         Q_ASSERT(!d->registered);
183 
184         auto queueOption = QDBusConnectionInterface::DontQueueService;
185 
186         if (options & KDBusService::Unique) {
187             // When a process crashes and gets auto-restarted by KCrash we may
188             // be in this code path "too early". There is a bit of a delay
189             // between the restart and the previous process dropping off of the
190             // bus and thus releasing its registered names. As a result there
191             // is a good chance that if we wait a bit the name will shortly
192             // become registered.
193 
194             queueOption = QDBusConnectionInterface::QueueService;
195 
196             connect(bus, &QDBusConnectionInterface::serviceRegistered, this, [this](const QString &service) {
197                 if (service != d->serviceName) {
198                     return;
199                 }
200 
201                 d->registered = true;
202                 registrationLoop.quit();
203             });
204         }
205 
206         d->registered = (bus->registerService(d->serviceName, queueOption) == QDBusConnectionInterface::ServiceRegistered);
207 
208         if (d->registered) {
209             return;
210         }
211 
212         if (options & KDBusService::Replace) {
213             auto message = QDBusMessage::createMethodCall(d->serviceName,
214                                                           QStringLiteral("/MainApplication"),
215                                                           QStringLiteral("org.qtproject.Qt.QCoreApplication"),
216                                                           QStringLiteral("quit"));
217             QDBusConnection::sessionBus().asyncCall(message);
218             waitForRegistration();
219         } else if (options & KDBusService::Unique) {
220             // Already running so it's ok!
221             QVariantMap platform_data;
222 #if HAVE_X11
223             if (QX11Info::isPlatformX11()) {
224                 QString startupId = QString::fromUtf8(qgetenv("DESKTOP_STARTUP_ID"));
225                 if (startupId.isEmpty()) {
226                     startupId = QString::fromUtf8(QX11Info::nextStartupId());
227                 }
228                 if (!startupId.isEmpty()) {
229                     platform_data.insert(QStringLiteral("desktop-startup-id"), startupId);
230                 }
231             }
232 #endif
233 
234             if (qEnvironmentVariableIsSet("XDG_ACTIVATION_TOKEN")) {
235                 platform_data.insert(QStringLiteral("activation-token"), qgetenv("XDG_ACTIVATION_TOKEN"));
236             }
237 
238             if (QCoreApplication::arguments().count() > 1) {
239                 OrgKdeKDBusServiceInterface iface(d->serviceName, objectPath, QDBusConnection::sessionBus());
240                 iface.setTimeout(5 * 60 * 1000); // Application can take time to answer
241                 QDBusReply<int> reply = iface.CommandLine(QCoreApplication::arguments(), QDir::currentPath(), platform_data);
242                 if (reply.isValid()) {
243                     exit(reply.value());
244                 } else {
245                     d->errorMessage = reply.error().message();
246                 }
247             } else {
248                 OrgFreedesktopApplicationInterface iface(d->serviceName, objectPath, QDBusConnection::sessionBus());
249                 iface.setTimeout(5 * 60 * 1000); // Application can take time to answer
250                 QDBusReply<void> reply = iface.Activate(platform_data);
251                 if (reply.isValid()) {
252                     exit(0);
253                 } else {
254                     d->errorMessage = reply.error().message();
255                 }
256             }
257 
258             // service did not respond in a valid way....
259             // let's wait to see if our queued registration finishes perhaps.
260             waitForRegistration();
261         }
262 
263         if (!d->registered) { // either multi service or failed to reclaim name
264             d->errorMessage = QLatin1String("Couldn't register name '") + d->serviceName + QLatin1String("' with DBUS - another process owns it already!");
265         }
266     }
267 
waitForRegistration()268     void waitForRegistration()
269     {
270         QTimer quitTimer;
271         // Wait a bit longer when we know this instance was restarted. There's
272         // a very good chance we'll eventually get the name once the defunct
273         // process closes its sockets.
274         quitTimer.start(qEnvironmentVariableIsSet("KCRASH_AUTO_RESTARTED") ? 8000 : 2000);
275         connect(&quitTimer, &QTimer::timeout, &registrationLoop, &QEventLoop::quit);
276         registrationLoop.exec();
277     }
278 
279     QDBusConnectionInterface *bus = nullptr;
280     KDBusService *s = nullptr;
281     KDBusServicePrivate *d = nullptr;
282     KDBusService::StartupOptions options;
283     QEventLoop registrationLoop;
284     QString objectPath;
285 };
286 
KDBusService(StartupOptions options,QObject * parent)287 KDBusService::KDBusService(StartupOptions options, QObject *parent)
288     : QObject(parent)
289     , d(new KDBusServicePrivate)
290 {
291     new KDBusServiceAdaptor(this);
292     new KDBusServiceExtensionsAdaptor(this);
293 
294     Registration registration(this, d.get(), options);
295     registration.run();
296 }
297 
298 KDBusService::~KDBusService() = default;
299 
isRegistered() const300 bool KDBusService::isRegistered() const
301 {
302     return d->registered;
303 }
304 
errorMessage() const305 QString KDBusService::errorMessage() const
306 {
307     return d->errorMessage;
308 }
309 
setExitValue(int value)310 void KDBusService::setExitValue(int value)
311 {
312     d->exitValue = value;
313 }
314 
serviceName() const315 QString KDBusService::serviceName() const
316 {
317     return d->serviceName;
318 }
319 
unregister()320 void KDBusService::unregister()
321 {
322     QDBusConnectionInterface *bus = nullptr;
323     if (!d->registered || !QDBusConnection::sessionBus().isConnected() || !(bus = QDBusConnection::sessionBus().interface())) {
324         return;
325     }
326     bus->unregisterService(d->serviceName);
327 }
328 
Activate(const QVariantMap & platform_data)329 void KDBusService::Activate(const QVariantMap &platform_data)
330 {
331     d->handlePlatformData(platform_data);
332     Q_EMIT activateRequested(QStringList(), QString());
333     qunsetenv("XDG_ACTIVATION_TOKEN");
334 }
335 
Open(const QStringList & uris,const QVariantMap & platform_data)336 void KDBusService::Open(const QStringList &uris, const QVariantMap &platform_data)
337 {
338     d->handlePlatformData(platform_data);
339     Q_EMIT openRequested(QUrl::fromStringList(uris));
340     qunsetenv("XDG_ACTIVATION_TOKEN");
341 }
342 
ActivateAction(const QString & action_name,const QVariantList & maybeParameter,const QVariantMap & platform_data)343 void KDBusService::ActivateAction(const QString &action_name, const QVariantList &maybeParameter, const QVariantMap &platform_data)
344 {
345     d->handlePlatformData(platform_data);
346 
347     // This is a workaround for D-Bus not supporting null variants.
348     const QVariant param = maybeParameter.count() == 1 ? maybeParameter.first() : QVariant();
349 
350     Q_EMIT activateActionRequested(action_name, param);
351     qunsetenv("XDG_ACTIVATION_TOKEN");
352 }
353 
CommandLine(const QStringList & arguments,const QString & workingDirectory,const QVariantMap & platform_data)354 int KDBusService::CommandLine(const QStringList &arguments, const QString &workingDirectory, const QVariantMap &platform_data)
355 {
356     d->exitValue = 0;
357     d->handlePlatformData(platform_data);
358     // The TODOs here only make sense if this method can be called from the GUI.
359     // If it's for pure "usage in the terminal" then no startup notification got started.
360     // But maybe one day the workspace wants to call this for the Exec key of a .desktop file?
361     Q_EMIT activateRequested(arguments, workingDirectory);
362     qunsetenv("XDG_ACTIVATION_TOKEN");
363     return d->exitValue;
364 }
365 
366 #include "kdbusservice.moc"
367