File indexing completed on 2024-04-14 03:51:47

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