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
162 void attemptRegistration()
163 {
164 Q_ASSERT(!d->registered);
165
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
177
178 connect(bus, &QDBusConnectionInterface::serviceRegistered, this, [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(d->serviceName, queueOption) == QDBusConnectionInterface::ServiceRegistered);
189
190 if (d->registered) {
191 return;
192 }
193
194 if (options & KDBusService::Replace) {
195 auto message = QDBusMessage::createMethodCall(d->serviceName,
196 QStringLiteral("/MainApplication"),
197 QStringLiteral("org.qtproject.Qt.QCoreApplication"),
198 QStringLiteral("quit"));
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(qgetenv("DESKTOP_STARTUP_ID"));
207 if (startupId.isEmpty()) {
208 startupId = QString::fromUtf8(QX11Info::nextStartupId());
209 }
210 if (!startupId.isEmpty()) {
211 platform_data.insert(QStringLiteral("desktop-startup-id"), startupId);
212 }
213 }
214#endif
215
216 if (qEnvironmentVariableIsSet("XDG_ACTIVATION_TOKEN")) {
217 platform_data.insert(QStringLiteral("activation-token"), qgetenv("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(QCoreApplication::arguments(), QDir::currentPath(), platform_data);
224 if (reply.isValid()) {
225 exit(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(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 // Wait a bit longer when we know this instance was restarted. There's
254 // a very good chance we'll eventually get the name once the defunct
255 // process closes its sockets.
256 quitTimer.start(qEnvironmentVariableIsSet("KCRASH_AUTO_RESTARTED") ? 8000 : 2000);
257 connect(&quitTimer, &QTimer::timeout, &registrationLoop, &QEventLoop::quit);
258 registrationLoop.exec();
259 }
260
261 QDBusConnectionInterface *bus = nullptr;
262 KDBusService *s = nullptr;
263 KDBusServicePrivate *d = nullptr;
265 QEventLoop registrationLoop;
266 QString objectPath;
267};
268
270 : QObject(parent)
271 , d(new KDBusServicePrivate)
272{
273 new KDBusServiceAdaptor(this);
274 new KDBusServiceExtensionsAdaptor(this);
275
276 Registration registration(this, d.get(), options);
277 registration.run();
278}
279
281
283{
284 return d->registered;
285}
286
288{
289 return d->errorMessage;
290}
291
293{
294 d->exitValue = value;
295}
296
298{
299 return d->serviceName;
300}
301
303{
304 QDBusConnectionInterface *bus = nullptr;
305 if (!d->registered || !QDBusConnection::sessionBus().isConnected() || !(bus = QDBusConnection::sessionBus().interface())) {
306 return;
307 }
308 bus->unregisterService(d->serviceName);
309}
310
311void KDBusService::Activate(const QVariantMap &platform_data)
312{
313 d->handlePlatformData(platform_data);
315 qunsetenv("XDG_ACTIVATION_TOKEN");
316}
317
318void KDBusService::Open(const QStringList &uris, const QVariantMap &platform_data)
319{
320 d->handlePlatformData(platform_data);
322 qunsetenv("XDG_ACTIVATION_TOKEN");
323}
324
325void KDBusService::ActivateAction(const QString &action_name, const QVariantList &maybeParameter, const QVariantMap &platform_data)
326{
327 d->handlePlatformData(platform_data);
328
329 // This is a workaround for D-Bus not supporting null variants.
330 const QVariant param = maybeParameter.count() == 1 ? maybeParameter.first() : QVariant();
331
332 Q_EMIT activateActionRequested(action_name, param);
333 qunsetenv("XDG_ACTIVATION_TOKEN");
334}
335
336int KDBusService::CommandLine(const QStringList &arguments, const QString &workingDirectory, const QVariantMap &platform_data)
337{
338 d->exitValue = 0;
339 d->handlePlatformData(platform_data);
340 // The TODOs here only make sense if this method can be called from the GUI.
341 // If it's for pure "usage in the terminal" then no startup notification got started.
342 // But maybe one day the workspace wants to call this for the Exec key of a .desktop file?
343 Q_EMIT activateRequested(arguments, workingDirectory);
344 qunsetenv("XDG_ACTIVATION_TOKEN");
345 return d->exitValue;
346}
347
348#include "kdbusservice.moc"
349#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()
Manually unregister the given serviceName 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 Fri Oct 11 2024 12:14:09 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.