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