GCC Code Coverage Report


Directory: ./
File: lib/backendmanager.cpp
Date: 2023-04-20 22:59:23
Exec Total Coverage
Lines: 130 252 51.6%
Branches: 77 172 44.8%

Line Branch Exec Source
1 /*
2 * Copyright (C) 2014 Daniel Vratil <dvratil@redhat.com>
3 * Copyright 2015 Sebastian K├╝gler <sebas@kde.org>
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Lesser General Public
7 * License as published by the Free Software Foundation; either
8 * version 2.1 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public
16 * License along with this library; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 *
19 */
20 #include "backendmanager_p.h"
21
22 #include "backend.h"
23 #include "backendinterface.h"
24 #include "config.h"
25 #include "configmonitor.h"
26 #include "configserializer_p.h"
27 #include "disman_debug.h"
28 #include "getconfigoperation.h"
29 #include "log.h"
30
31 #include <QDBusConnection>
32 #include <QDBusConnectionInterface>
33 #include <QDBusPendingCall>
34 #include <QDBusPendingCallWatcher>
35 #include <QDBusPendingReply>
36 #include <QGuiApplication>
37 #include <QStandardPaths>
38 #include <QThread>
39
40 #include <memory>
41
42 using namespace Disman;
43
44
2/4
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
2 Q_DECLARE_METATYPE(org::kwinft::disman::backend*)
45
46 const int BackendManager::sMaxCrashCount = 4;
47
48 BackendManager* BackendManager::sInstance = nullptr;
49
50 431 BackendManager* BackendManager::instance()
51 {
52
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 421 times.
431 if (!sInstance) {
53 10 sInstance = new BackendManager();
54 }
55
56 431 return sInstance;
57 }
58
59 10 BackendManager::BackendManager()
60 10 : mInterface(nullptr)
61 10 , mCrashCount(0)
62 10 , mShuttingDown(false)
63 10 , mRequestsCounter(0)
64 10 , mLoader(nullptr)
65 10 , mMethod(OutOfProcess)
66 {
67 10 Log::instance();
68 // Decide whether to run in, or out-of-process
69
70 // if DISMAN_IN_PROCESS is set explicitly, we respect that
71 10 auto const in_process = qgetenv("DISMAN_IN_PROCESS");
72
2/2
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 1 times.
10 if (!in_process.isEmpty()) {
73
74
2/2
✓ Branch 3 taken 18 times.
✓ Branch 4 taken 9 times.
27 const QByteArrayList falses({QByteArray("0"), QByteArray("false")});
75
2/2
✓ Branch 3 taken 8 times.
✓ Branch 4 taken 1 times.
9 if (!falses.contains(in_process.toLower())) {
76 8 mMethod = InProcess;
77 } else {
78 1 mMethod = OutOfProcess;
79 }
80 9 } else {
81 1 mMethod = OutOfProcess;
82 }
83
84 // TODO(romangg): fallback to in-process when dbus not available?
85
86 10 init_method();
87 10 }
88
89 11 void BackendManager::init_method()
90 {
91
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 9 times.
11 if (mMethod == OutOfProcess) {
92 2 qRegisterMetaType<org::kwinft::disman::backend*>("OrgKwinftDismanBackendInterface");
93
94 2 mServiceWatcher.setConnection(QDBusConnection::sessionBus());
95 2 connect(&mServiceWatcher,
96 &QDBusServiceWatcher::serviceUnregistered,
97 this,
98 &BackendManager::backend_service_unregistered);
99
100 2 mResetCrashCountTimer.setSingleShot(true);
101 2 mResetCrashCountTimer.setInterval(60000);
102 2 connect(&mResetCrashCountTimer, &QTimer::timeout, this, [=]() { mCrashCount = 0; });
103 }
104 11 }
105
106 9 void BackendManager::set_method(BackendManager::Method m)
107 {
108
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1 times.
9 if (mMethod == m) {
109 8 return;
110 }
111 1 shutdown_backend();
112 1 mMethod = m;
113 1 init_method();
114 }
115
116 232 BackendManager::Method BackendManager::method() const
117 {
118 232 return mMethod;
119 }
120
121 BackendManager::~BackendManager()
122 {
123 if (mMethod == InProcess) {
124 shutdown_backend();
125 }
126 }
127
128 65 QFileInfo BackendManager::preferred_backend(std::string const& pre_select)
129 {
130 /**
131 * Logic to pick a backend. It does this in order of priority:
132 * - pre_select argument is used if not empty,
133 * - env var DISMAN_BACKEND is considered,
134 * - if platform is X11, the XRandR backend is picked,
135 * - if platform is wayland, Wayland backend is picked,
136 * - if neither is the case, QScreen backend is picked,
137 * - the QScreen backend is also used as fallback.
138 */
139
140 65 auto env_select = qgetenv("DISMAN_BACKEND").toStdString();
141
142 65 auto get_selection = [&]() -> std::string {
143
2/2
✓ Branch 1 taken 59 times.
✓ Branch 2 taken 6 times.
65 if (pre_select.size()) {
144 59 return pre_select;
145 }
146
2/2
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 1 times.
6 if (env_select.size()) {
147 5 return env_select;
148 }
149
150 // If XDG_SESSION_TYPE is defined and indicates a certain windowing system we prefer
151 // that variable, since it likely reflects correctly the current session setup.
152 1 auto const session_type = qgetenv("XDG_SESSION_TYPE");
153
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
1 if (session_type == "wayland") {
154 return "wayland";
155 }
156
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
1 if (session_type == "x11") {
157 return "randr";
158 }
159
160
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
1 if (auto display = qgetenv("WAYLAND_DISPLAY"); !display.isEmpty()) {
161 auto dsp_str = QString::fromLatin1(display);
162
163 auto socket_exists = [&dsp_str] {
164 if (QDir::isAbsolutePath(dsp_str)) {
165 return QFile(dsp_str).exists();
166 }
167 auto const locations
168 = QStandardPaths::standardLocations(QStandardPaths::RuntimeLocation);
169 for (auto const& dir : qAsConst(locations)) {
170 if (QFileInfo(QDir(dir), dsp_str).exists()) {
171 return true;
172 }
173 }
174 return false;
175 };
176
177 if (socket_exists()) {
178 return "wayland";
179 }
180
1/4
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 }
181
1/2
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
1 if (!qEnvironmentVariableIsEmpty("DISPLAY")) {
182 1 return "randr";
183 }
184 return "qscreen";
185 1 };
186 65 auto const select = get_selection();
187
2/2
✓ Branch 9 taken 65 times.
✓ Branch 10 taken 65 times.
130 qCDebug(DISMAN) << "Selection for preferred backend:" << select.c_str();
188
189 65 QFileInfo fallback;
190 65 auto const& backends = list_backends();
191
2/2
✓ Branch 6 taken 146 times.
✓ Branch 7 taken 2 times.
148 for (auto const& file_info : qAsConst(backends)) {
192
2/2
✓ Branch 5 taken 63 times.
✓ Branch 6 taken 83 times.
146 if (file_info.baseName().toStdString() == select) {
193 63 return file_info;
194 }
195
2/2
✓ Branch 4 taken 26 times.
✓ Branch 5 taken 57 times.
83 if (file_info.baseName() == QLatin1String("qscreen")) {
196 26 fallback = file_info;
197 }
198 }
199
2/2
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 2 times.
4 qCWarning(DISMAN) << "No preferred backend found. Env var DISMAN_BACKEND was"
200
2/4
✓ Branch 5 taken 2 times.
✗ Branch 6 not taken.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
6 << (env_select.size() ? (std::string("set to:") + env_select).c_str()
201
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
4 : "not set.")
202
1/2
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2 << "Falling back to:" << fallback.fileName();
203 2 return fallback;
204 65 }
205
206 66 QFileInfoList BackendManager::list_backends()
207 {
208 // Compile a list of installed backends first
209 66 const QStringList paths = QCoreApplication::libraryPaths();
210
211 66 QFileInfoList finfos;
212
2/2
✓ Branch 5 taken 132 times.
✓ Branch 6 taken 66 times.
198 for (const QString& path : paths) {
213 264 const QDir dir(path + QLatin1String("/disman/"),
214 264 QString(),
215 QDir::SortFlags(QDir::QDir::Name),
216 264 QDir::NoDotAndDotDot | QDir::Files);
217 132 finfos.append(dir.entryInfoList());
218 132 }
219 66 return finfos;
220 66 }
221
222 58 Disman::Backend* BackendManager::load_backend_plugin(QPluginLoader* loader,
223 const QString& name,
224 const QVariantMap& arguments)
225 {
226 58 const auto finfo = preferred_backend(name.toStdString());
227 58 loader->setFileName(finfo.filePath());
228
2/2
✓ Branch 10 taken 58 times.
✓ Branch 11 taken 58 times.
116 qCDebug(DISMAN) << "Loading backend plugin:" << finfo.filePath();
229
230 58 QObject* instance = loader->instance();
231
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 58 times.
58 if (!instance) {
232 qCDebug(DISMAN) << loader->errorString();
233 return nullptr;
234 }
235
236 58 auto backend = qobject_cast<Disman::Backend*>(instance);
237
1/2
✓ Branch 0 taken 58 times.
✗ Branch 1 not taken.
58 if (backend) {
238 58 backend->init(arguments);
239
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 58 times.
58 if (!backend->valid()) {
240 qCDebug(DISMAN) << "Skipping" << backend->name() << "backend";
241 delete backend;
242 return nullptr;
243 }
244
2/2
✓ Branch 10 taken 58 times.
✓ Branch 11 taken 58 times.
116 qCDebug(DISMAN) << "Loaded successfully backend:" << backend->name();
245 58 return backend;
246 } else {
247 qCWarning(DISMAN) << finfo.fileName() << "does not provide a valid Disman backend.";
248 }
249
250 return nullptr;
251 58 }
252
253 80 Disman::Backend* BackendManager::load_backend_in_process(const QString& name)
254 {
255
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 80 times.
80 Q_ASSERT(mMethod == InProcess);
256
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 80 times.
80 if (mMethod == OutOfProcess) {
257 qCWarning(DISMAN)
258 << "You are trying to load a backend in process, while the BackendManager is set to "
259 "use OutOfProcess communication. Use load_backend_plugin() instead.";
260 return nullptr;
261 }
262 160 if (m_inProcessBackend.first != nullptr
263
9/10
✓ Branch 0 taken 35 times.
✓ Branch 1 taken 45 times.
✓ Branch 3 taken 35 times.
✗ Branch 4 not taken.
✓ Branch 7 taken 22 times.
✓ Branch 8 taken 13 times.
✓ Branch 9 taken 35 times.
✓ Branch 10 taken 45 times.
✓ Branch 12 taken 22 times.
✓ Branch 13 taken 58 times.
80 && (name.isEmpty() || m_inProcessBackend.first->name() == name)) {
264 22 return m_inProcessBackend.first;
265
7/8
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 45 times.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 13 times.
✓ Branch 7 taken 45 times.
✓ Branch 9 taken 13 times.
✓ Branch 10 taken 45 times.
58 } else if (m_inProcessBackend.first != nullptr && m_inProcessBackend.first->name() != name) {
266 13 shutdown_backend();
267 }
268
269
1/2
✓ Branch 0 taken 58 times.
✗ Branch 1 not taken.
58 if (mLoader == nullptr) {
270 58 mLoader = new QPluginLoader(this);
271 }
272 58 auto test_data_equals = QStringLiteral("TEST_DATA=");
273 58 QVariantMap arguments;
274 58 auto beargs = QString::fromLocal8Bit(qgetenv("DISMAN_BACKEND_ARGS"));
275
2/2
✓ Branch 1 taken 34 times.
✓ Branch 2 taken 24 times.
58 if (beargs.startsWith(test_data_equals)) {
276 68 arguments[QStringLiteral("TEST_DATA")] = beargs.remove(test_data_equals);
277 }
278 58 auto backend = BackendManager::load_backend_plugin(mLoader, name, arguments);
279
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 58 times.
58 if (!backend) {
280 return nullptr;
281 }
282 // qCDebug(DISMAN) << "Connecting ConfigMonitor to backend.";
283 58 ConfigMonitor::instance()->connect_in_process_backend(backend);
284 58 m_inProcessBackend = {backend, arguments};
285 58 set_config(backend->config());
286 58 return backend;
287 58 }
288
289 void BackendManager::request_backend()
290 {
291 Q_ASSERT(mMethod == OutOfProcess);
292 if (mInterface && mInterface->isValid()) {
293 ++mRequestsCounter;
294 QMetaObject::invokeMethod(this, "emit_backend_ready", Qt::QueuedConnection);
295 return;
296 }
297
298 // Another request already pending
299 if (mRequestsCounter > 0) {
300 return;
301 }
302 ++mRequestsCounter;
303
304 const QByteArray args = qgetenv("DISMAN_BACKEND_ARGS");
305 QVariantMap arguments;
306 if (!args.isEmpty()) {
307 QList<QByteArray> arglist = args.split(';');
308 Q_FOREACH (const QByteArray& arg, arglist) {
309 const int pos = arg.indexOf('=');
310 if (pos == -1) {
311 continue;
312 }
313 arguments.insert(QString::fromUtf8(arg.left(pos)), arg.mid(pos + 1));
314 }
315 }
316
317 start_backend(QString::fromLatin1(qgetenv("DISMAN_BACKEND")), arguments);
318 }
319
320 void BackendManager::emit_backend_ready()
321 {
322 Q_ASSERT(mMethod == OutOfProcess);
323 Q_EMIT backend_ready(mInterface);
324 --mRequestsCounter;
325 if (mShutdownLoop.isRunning()) {
326 mShutdownLoop.quit();
327 }
328 }
329
330 void BackendManager::start_backend(const QString& backend, const QVariantMap& arguments)
331 {
332 // This will autostart the launcher if it's not running already, calling
333 // request_backend(backend) will:
334 // a) if the launcher is started it will force it to load the correct backend,
335 // b) if the launcher is already running it will make sure it's running with
336 // the same backend as the one we requested and send an error otherwise
337 QDBusConnection conn = QDBusConnection::sessionBus();
338 QDBusMessage call = QDBusMessage::createMethodCall(QStringLiteral("org.kwinft.disman"),
339 QStringLiteral("/"),
340 QStringLiteral("org.kwinft.disman"),
341 QStringLiteral("requestBackend"));
342 call.setArguments({backend, arguments});
343 QDBusPendingCall pending = conn.asyncCall(call);
344 QDBusPendingCallWatcher* watcher = new QDBusPendingCallWatcher(pending);
345 connect(watcher,
346 &QDBusPendingCallWatcher::finished,
347 this,
348 &BackendManager::on_backend_request_done);
349 }
350
351 void BackendManager::on_backend_request_done(QDBusPendingCallWatcher* watcher)
352 {
353 Q_ASSERT(mMethod == OutOfProcess);
354 watcher->deleteLater();
355 QDBusPendingReply<bool> reply = *watcher;
356 // Most probably we requested an explicit backend that is different than the
357 // one already loaded in the launcher
358 if (reply.isError()) {
359 qCWarning(DISMAN) << "Failed to request backend:" << reply.error().name() << ":"
360 << reply.error().message();
361 invalidate_interface();
362 emit_backend_ready();
363 return;
364 }
365
366 // Most probably request and explicit backend which is not available or failed
367 // to initialize, or the launcher did not find any suitable backend for the
368 // current platform.
369 if (!reply.value()) {
370 qCWarning(DISMAN) << "Failed to request backend: unknown error";
371 invalidate_interface();
372 emit_backend_ready();
373 return;
374 }
375
376 // The launcher has successfully loaded the backend we wanted and registered
377 // it to DBus (hopefuly), let's try to get an interface for the backend.
378 if (mInterface) {
379 invalidate_interface();
380 }
381 mInterface = new org::kwinft::disman::backend(QStringLiteral("org.kwinft.disman"),
382 QStringLiteral("/backend"),
383 QDBusConnection::sessionBus());
384 if (!mInterface->isValid()) {
385 qCWarning(DISMAN) << "Backend successfully requested, but we failed to obtain a valid DBus "
386 "interface for it";
387 invalidate_interface();
388 emit_backend_ready();
389 return;
390 }
391
392 // The backend is GO, so let's watch for it's possible disappearance, so we
393 // can invalidate the interface
394 mServiceWatcher.addWatchedService(mBackendService);
395
396 // Immediatelly request config
397 connect(new GetConfigOperation, &GetConfigOperation::finished, this, [&](ConfigOperation* op) {
398 mConfig = qobject_cast<GetConfigOperation*>(op)->config();
399 emit_backend_ready();
400 });
401 // And listen for its change.
402 connect(mInterface,
403 &org::kwinft::disman::backend::configChanged,
404 this,
405 [&](const QVariantMap& newConfig) {
406 mConfig = Disman::ConfigSerializer::deserialize_config(newConfig);
407 });
408 }
409
410 void BackendManager::backend_service_unregistered(const QString& service_name)
411 {
412 Q_ASSERT(mMethod == OutOfProcess);
413 mServiceWatcher.removeWatchedService(service_name);
414
415 invalidate_interface();
416 request_backend();
417 }
418
419 void BackendManager::invalidate_interface()
420 {
421 Q_ASSERT(mMethod == OutOfProcess);
422 delete mInterface;
423 mInterface = nullptr;
424 mBackendService.clear();
425 }
426
427 9 ConfigPtr BackendManager::config() const
428 {
429 9 return mConfig;
430 }
431
432 79 void BackendManager::set_config(ConfigPtr c)
433 {
434 79 mConfig = c;
435 79 }
436
437 86 void BackendManager::shutdown_backend()
438 {
439
2/2
✓ Branch 0 taken 83 times.
✓ Branch 1 taken 3 times.
86 if (mMethod == InProcess) {
440
2/2
✓ Branch 0 taken 57 times.
✓ Branch 1 taken 26 times.
83 delete mLoader;
441 83 mLoader = nullptr;
442 83 m_inProcessBackend.second.clear();
443
2/2
✓ Branch 0 taken 57 times.
✓ Branch 1 taken 26 times.
83 delete m_inProcessBackend.first;
444 83 m_inProcessBackend.first = nullptr;
445 } else {
446
447
3/6
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
✗ Branch 6 not taken.
3 if (mBackendService.isEmpty() && !mInterface) {
448 3 return;
449 }
450
451 // If there are some currently pending requests, then wait for them to
452 // finish before quitting
453 while (mRequestsCounter > 0) {
454 mShutdownLoop.exec();
455 }
456
457 mServiceWatcher.removeWatchedService(mBackendService);
458 mShuttingDown = true;
459
460 QDBusMessage call = QDBusMessage::createMethodCall(QStringLiteral("org.kwinft.disman"),
461 QStringLiteral("/"),
462 QStringLiteral("org.kwinft.disman"),
463 QStringLiteral("quit"));
464 // Call synchronously
465 QDBusConnection::sessionBus().call(call);
466 invalidate_interface();
467
468 while (QDBusConnection::sessionBus().interface()->isServiceRegistered(
469 QStringLiteral("org.kwinft.disman"))) {
470 QThread::msleep(100);
471 }
472 }
473 }
474