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 }