File indexing completed on 2024-04-28 09:26:07

0001 /*
0002  * SPDX-FileCopyrightText: 2014 Daniel Vratil <dvratil@redhat.com>
0003  * SPDX-FileCopyrightText: 2015 Sebastian Kügler <sebas@kde.org>
0004  *
0005  * SPDX-License-Identifier: LGPL-2.1-or-later
0006  *
0007  */
0008 
0009 #include "backendmanager_p.h"
0010 
0011 #include "abstractbackend.h"
0012 #include "backendinterface.h"
0013 #include "configmonitor.h"
0014 #include "configserializer_p.h"
0015 #include "getconfigoperation.h"
0016 #include "kscreen_debug.h"
0017 #include "log.h"
0018 
0019 #include <QDBusConnection>
0020 #include <QDBusConnectionInterface>
0021 #include <QDBusPendingCall>
0022 #include <QDBusPendingCallWatcher>
0023 #include <QDBusPendingReply>
0024 #include <QGuiApplication>
0025 #include <QStandardPaths>
0026 #include <QThread>
0027 #include <QtGui/private/qtx11extras_p.h>
0028 
0029 #include <memory>
0030 
0031 using namespace KScreen;
0032 
0033 const int BackendManager::sMaxCrashCount = 4;
0034 
0035 BackendManager *BackendManager::sInstance = nullptr;
0036 
0037 BackendManager *BackendManager::instance()
0038 {
0039     if (!sInstance) {
0040         sInstance = new BackendManager();
0041     }
0042 
0043     return sInstance;
0044 }
0045 
0046 BackendManager::BackendManager()
0047     : mInterface(nullptr)
0048     , mCrashCount(0)
0049     , mShuttingDown(false)
0050     , mRequestsCounter(0)
0051     , mLoader(nullptr)
0052     , mInProcessBackend(nullptr)
0053     , mMethod(OutOfProcess)
0054 {
0055     Log::instance();
0056     // Decide whether to run in, or out-of-process
0057 
0058     // if KSCREEN_BACKEND_INPROCESS is set explicitly, we respect that
0059     const auto _inprocess = qgetenv("KSCREEN_BACKEND_INPROCESS");
0060     if (!_inprocess.isEmpty()) {
0061         const QByteArrayList falses({QByteArray("0"), QByteArray("false")});
0062         if (!falses.contains(_inprocess.toLower())) {
0063             mMethod = InProcess;
0064         } else {
0065             mMethod = OutOfProcess;
0066         }
0067     } else {
0068         // For XRandR backends, use out of process
0069         if (preferredBackend().fileName().startsWith(QLatin1String("KSC_XRandR"))) {
0070             mMethod = OutOfProcess;
0071         } else {
0072             mMethod = InProcess;
0073         }
0074     }
0075     initMethod();
0076 }
0077 
0078 void BackendManager::initMethod()
0079 {
0080     if (mMethod == OutOfProcess) {
0081         qRegisterMetaType<org::kde::kscreen::Backend *>("OrgKdeKscreenBackendInterface");
0082 
0083         mServiceWatcher.setConnection(QDBusConnection::sessionBus());
0084         connect(&mServiceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &BackendManager::backendServiceUnregistered);
0085 
0086         mResetCrashCountTimer.setSingleShot(true);
0087         mResetCrashCountTimer.setInterval(60000);
0088         connect(&mResetCrashCountTimer, &QTimer::timeout, this, [this]() {
0089             mCrashCount = 0;
0090         });
0091     }
0092 }
0093 
0094 void BackendManager::setMethod(BackendManager::Method m)
0095 {
0096     if (mMethod == m) {
0097         return;
0098     }
0099     shutdownBackend();
0100     mMethod = m;
0101     initMethod();
0102 }
0103 
0104 BackendManager::Method BackendManager::method() const
0105 {
0106     return mMethod;
0107 }
0108 
0109 BackendManager::~BackendManager()
0110 {
0111     if (mMethod == InProcess) {
0112         shutdownBackend();
0113     }
0114 }
0115 
0116 QFileInfo BackendManager::preferredBackend(const QString &backend)
0117 {
0118     /** this is the logic to pick a backend, in order of priority
0119      *
0120      * - backend argument is used if not empty
0121      * - env var KSCREEN_BACKEND is considered
0122      * - if platform is X11, the XRandR backend is picked
0123      * - if platform is wayland, KWayland backend is picked
0124      * - if neither is the case, QScreen backend is picked
0125      * - the QScreen backend is also used as fallback
0126      *
0127      */
0128     QString backendFilter;
0129     const auto env_kscreen_backend = QString::fromUtf8(qgetenv("KSCREEN_BACKEND"));
0130     if (!backend.isEmpty()) {
0131         backendFilter = backend;
0132     } else if (!env_kscreen_backend.isEmpty()) {
0133         backendFilter = env_kscreen_backend;
0134     } else {
0135         if (QX11Info::isPlatformX11()) {
0136             backendFilter = QStringLiteral("XRandR");
0137         } else if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) {
0138             backendFilter = QStringLiteral("KWayland");
0139         } else {
0140             backendFilter = QStringLiteral("QScreen");
0141         }
0142     }
0143     QFileInfo fallback;
0144     const auto backends = listBackends();
0145     for (const QFileInfo &f : backends) {
0146         // Here's the part where we do the match case-insensitive
0147         if (f.baseName().toLower() == QStringLiteral("ksc_%1").arg(backendFilter.toLower())) {
0148             return f;
0149         }
0150         if (f.baseName() == QLatin1String("KSC_QScreen")) {
0151             fallback = f;
0152         }
0153     }
0154     //     qCWarning(KSCREEN) << "No preferred backend found. KSCREEN_BACKEND is set to " << env_kscreen_backend;
0155     //     qCWarning(KSCREEN) << "falling back to " << fallback.fileName();
0156     return fallback;
0157 }
0158 
0159 QFileInfoList BackendManager::listBackends()
0160 {
0161     // Compile a list of installed backends first
0162     const QString backendFilter = QStringLiteral("KSC_*");
0163     const QStringList paths = QCoreApplication::libraryPaths();
0164     QFileInfoList finfos;
0165     for (const QString &path : paths) {
0166         const QDir dir(path + QStringLiteral("/kf6/kscreen/"), backendFilter, QDir::SortFlags(QDir::QDir::Name), QDir::NoDotAndDotDot | QDir::Files);
0167         finfos.append(dir.entryInfoList());
0168     }
0169     return finfos;
0170 }
0171 
0172 void BackendManager::setBackendArgs(const QVariantMap &arguments)
0173 {
0174     if (mBackendArguments != arguments) {
0175         mBackendArguments = arguments;
0176     }
0177 }
0178 
0179 QVariantMap BackendManager::getBackendArgs()
0180 {
0181     return mBackendArguments;
0182 }
0183 
0184 KScreen::AbstractBackend *BackendManager::loadBackendPlugin(QPluginLoader *loader, const QString &name, const QVariantMap &arguments)
0185 {
0186     const auto finfo = preferredBackend(name);
0187     loader->setFileName(finfo.filePath());
0188     QObject *instance = loader->instance();
0189     if (!instance) {
0190         qCDebug(KSCREEN) << loader->errorString();
0191         return nullptr;
0192     }
0193 
0194     auto backend = qobject_cast<KScreen::AbstractBackend *>(instance);
0195     if (backend) {
0196         backend->init(arguments);
0197         if (!backend->isValid()) {
0198             qCDebug(KSCREEN) << "Skipping" << backend->name() << "backend";
0199             delete backend;
0200             return nullptr;
0201         }
0202         // qCDebug(KSCREEN) << "Loaded" << backend->name() << "backend";
0203         return backend;
0204     } else {
0205         qCDebug(KSCREEN) << finfo.fileName() << "does not provide valid KScreen backend";
0206     }
0207 
0208     return nullptr;
0209 }
0210 
0211 KScreen::AbstractBackend *BackendManager::loadBackendInProcess(const QString &name)
0212 {
0213     Q_ASSERT(mMethod == InProcess);
0214     if (mMethod == OutOfProcess) {
0215         qCWarning(KSCREEN) << "You are trying to load a backend in process, while the BackendManager is set to use OutOfProcess communication. Use "
0216                               "loadBackendPlugin() instead.";
0217         return nullptr;
0218     }
0219     if (mInProcessBackend != nullptr && (name.isEmpty() || mInProcessBackend->name() == name)) {
0220         return mInProcessBackend;
0221     } else if (mInProcessBackend != nullptr && mInProcessBackend->name() != name) {
0222         shutdownBackend();
0223     }
0224 
0225     if (mLoader == nullptr) {
0226         mLoader = new QPluginLoader(this);
0227     }
0228 
0229     auto backend = BackendManager::loadBackendPlugin(mLoader, name, mBackendArguments);
0230     if (!backend) {
0231         return nullptr;
0232     }
0233     // qCDebug(KSCREEN) << "Connecting ConfigMonitor to backend.";
0234     ConfigMonitor::instance()->connectInProcessBackend(backend);
0235     mInProcessBackend = backend;
0236     setConfig(backend->config());
0237     return backend;
0238 }
0239 
0240 void BackendManager::requestBackend()
0241 {
0242     Q_ASSERT(mMethod == OutOfProcess);
0243     if (mInterface && mInterface->isValid()) {
0244         ++mRequestsCounter;
0245         QMetaObject::invokeMethod(this, "emitBackendReady", Qt::QueuedConnection);
0246         return;
0247     }
0248 
0249     // Another request already pending
0250     if (mRequestsCounter > 0) {
0251         return;
0252     }
0253     ++mRequestsCounter;
0254 
0255     startBackend(QString::fromLatin1(qgetenv("KSCREEN_BACKEND")), mBackendArguments);
0256 }
0257 
0258 void BackendManager::emitBackendReady()
0259 {
0260     Q_ASSERT(mMethod == OutOfProcess);
0261     Q_EMIT backendReady(mInterface);
0262     --mRequestsCounter;
0263     if (mShutdownLoop.isRunning()) {
0264         mShutdownLoop.quit();
0265     }
0266 }
0267 
0268 void BackendManager::startBackend(const QString &backend, const QVariantMap &arguments)
0269 {
0270     // This will autostart the launcher if it's not running already, calling
0271     // requestBackend(backend) will:
0272     //   a) if the launcher is started it will force it to load the correct backend,
0273     //   b) if the launcher is already running it will make sure it's running with
0274     //      the same backend as the one we requested and send an error otherwise
0275     QDBusConnection conn = QDBusConnection::sessionBus();
0276     QDBusMessage call = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KScreen"),
0277                                                        QStringLiteral("/"),
0278                                                        QStringLiteral("org.kde.KScreen"),
0279                                                        QStringLiteral("requestBackend"));
0280     call.setArguments({backend, arguments});
0281     QDBusPendingCall pending = conn.asyncCall(call);
0282     QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending);
0283     connect(watcher, &QDBusPendingCallWatcher::finished, this, &BackendManager::onBackendRequestDone);
0284 }
0285 
0286 void BackendManager::onBackendRequestDone(QDBusPendingCallWatcher *watcher)
0287 {
0288     Q_ASSERT(mMethod == OutOfProcess);
0289     watcher->deleteLater();
0290     QDBusPendingReply<bool> reply = *watcher;
0291     // Most probably we requested an explicit backend that is different than the
0292     // one already loaded in the launcher
0293     if (reply.isError()) {
0294         qCWarning(KSCREEN) << "Failed to request backend:" << reply.error().name() << ":" << reply.error().message();
0295         invalidateInterface();
0296         emitBackendReady();
0297         return;
0298     }
0299 
0300     // Most probably request and explicit backend which is not available or failed
0301     // to initialize, or the launcher did not find any suitable backend for the
0302     // current platform.
0303     if (!reply.value()) {
0304         qCWarning(KSCREEN) << "Failed to request backend: unknown error";
0305         invalidateInterface();
0306         emitBackendReady();
0307         return;
0308     }
0309 
0310     // The launcher has successfully loaded the backend we wanted and registered
0311     // it to DBus (hopefuly), let's try to get an interface for the backend.
0312     if (mInterface) {
0313         invalidateInterface();
0314     }
0315     mInterface = new org::kde::kscreen::Backend(QStringLiteral("org.kde.KScreen"), QStringLiteral("/backend"), QDBusConnection::sessionBus());
0316     if (!mInterface->isValid()) {
0317         qCWarning(KSCREEN) << "Backend successfully requested, but we failed to obtain a valid DBus interface for it";
0318         invalidateInterface();
0319         emitBackendReady();
0320         return;
0321     }
0322 
0323     // The backend is GO, so let's watch for it's possible disappearance, so we
0324     // can invalidate the interface
0325     mServiceWatcher.addWatchedService(mBackendService);
0326 
0327     // Immediatelly request config
0328     connect(new GetConfigOperation(GetConfigOperation::NoEDID), &GetConfigOperation::finished, [&](ConfigOperation *op) {
0329         mConfig = qobject_cast<GetConfigOperation *>(op)->config();
0330         emitBackendReady();
0331     });
0332     // And listen for its change.
0333     connect(mInterface, &org::kde::kscreen::Backend::configChanged, [&](const QVariantMap &newConfig) {
0334         mConfig = KScreen::ConfigSerializer::deserializeConfig(newConfig);
0335     });
0336 }
0337 
0338 void BackendManager::backendServiceUnregistered(const QString &serviceName)
0339 {
0340     Q_ASSERT(mMethod == OutOfProcess);
0341     mServiceWatcher.removeWatchedService(serviceName);
0342 
0343     invalidateInterface();
0344     requestBackend();
0345 }
0346 
0347 void BackendManager::invalidateInterface()
0348 {
0349     Q_ASSERT(mMethod == OutOfProcess);
0350     delete mInterface;
0351     mInterface = nullptr;
0352     mBackendService.clear();
0353 }
0354 
0355 ConfigPtr BackendManager::config() const
0356 {
0357     return mConfig;
0358 }
0359 
0360 void BackendManager::setConfig(ConfigPtr c)
0361 {
0362     // qCDebug(KSCREEN) << "BackendManager::setConfig, outputs:" << c->outputs().count();
0363     mConfig = c;
0364 }
0365 
0366 void BackendManager::shutdownBackend()
0367 {
0368     if (mMethod == InProcess) {
0369         delete mLoader;
0370         mLoader = nullptr;
0371         delete mInProcessBackend;
0372         mInProcessBackend = nullptr;
0373     } else {
0374         if (mBackendService.isEmpty() && !mInterface) {
0375             return;
0376         }
0377 
0378         // If there are some currently pending requests, then wait for them to
0379         // finish before quitting
0380         while (mRequestsCounter > 0) {
0381             mShutdownLoop.exec();
0382         }
0383 
0384         mServiceWatcher.removeWatchedService(mBackendService);
0385         mShuttingDown = true;
0386 
0387         QDBusMessage call =
0388             QDBusMessage::createMethodCall(QStringLiteral("org.kde.KScreen"), QStringLiteral("/"), QStringLiteral("org.kde.KScreen"), QStringLiteral("quit"));
0389         // Call synchronously
0390         QDBusConnection::sessionBus().call(call);
0391         invalidateInterface();
0392 
0393         while (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.KScreen"))) {
0394             QThread::msleep(100);
0395         }
0396     }
0397 }
0398 
0399 #include "moc_backendmanager_p.cpp"