File indexing completed on 2024-04-28 15:20:31

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, &registrationLoop, &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"