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 
33 class KDBusServicePrivate
34 {
35 public:
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;
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 
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 
120 private:
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 
286 KDBusService::~KDBusService() = default;
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 
317 void KDBusService::Activate(const QVariantMap &platform_data)
318 {
319  d->handlePlatformData(platform_data);
321  qunsetenv("XDG_ACTIVATION_TOKEN");
322 }
323 
324 void KDBusService::Open(const QStringList &uris, const QVariantMap &platform_data)
325 {
326  d->handlePlatformData(platform_data);
328  qunsetenv("XDG_ACTIVATION_TOKEN");
329 }
330 
331 void 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 
342 int 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"
Q_OBJECTQ_OBJECT
QFuture< T > run(Function function,...)
@ Multiple
Indicates that multiple instances of the application may exist.
Definition: kdbusservice.h:101
bool isRegistered() const
Returns true if the D-Bus registration succeeded.
QString number(int n, int base)
QString fromUtf8(const char *str, int size)
Q_EMITQ_EMIT
@ Replace
Indicates that if there's already a unique service running, to be quit and replaced with our own.
Definition: kdbusservice.h:121
QString message() const const
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool isValid() const const
void serviceRegistered(const QString &service)
QString & prepend(QChar ch)
qint64 applicationPid()
bool exists() const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QList< QUrl > fromStringList(const QStringList &urls, QUrl::ParsingMode mode)
QString baseService() const const
void openRequested(const QList< QUrl > &uris)
Signals that one or more files should be opened in the application.
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
@ Unique
Indicates that only one instance of this application should ever exist.
Definition: kdbusservice.h:95
void start(int msec)
const QDBusError & error()
QDBusConnection sessionBus()
void quit()
QString currentPath()
SkipEmptyParts
void timeout()
bool isEmpty() const const
QCoreApplication * instance()
void unregister()
Unregister from D-Bus.
bool isEmpty() const const
void activateActionRequested(const QString &actionName, const QVariant &parameter)
Signals that an application action should be triggered.
bool isConnected() const const
QDBusMessage createMethodCall(const QString &service, const QString &path, const QString &interface, const QString &method)
QDBusConnectionInterface * interface() const const
QString & replace(int position, int n, QChar after)
QDBusReply< bool > unregisterService(const QString &serviceName)
void activateRequested(const QStringList &arguments, const QString &workingDirectory)
Signals that the application is to be activated.
bool isEmpty() const const
@ NoExitOnFailure
Indicates that the application should not exit if it failed to register with D-Bus.
Definition: kdbusservice.h:111
QString errorMessage() const
Returns the error message from the D-Bus registration if it failed.
void setExitValue(int value)
Sets the exit value to be used for a duplicate instance.
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...
QString serviceName() const
Returns the name of the D-Bus service registered by this class.
QStringList arguments()
QDBusPendingCall asyncCall(const QDBusMessage &message, int timeout) const const
QDBusReply::Type value() const const
~KDBusService() override
Destroys this object (but does not unregister the application).
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Thu Feb 15 2024 03:49:05 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.