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, ®istrationLoop, &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