File indexing completed on 2024-03-24 17:07:42

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