KDBusAddons

kdbusservice.cpp
1 /*
2  This file is part of libkdbusaddons
3 
4  SPDX-FileCopyrightText: 2011 David Faure <[email protected]>
5  SPDX-FileCopyrightText: 2011 Kevin Ottens <[email protected]>
6  SPDX-FileCopyrightText: 2019 Harald Sitter <[email protected]>
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 "KDBusServiceIface.h"
21 #include "FreeDesktopApplpicationIface.h"
22 
23 #include "config-kdbusaddons.h"
24 
25 #if HAVE_X11
26 #include <QX11Info>
27 #endif
28 
29 #include "kdbusservice_adaptor.h"
30 #include "kdbusserviceextensions_adaptor.h"
31 
32 class KDBusServicePrivate
33 {
34 public:
35  KDBusServicePrivate()
36  : registered(false),
37  exitValue(0)
38  {}
39 
40  QString generateServiceName()
41  {
43  const QString domain = app->organizationDomain();
44 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
45  const QStringList parts = domain.split(QLatin1Char('.'), QString::SkipEmptyParts);
46 #else
47  const QStringList parts = domain.split(QLatin1Char('.'), Qt::SkipEmptyParts);
48 #endif
49 
50  QString reversedDomain;
51  if (parts.isEmpty()) {
52  reversedDomain = QStringLiteral("local.");
53  } else {
54  for (const QString &part : parts) {
55  reversedDomain.prepend(QLatin1Char('.'));
56  reversedDomain.prepend(part);
57  }
58  }
59 
60  return reversedDomain + app->applicationName();
61  }
62 
63  bool registered;
64  QString serviceName;
66  int exitValue;
67 };
68 
69 // Wraps a serviceName registration.
70 class Registration : public QObject {
71  Q_OBJECT
72 public:
73  enum class Register {
74  RegisterWitoutQueue,
75  RegisterWithQueue
76  };
77 
78  Registration(KDBusService *s_, KDBusServicePrivate *d_, KDBusService::StartupOptions options_)
79  : s(s_)
80  , d(d_)
81  , options(options_)
82  {
83  if (!QDBusConnection::sessionBus().isConnected() || !(bus = QDBusConnection::sessionBus().interface())) {
84  d->errorMessage = QLatin1String("Session bus not found\n"
85  "To circumvent this problem try the following command (with Linux and bash)\n"
86  "export $(dbus-launch)");
87  } else {
88  generateServiceName();
89  }
90  }
91 
92  void run() {
93  if (bus) {
94  registerOnBus();
95  }
96 
97  if (!d->registered && ((options & KDBusService::NoExitOnFailure) == 0)) {
98  qCritical() << d->errorMessage;
99  exit(1);
100  }
101  }
102 
103  private:
104 
105  void generateServiceName()
106  {
107  d->serviceName = d->generateServiceName();
108  objectPath = QLatin1Char('/') + d->serviceName;
109  objectPath.replace(QLatin1Char('.'), QLatin1Char('/'));
110  objectPath.replace(QLatin1Char('-'), QLatin1Char('_')); // see spec change at https://bugs.freedesktop.org/show_bug.cgi?id=95129
111 
112  if (options & KDBusService::Multiple) {
113  const bool inSandbox = QFileInfo::exists(QStringLiteral("/.flatpak-info"));
114  if (inSandbox) {
115  d->serviceName += QStringLiteral(".kdbus-") + QDBusConnection::sessionBus().baseService().replace(QRegularExpression(QStringLiteral("[\\.:]")), QStringLiteral("_"));
116  } else {
118  }
119  }
120  }
121 
122  void registerOnBus()
123  {
124  auto bus = QDBusConnection::sessionBus();
125  bool objectRegistered = false;
126  objectRegistered = bus.registerObject(QStringLiteral("/MainApplication"),
131  if (!objectRegistered) {
132  qWarning() << "Failed to register /MainApplication on DBus";
133  return;
134  }
135 
136  objectRegistered = bus.registerObject(objectPath, s, QDBusConnection::ExportAdaptors);
137  if (!objectRegistered) {
138  qWarning() << "Failed to register" << objectPath << "on DBus";
139  return;
140  }
141 
142  attemptRegistration();
143 
144  if (d->registered) {
146  connect(app, &QCoreApplication::aboutToQuit,
148  }
149  }
150  }
151 
152  void attemptRegistration()
153  {
154  Q_ASSERT(!d->registered);
155 
157 
158  if (options & KDBusService::Unique) {
159  // When a process crashes and gets auto-restarted by KCrash we may
160  // be in this code path "too early". There is a bit of a delay
161  // between the restart and the previous process dropping off of the
162  // bus and thus releasing its registered names. As a result there
163  // is a good chance that if we wait a bit the name will shortly
164  // become registered.
165 
167 
169  this, [this](const QString &service) {
170  if (service != d->serviceName) {
171  return;
172  }
173 
174  d->registered = true;
175  registrationLoop.quit();
176  });
177  }
178 
179  d->registered =
180  (bus->registerService(d->serviceName, queueOption) == QDBusConnectionInterface::ServiceRegistered);
181 
182  if (d->registered) {
183  return;
184  }
185 
186  if (options & KDBusService::Replace) {
187  auto message = QDBusMessage::createMethodCall(d->serviceName,
188  QStringLiteral("/MainApplication"),
189  QStringLiteral("org.qtproject.Qt.QCoreApplication"),
190  QStringLiteral("quit"));
192  waitForRegistration();
193  } else if (options & KDBusService::Unique) {
194  // Already running so it's ok!
195  QVariantMap platform_data;
196  platform_data.insert(QStringLiteral("desktop-startup-id"), QString::fromUtf8(qgetenv("DESKTOP_STARTUP_ID")));
197  if (QCoreApplication::arguments().count() > 1) {
198  OrgKdeKDBusServiceInterface iface(d->serviceName, objectPath, QDBusConnection::sessionBus());
199  iface.setTimeout(5 * 60 * 1000); // Application can take time to answer
200  QDBusReply<int> reply = iface.CommandLine(QCoreApplication::arguments(), QDir::currentPath(), platform_data);
201  if (reply.isValid()) {
202  exit(reply.value());
203  } else {
204  d->errorMessage = reply.error().message();
205  }
206  } else {
207  OrgFreedesktopApplicationInterface iface(d->serviceName, objectPath, QDBusConnection::sessionBus());
208  iface.setTimeout(5 * 60 * 1000); // Application can take time to answer
209  QDBusReply<void> reply = iface.Activate(platform_data);
210  if (reply.isValid()) {
211  exit(0);
212  } else {
213  d->errorMessage = reply.error().message();
214  }
215  }
216 
217  // service did not respond in a valid way....
218  // let's wait to see if our queued registration finishes perhaps.
219  waitForRegistration();
220  }
221 
222  if (!d->registered) { // either multi service or failed to reclaim name
223  d->errorMessage = QLatin1String("Couldn't register name '")
224  + d->serviceName
225  + QLatin1String("' with DBUS - another process owns it already!");
226  }
227  }
228 
229  void waitForRegistration()
230  {
231  QTimer quitTimer;
232  // Wait a bit longer when we know this instance was restarted. There's
233  // a very good chance we'll eventually get the name once the defunct
234  // process closes its sockets.
235  quitTimer.start(qEnvironmentVariableIsSet("KCRASH_AUTO_RESTARTED") ? 8000 : 2000);
236  connect(&quitTimer, &QTimer::timeout, &registrationLoop, &QEventLoop::quit);
237  registrationLoop.exec();
238  }
239 
240  QDBusConnectionInterface *bus = nullptr;
241  KDBusService *s = nullptr;
242  KDBusServicePrivate *d = nullptr;
244  QEventLoop registrationLoop;
245  QString objectPath;
246 };
247 
249  : QObject(parent), d(new KDBusServicePrivate)
250 {
251  new KDBusServiceAdaptor(this);
252  new KDBusServiceExtensionsAdaptor(this);
253 
254  Registration registration(this, d, options);
255  registration.run();
256 }
257 
259 {
260  delete d;
261 }
262 
264 {
265  return d->registered;
266 }
267 
269 {
270  return d->errorMessage;
271 }
272 
274 {
275  d->exitValue = value;
276 }
277 
279 {
280  return d->serviceName;
281 }
282 
284 {
285  QDBusConnectionInterface *bus = nullptr;
286  if (!d->registered || !QDBusConnection::sessionBus().isConnected() || !(bus = QDBusConnection::sessionBus().interface())) {
287  return;
288  }
289  bus->unregisterService(d->serviceName);
290 }
291 
292 void KDBusService::Activate(const QVariantMap &platform_data)
293 {
294  Q_UNUSED(platform_data);
295 #if HAVE_X11
296  if (QX11Info::isPlatformX11()) {
297  QX11Info::setAppTime(QX11Info::getTimestamp());
298  }
299 #endif
300  // TODO QX11Info::setNextStartupId(platform_data.value("desktop-startup-id"))
302  // TODO (via hook) KStartupInfo::appStarted(platform_data.value("desktop-startup-id"))
303  // ^^ same discussion as below
304 }
305 
306 void KDBusService::Open(const QStringList &uris, const QVariantMap &platform_data)
307 {
308  Q_UNUSED(platform_data);
309  // TODO QX11Info::setNextStartupId(platform_data.value("desktop-startup-id"))
311  // TODO (via hook) KStartupInfo::appStarted(platform_data.value("desktop-startup-id"))
312  // ^^ not needed if the app actually opened a new window.
313  // Solution 1: do it all the time anyway (needs API in QX11Info)
314  // Solution 2: pass the id to the app and let it use KStartupInfo::appStarted if reusing a window
315 }
316 
317 void KDBusService::ActivateAction(const QString &action_name, const QVariantList &maybeParameter, const QVariantMap &platform_data)
318 {
319  Q_UNUSED(platform_data);
320  // This is a workaround for D-Bus not supporting null variants.
321  const QVariant param = maybeParameter.count() == 1 ? maybeParameter.first() : QVariant();
322  emit activateActionRequested(action_name, param);
323  // TODO (via hook) KStartupInfo::appStarted(platform_data.value("desktop-startup-id"))
324  // if desktop-startup-id is set, the action is supposed to show a window (since it could be
325  // called when the app is not running)
326 }
327 
328 int KDBusService::CommandLine(const QStringList &arguments, const QString &workingDirectory, const QVariantMap &platform_data)
329 {
330  Q_UNUSED(platform_data);
331  d->exitValue = 0;
332  // The TODOs here only make sense if this method can be called from the GUI.
333  // If it's for pure "usage in the terminal" then no startup notification got started.
334  // But maybe one day the workspace wants to call this for the Exec key of a .desktop file?
335  // TODO QX11Info::setNextStartupId(platform_data.value("desktop-startup-id"))
336  emit activateRequested(arguments, workingDirectory);
337  // TODO (via hook) KStartupInfo::appStarted(platform_data.value("desktop-startup-id"))
338  return d->exitValue;
339 }
340 
341 #include "kdbusservice.moc"
KCOREADDONS_EXPORT void message(KMessage::MessageType messageType, const QString &text, const QString &caption=QString())
void quit()
QString & prepend(QChar ch)
QDBusConnectionInterface * interface() const const
void activateRequested(const QStringList &arguments, const QString &workingDirectory)
Signals that the application is to be activated.
QString message() const const
bool isValid() const const
QDBusConnection sessionBus()
Indicates that if there&#39;s already a unique service running, to be quit and replaced with our own...
Definition: kdbusservice.h:119
QString currentPath()
Indicates that only one instance of this application should ever exist.
Definition: kdbusservice.h:93
void setExitValue(int value)
Sets the exit value to be used for a duplicate instance.
QString baseService() const const
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.
QDBusReply< bool > unregisterService(const QString &serviceName)
void timeout()
QString number(int n, int base)
QString fromUtf8(const char *str, int size)
~KDBusService()
Destroys this object (but does not unregister the application).
QDBusReply::Type value() const const
bool isEmpty() const const
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
qint64 applicationPid()
QCoreApplication * instance()
QList< QUrl > fromStringList(const QStringList &urls, QUrl::ParsingMode mode)
bool exists() const const
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
SkipEmptyParts
Indicates that the application should not exit if it failed to register with D-Bus.
Definition: kdbusservice.h:109
const QDBusError & error()
QString & replace(int position, int n, QChar after)
void unregister()
Unregister from D-Bus.
QString errorMessage() const
Returns the error message from the D-Bus registration if it failed.
void activateActionRequested(const QString &actionName, const QVariant &parameter)
Signals that an application action should be triggered.
bool isConnected() const const
KIOWIDGETS_EXPORT bool run(const QUrl &_url, bool _is_local)
bool isRegistered() const
Returns true if the D-Bus registration succeeded.
QDBusPendingCall asyncCall(const QDBusMessage &message, int timeout) const const
void start(int msec)
void serviceRegistered(const QString &service)
QStringList arguments()
void openRequested(const QList< QUrl > &uris)
Signals that one or more files should be opened in the application.
QDBusMessage createMethodCall(const QString &service, const QString &path, const QString &interface, const QString &method)
KDBusService takes care of registering the current process with D-Bus.
Definition: kdbusservice.h:78
Indicates that multiple instances of the application may exist.
Definition: kdbusservice.h:99
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Tue Aug 4 2020 22:44:26 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.