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"