File indexing completed on 2024-04-28 11:35:28
0001 /* 0002 This file is part of libkdbusaddons 0003 0004 SPDX-FileCopyrightText: 2011 David Faure <faure@kde.org> 0005 SPDX-FileCopyrightText: 2011 Kevin Ottens <ervin@kde.org> 0006 SPDX-FileCopyrightText: 2019 Harald Sitter <sitter@kde.org> 0007 0008 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0009 */ 0010 0011 #include "kdbusservice.h" 0012 0013 #include <QCoreApplication> 0014 #include <QDebug> 0015 0016 #include <QDBusConnection> 0017 #include <QDBusConnectionInterface> 0018 #include <QDBusReply> 0019 0020 #include "FreeDesktopApplpicationIface.h" 0021 #include "KDBusServiceIface.h" 0022 0023 #include "config-kdbusaddons.h" 0024 0025 #if HAVE_X11 0026 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 0027 #include <private/qtx11extras_p.h> 0028 #else 0029 #include <QX11Info> 0030 #endif 0031 #endif 0032 0033 #include "kdbusaddons_debug.h" 0034 #include "kdbusservice_adaptor.h" 0035 #include "kdbusserviceextensions_adaptor.h" 0036 0037 class KDBusServicePrivate 0038 { 0039 public: 0040 KDBusServicePrivate() 0041 : registered(false) 0042 , exitValue(0) 0043 { 0044 } 0045 0046 QString generateServiceName() 0047 { 0048 const QCoreApplication *app = QCoreApplication::instance(); 0049 const QString domain = app->organizationDomain(); 0050 const QStringList parts = domain.split(QLatin1Char('.'), Qt::SkipEmptyParts); 0051 0052 QString reversedDomain; 0053 if (parts.isEmpty()) { 0054 reversedDomain = QStringLiteral("local."); 0055 } else { 0056 for (const QString &part : parts) { 0057 reversedDomain.prepend(QLatin1Char('.')); 0058 reversedDomain.prepend(part); 0059 } 0060 } 0061 0062 return reversedDomain + app->applicationName(); 0063 } 0064 0065 static void handlePlatformData(const QVariantMap &platformData) 0066 { 0067 #if HAVE_X11 0068 if (QX11Info::isPlatformX11()) { 0069 QByteArray desktopStartupId = platformData.value(QStringLiteral("desktop-startup-id")).toByteArray(); 0070 if (!desktopStartupId.isEmpty()) { 0071 QX11Info::setNextStartupId(desktopStartupId); 0072 } 0073 } 0074 #endif 0075 0076 const auto xdgActivationToken = platformData.value(QLatin1String("activation-token")).toByteArray(); 0077 if (!xdgActivationToken.isEmpty()) { 0078 qputenv("XDG_ACTIVATION_TOKEN", xdgActivationToken); 0079 } 0080 } 0081 0082 bool registered; 0083 QString serviceName; 0084 QString errorMessage; 0085 int exitValue; 0086 }; 0087 0088 // Wraps a serviceName registration. 0089 class Registration : public QObject 0090 { 0091 Q_OBJECT 0092 public: 0093 enum class Register { 0094 RegisterWitoutQueue, 0095 RegisterWithQueue, 0096 }; 0097 0098 Registration(KDBusService *s_, KDBusServicePrivate *d_, KDBusService::StartupOptions options_) 0099 : s(s_) 0100 , d(d_) 0101 , options(options_) 0102 { 0103 if (!QDBusConnection::sessionBus().isConnected() || !(bus = QDBusConnection::sessionBus().interface())) { 0104 d->errorMessage = QLatin1String( 0105 "DBus session bus not found. To circumvent this problem try the following command (with bash):\n" 0106 " export $(dbus-launch)"); 0107 } else { 0108 generateServiceName(); 0109 } 0110 } 0111 0112 void run() 0113 { 0114 if (bus) { 0115 registerOnBus(); 0116 } 0117 0118 if (!d->registered && ((options & KDBusService::NoExitOnFailure) == 0)) { 0119 qCCritical(KDBUSADDONS_LOG) << qPrintable(d->errorMessage); 0120 exit(1); 0121 } 0122 } 0123 0124 private: 0125 void generateServiceName() 0126 { 0127 d->serviceName = d->generateServiceName(); 0128 objectPath = QLatin1Char('/') + d->serviceName; 0129 objectPath.replace(QLatin1Char('.'), QLatin1Char('/')); 0130 objectPath.replace(QLatin1Char('-'), QLatin1Char('_')); // see spec change at https://bugs.freedesktop.org/show_bug.cgi?id=95129 0131 0132 if (options & KDBusService::Multiple) { 0133 const bool inSandbox = QFileInfo::exists(QStringLiteral("/.flatpak-info")); 0134 if (inSandbox) { 0135 d->serviceName += QStringLiteral(".kdbus-") 0136 + QDBusConnection::sessionBus().baseService().replace(QRegularExpression(QStringLiteral("[\\.:]")), QStringLiteral("_")); 0137 } else { 0138 d->serviceName += QLatin1Char('-') + QString::number(QCoreApplication::applicationPid()); 0139 } 0140 } 0141 } 0142 0143 void registerOnBus() 0144 { 0145 auto bus = QDBusConnection::sessionBus(); 0146 bool objectRegistered = false; 0147 objectRegistered = bus.registerObject(QStringLiteral("/MainApplication"), 0148 QCoreApplication::instance(), 0149 QDBusConnection::ExportAllSlots // 0150 | QDBusConnection::ExportScriptableProperties // 0151 | QDBusConnection::ExportAdaptors); 0152 if (!objectRegistered) { 0153 qCWarning(KDBUSADDONS_LOG) << "Failed to register /MainApplication on DBus"; 0154 return; 0155 } 0156 0157 objectRegistered = bus.registerObject(objectPath, s, QDBusConnection::ExportAdaptors); 0158 if (!objectRegistered) { 0159 qCWarning(KDBUSADDONS_LOG) << "Failed to register" << objectPath << "on DBus"; 0160 return; 0161 } 0162 0163 attemptRegistration(); 0164 0165 if (d->registered) { 0166 if (QCoreApplication *app = QCoreApplication::instance()) { 0167 connect(app, &QCoreApplication::aboutToQuit, s, &KDBusService::unregister); 0168 } 0169 } 0170 } 0171 0172 void attemptRegistration() 0173 { 0174 Q_ASSERT(!d->registered); 0175 0176 auto queueOption = QDBusConnectionInterface::DontQueueService; 0177 0178 if (options & KDBusService::Unique) { 0179 // When a process crashes and gets auto-restarted by KCrash we may 0180 // be in this code path "too early". There is a bit of a delay 0181 // between the restart and the previous process dropping off of the 0182 // bus and thus releasing its registered names. As a result there 0183 // is a good chance that if we wait a bit the name will shortly 0184 // become registered. 0185 0186 queueOption = QDBusConnectionInterface::QueueService; 0187 0188 connect(bus, &QDBusConnectionInterface::serviceRegistered, this, [this](const QString &service) { 0189 if (service != d->serviceName) { 0190 return; 0191 } 0192 0193 d->registered = true; 0194 registrationLoop.quit(); 0195 }); 0196 } 0197 0198 d->registered = (bus->registerService(d->serviceName, queueOption) == QDBusConnectionInterface::ServiceRegistered); 0199 0200 if (d->registered) { 0201 return; 0202 } 0203 0204 if (options & KDBusService::Replace) { 0205 auto message = QDBusMessage::createMethodCall(d->serviceName, 0206 QStringLiteral("/MainApplication"), 0207 QStringLiteral("org.qtproject.Qt.QCoreApplication"), 0208 QStringLiteral("quit")); 0209 QDBusConnection::sessionBus().asyncCall(message); 0210 waitForRegistration(); 0211 } else if (options & KDBusService::Unique) { 0212 // Already running so it's ok! 0213 QVariantMap platform_data; 0214 #if HAVE_X11 0215 if (QX11Info::isPlatformX11()) { 0216 QString startupId = QString::fromUtf8(qgetenv("DESKTOP_STARTUP_ID")); 0217 if (startupId.isEmpty()) { 0218 startupId = QString::fromUtf8(QX11Info::nextStartupId()); 0219 } 0220 if (!startupId.isEmpty()) { 0221 platform_data.insert(QStringLiteral("desktop-startup-id"), startupId); 0222 } 0223 } 0224 #endif 0225 0226 if (qEnvironmentVariableIsSet("XDG_ACTIVATION_TOKEN")) { 0227 platform_data.insert(QStringLiteral("activation-token"), qgetenv("XDG_ACTIVATION_TOKEN")); 0228 } 0229 0230 if (QCoreApplication::arguments().count() > 1) { 0231 OrgKdeKDBusServiceInterface iface(d->serviceName, objectPath, QDBusConnection::sessionBus()); 0232 iface.setTimeout(5 * 60 * 1000); // Application can take time to answer 0233 QDBusReply<int> reply = iface.CommandLine(QCoreApplication::arguments(), QDir::currentPath(), platform_data); 0234 if (reply.isValid()) { 0235 exit(reply.value()); 0236 } else { 0237 d->errorMessage = reply.error().message(); 0238 } 0239 } else { 0240 OrgFreedesktopApplicationInterface iface(d->serviceName, objectPath, QDBusConnection::sessionBus()); 0241 iface.setTimeout(5 * 60 * 1000); // Application can take time to answer 0242 QDBusReply<void> reply = iface.Activate(platform_data); 0243 if (reply.isValid()) { 0244 exit(0); 0245 } else { 0246 d->errorMessage = reply.error().message(); 0247 } 0248 } 0249 0250 // service did not respond in a valid way.... 0251 // let's wait to see if our queued registration finishes perhaps. 0252 waitForRegistration(); 0253 } 0254 0255 if (!d->registered) { // either multi service or failed to reclaim name 0256 d->errorMessage = QLatin1String("Couldn't register name '") + d->serviceName + QLatin1String("' with DBUS - another process owns it already!"); 0257 } 0258 } 0259 0260 void waitForRegistration() 0261 { 0262 QTimer quitTimer; 0263 // Wait a bit longer when we know this instance was restarted. There's 0264 // a very good chance we'll eventually get the name once the defunct 0265 // process closes its sockets. 0266 quitTimer.start(qEnvironmentVariableIsSet("KCRASH_AUTO_RESTARTED") ? 8000 : 2000); 0267 connect(&quitTimer, &QTimer::timeout, ®istrationLoop, &QEventLoop::quit); 0268 registrationLoop.exec(); 0269 } 0270 0271 QDBusConnectionInterface *bus = nullptr; 0272 KDBusService *s = nullptr; 0273 KDBusServicePrivate *d = nullptr; 0274 KDBusService::StartupOptions options; 0275 QEventLoop registrationLoop; 0276 QString objectPath; 0277 }; 0278 0279 KDBusService::KDBusService(StartupOptions options, QObject *parent) 0280 : QObject(parent) 0281 , d(new KDBusServicePrivate) 0282 { 0283 new KDBusServiceAdaptor(this); 0284 new KDBusServiceExtensionsAdaptor(this); 0285 0286 Registration registration(this, d.get(), options); 0287 registration.run(); 0288 } 0289 0290 KDBusService::~KDBusService() = default; 0291 0292 bool KDBusService::isRegistered() const 0293 { 0294 return d->registered; 0295 } 0296 0297 QString KDBusService::errorMessage() const 0298 { 0299 return d->errorMessage; 0300 } 0301 0302 void KDBusService::setExitValue(int value) 0303 { 0304 d->exitValue = value; 0305 } 0306 0307 QString KDBusService::serviceName() const 0308 { 0309 return d->serviceName; 0310 } 0311 0312 void KDBusService::unregister() 0313 { 0314 QDBusConnectionInterface *bus = nullptr; 0315 if (!d->registered || !QDBusConnection::sessionBus().isConnected() || !(bus = QDBusConnection::sessionBus().interface())) { 0316 return; 0317 } 0318 bus->unregisterService(d->serviceName); 0319 } 0320 0321 void KDBusService::Activate(const QVariantMap &platform_data) 0322 { 0323 d->handlePlatformData(platform_data); 0324 Q_EMIT activateRequested(QStringList(), QString()); 0325 qunsetenv("XDG_ACTIVATION_TOKEN"); 0326 } 0327 0328 void KDBusService::Open(const QStringList &uris, const QVariantMap &platform_data) 0329 { 0330 d->handlePlatformData(platform_data); 0331 Q_EMIT openRequested(QUrl::fromStringList(uris)); 0332 qunsetenv("XDG_ACTIVATION_TOKEN"); 0333 } 0334 0335 void KDBusService::ActivateAction(const QString &action_name, const QVariantList &maybeParameter, const QVariantMap &platform_data) 0336 { 0337 d->handlePlatformData(platform_data); 0338 0339 // This is a workaround for D-Bus not supporting null variants. 0340 const QVariant param = maybeParameter.count() == 1 ? maybeParameter.first() : QVariant(); 0341 0342 Q_EMIT activateActionRequested(action_name, param); 0343 qunsetenv("XDG_ACTIVATION_TOKEN"); 0344 } 0345 0346 int KDBusService::CommandLine(const QStringList &arguments, const QString &workingDirectory, const QVariantMap &platform_data) 0347 { 0348 d->exitValue = 0; 0349 d->handlePlatformData(platform_data); 0350 // The TODOs here only make sense if this method can be called from the GUI. 0351 // If it's for pure "usage in the terminal" then no startup notification got started. 0352 // But maybe one day the workspace wants to call this for the Exec key of a .desktop file? 0353 Q_EMIT activateRequested(arguments, workingDirectory); 0354 qunsetenv("XDG_ACTIVATION_TOKEN"); 0355 return d->exitValue; 0356 } 0357 0358 #include "kdbusservice.moc" 0359 #include "moc_kdbusservice.cpp"