KDBusAddons

kdbusservice.cpp
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 {
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
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.
85class Registration : public QObject
86{
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(1);
117 }
118 }
119
120private:
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 {
135 }
136 }
137 }
138
139 void registerOnBus()
140 {
141 auto bus = QDBusConnection::sessionBus();
142 bool objectRegistered = false;
143 objectRegistered = bus.registerObject(QStringLiteral("/MainApplication"),
148 if (!objectRegistered) {
149 qCWarning(KDBUSADDONS_LOG) << "Failed to register /MainApplication on DBus";
150 return;
151 }
152
153 objectRegistered = bus.registerObject(objectPath, s, QDBusConnection::ExportAdaptors);
154 if (!objectRegistered) {
155 qCWarning(KDBUSADDONS_LOG) << "Failed to register" << objectPath << "on DBus";
156 return;
157 }
158
159 attemptRegistration();
160
161 if (d->registered) {
164 }
165 }
166 }
167
168 void attemptRegistration()
169 {
170 Q_ASSERT(!d->registered);
171
173
174 if (options & KDBusService::Unique) {
175 // When a process crashes and gets auto-restarted by KCrash we may
176 // be in this code path "too early". There is a bit of a delay
177 // between the restart and the previous process dropping off of the
178 // bus and thus releasing its registered names. As a result there
179 // is a good chance that if we wait a bit the name will shortly
180 // become registered.
181
183
184 connect(bus, &QDBusConnectionInterface::serviceRegistered, this, [this](const QString &service) {
185 if (service != d->serviceName) {
186 return;
187 }
188
189 d->registered = true;
190 registrationLoop.quit();
191 });
192 }
193
194 d->registered = (bus->registerService(d->serviceName, queueOption) == QDBusConnectionInterface::ServiceRegistered);
195
196 if (d->registered) {
197 return;
198 }
199
200 if (options & KDBusService::Replace) {
201 auto message = QDBusMessage::createMethodCall(d->serviceName,
202 QStringLiteral("/MainApplication"),
203 QStringLiteral("org.qtproject.Qt.QCoreApplication"),
204 QStringLiteral("quit"));
206 waitForRegistration();
207 } else if (options & KDBusService::Unique) {
208 // Already running so it's ok!
209 QVariantMap platform_data;
210#if HAVE_X11
211 if (QX11Info::isPlatformX11()) {
212 QString startupId = QString::fromUtf8(qgetenv("DESKTOP_STARTUP_ID"));
213 if (startupId.isEmpty()) {
214 startupId = QString::fromUtf8(QX11Info::nextStartupId());
215 }
216 if (!startupId.isEmpty()) {
217 platform_data.insert(QStringLiteral("desktop-startup-id"), startupId);
218 }
219 }
220#endif
221
222 if (qEnvironmentVariableIsSet("XDG_ACTIVATION_TOKEN")) {
223 platform_data.insert(QStringLiteral("activation-token"), qgetenv("XDG_ACTIVATION_TOKEN"));
224 }
225
226 if (QCoreApplication::arguments().count() > 1) {
227 OrgKdeKDBusServiceInterface iface(d->serviceName, objectPath, QDBusConnection::sessionBus());
228 iface.setTimeout(5 * 60 * 1000); // Application can take time to answer
229 QDBusReply<int> reply = iface.CommandLine(QCoreApplication::arguments(), QDir::currentPath(), platform_data);
230 if (reply.isValid()) {
231 exit(reply.value());
232 } else {
233 d->errorMessage = reply.error().message();
234 }
235 } else {
236 OrgFreedesktopApplicationInterface iface(d->serviceName, objectPath, QDBusConnection::sessionBus());
237 iface.setTimeout(5 * 60 * 1000); // Application can take time to answer
238 QDBusReply<void> reply = iface.Activate(platform_data);
239 if (reply.isValid()) {
240 exit(0);
241 } else {
242 d->errorMessage = reply.error().message();
243 }
244 }
245
246 // service did not respond in a valid way....
247 // let's wait to see if our queued registration finishes perhaps.
248 waitForRegistration();
249 }
250
251 if (!d->registered) { // either multi service or failed to reclaim name
252 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?");
253 }
254 }
255
256 void waitForRegistration()
257 {
258 QTimer quitTimer;
259 // Wait a bit longer when we know this instance was restarted. There's
260 // a very good chance we'll eventually get the name once the defunct
261 // process closes its sockets.
262 quitTimer.start(qEnvironmentVariableIsSet("KCRASH_AUTO_RESTARTED") ? 8000 : 2000);
263 connect(&quitTimer, &QTimer::timeout, &registrationLoop, &QEventLoop::quit);
264 registrationLoop.exec();
265 }
266
267 QDBusConnectionInterface *bus = nullptr;
268 KDBusService *s = nullptr;
269 KDBusServicePrivate *d = nullptr;
271 QEventLoop registrationLoop;
272 QString objectPath;
273};
274
276 : QObject(parent)
277 , d(new KDBusServicePrivate)
278{
279 new KDBusServiceAdaptor(this);
280 new KDBusServiceExtensionsAdaptor(this);
281
282 Registration registration(this, d.get(), options);
283 registration.run();
284}
285
287
289{
290 return d->registered;
291}
292
294{
295 return d->errorMessage;
296}
297
299{
300 d->exitValue = value;
301}
302
304{
305 return d->serviceName;
306}
307
309{
310 QDBusConnectionInterface *bus = nullptr;
311 if (!d->registered || !QDBusConnection::sessionBus().isConnected() || !(bus = QDBusConnection::sessionBus().interface())) {
312 return;
313 }
314 bus->unregisterService(d->serviceName);
315}
316
317void KDBusService::Activate(const QVariantMap &platform_data)
318{
319 d->handlePlatformData(platform_data);
321 qunsetenv("XDG_ACTIVATION_TOKEN");
322}
323
324void KDBusService::Open(const QStringList &uris, const QVariantMap &platform_data)
325{
326 d->handlePlatformData(platform_data);
328 qunsetenv("XDG_ACTIVATION_TOKEN");
329}
330
331void KDBusService::ActivateAction(const QString &action_name, const QVariantList &maybeParameter, const QVariantMap &platform_data)
332{
333 d->handlePlatformData(platform_data);
334
335 // This is a workaround for D-Bus not supporting null variants.
336 const QVariant param = maybeParameter.count() == 1 ? maybeParameter.first() : QVariant();
337
338 Q_EMIT activateActionRequested(action_name, param);
339 qunsetenv("XDG_ACTIVATION_TOKEN");
340}
341
342int KDBusService::CommandLine(const QStringList &arguments, const QString &workingDirectory, const QVariantMap &platform_data)
343{
344 d->exitValue = 0;
345 d->handlePlatformData(platform_data);
346 // The TODOs here only make sense if this method can be called from the GUI.
347 // If it's for pure "usage in the terminal" then no startup notification got started.
348 // But maybe one day the workspace wants to call this for the Exec key of a .desktop file?
349 Q_EMIT activateRequested(arguments, workingDirectory);
350 qunsetenv("XDG_ACTIVATION_TOKEN");
351 return d->exitValue;
352}
353
354#include "kdbusservice.moc"
355#include "moc_kdbusservice.cpp"
KDBusService takes care of registering the current process with D-Bus.
QString serviceName() const
Returns the name of the D-Bus service registered by this class.
void openRequested(const QList< QUrl > &uris)
Signals that one or more files should be opened in the application.
void unregister()
Unregister from D-Bus.
QString errorMessage() const
Returns the error message from the D-Bus registration if it failed.
~KDBusService() override
Destroys this object (but does not unregister the application).
bool isRegistered() const
Returns true if the D-Bus registration succeeded.
void activateActionRequested(const QString &actionName, const QVariant &parameter)
Signals that an application action should be triggered.
KDBusService(StartupOptions options=Multiple, QObject *parent=nullptr)
Tries to register the current process to D-Bus at an address based on the application name and organi...
@ NoExitOnFailure
Indicates that the application should not exit if it failed to register with D-Bus.
@ Multiple
Indicates that multiple instances of the application may exist.
@ Unique
Indicates that only one instance of this application should ever exist.
@ Replace
Indicates that if there's already a unique service running, to be quit and replaced with our own.
void setExitValue(int value)
Sets the exit value to be used for a duplicate instance.
void activateRequested(const QStringList &arguments, const QString &workingDirectory)
Signals that the application is to be activated.
bool isEmpty() const const
qint64 applicationPid()
QStringList arguments()
QCoreApplication * instance()
QDBusPendingCall asyncCall(const QDBusMessage &message, int timeout) const const
QString baseService() const const
QDBusConnectionInterface * interface() const const
bool isConnected() const const
QDBusConnection sessionBus()
QDBusReply< QDBusConnectionInterface::RegisterServiceReply > registerService(const QString &serviceName, ServiceQueueOptions qoption, ServiceReplacementOptions roption)
void serviceRegistered(const QString &service)
QDBusReply< bool > unregisterService(const QString &serviceName)
QString message() const const
QDBusMessage createMethodCall(const QString &service, const QString &path, const QString &interface, const QString &method)
const QDBusError & error()
bool isValid() const const
QString currentPath()
int exec(ProcessEventsFlags flags)
void quit()
bool exists() const const
bool isEmpty() const const
Q_EMITQ_EMIT
Q_OBJECTQ_OBJECT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
QString number(double n, char format, int precision)
QString & prepend(QChar ch)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
SkipEmptyParts
void start()
void timeout()
QList< QUrl > fromStringList(const QStringList &urls, ParsingMode mode)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:13:12 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.