backendmanager.cpp 14.2 KB
Newer Older
1
2
/*
 * Copyright (C) 2014  Daniel Vratil <dvratil@redhat.com>
Sebastian Kügler's avatar
Sebastian Kügler committed
3
 * Copyright 2015 Sebastian Kügler <sebas@kde.org>
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

21

22
#include "backendmanager_p.h"
23
24

#include "abstractbackend.h"
Sebastian Kügler's avatar
Sebastian Kügler committed
25
#include "config.h"
26
#include "configmonitor.h"
Daniel Vrátil's avatar
Daniel Vrátil committed
27
#include "backendinterface.h"
28
#include "debug_p.h"
29
30
#include "getconfigoperation.h"
#include "configserializer_p.h"
31
#include "log.h"
32
33
34
35
36
37

#include <QDBusConnection>
#include <QDBusPendingCall>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QDBusConnectionInterface>
Sebastian Kügler's avatar
Fixup    
Sebastian Kügler committed
38
#include <QGuiApplication>
Daniel Vrátil's avatar
Daniel Vrátil committed
39
#include <QStandardPaths>
40
#include <QThread>
41
42
43
44
#include <QX11Info>

#include <memory>

45

46
47
48
49
using namespace KScreen;

Q_DECLARE_METATYPE(org::kde::kscreen::Backend*)

50
const int BackendManager::sMaxCrashCount = 4;
51

52
BackendManager *BackendManager::sInstance = nullptr;
53
54
55
56
57
58
59
60
61
62
63

BackendManager *BackendManager::instance()
{
    if (!sInstance) {
        sInstance = new BackendManager();
    }

    return sInstance;
}

BackendManager::BackendManager()
64
    : mInterface(nullptr)
65
    , mCrashCount(0)
66
    , mShuttingDown(false)
67
    , mRequestsCounter(0)
Friedrich W. H. Kossebau's avatar
Friedrich W. H. Kossebau committed
68
    , mLoader(nullptr)
69
    , mMethod(OutOfProcess)
70
{
71
    Log::instance();
72
    // Decide whether to run in, or out-of-process
73

74
    // if KSCREEN_BACKEND_INPROCESS is set explicitly, we respect that
Sebastian Kügler's avatar
Sebastian Kügler committed
75
76
    const auto _inprocess = qgetenv("KSCREEN_BACKEND_INPROCESS");
    if (!_inprocess.isEmpty()) {
77

Sebastian Kügler's avatar
Sebastian Kügler committed
78
        const QByteArrayList falses({QByteArray("0"), QByteArray("false")});
79
80
81
82
83
        if (!falses.contains(_inprocess.toLower())) {
            mMethod = InProcess;
        } else {
            mMethod = OutOfProcess;
        }
84
85
    } else {
        // For XRandR backends, use out of process
Sebastian Kügler's avatar
Sebastian Kügler committed
86
        if (preferredBackend().fileName().startsWith(QLatin1String("KSC_XRandR"))) {
87
88
89
90
            mMethod = OutOfProcess;
        } else {
            mMethod = InProcess;
        }
91
    }
Sebastian Kügler's avatar
Sebastian Kügler committed
92
    initMethod();
93
}
94

Sebastian Kügler's avatar
Sebastian Kügler committed
95
void BackendManager::initMethod()
96
{
97
    if (mMethod == OutOfProcess) {
98
        qRegisterMetaType<org::kde::kscreen::Backend*>("OrgKdeKscreenBackendInterface");
99

100
101
102
103
104
105
106
107
108
109
110
111
112
        mServiceWatcher.setConnection(QDBusConnection::sessionBus());
        connect(&mServiceWatcher, &QDBusServiceWatcher::serviceUnregistered,
                this, &BackendManager::backendServiceUnregistered);

        mResetCrashCountTimer.setSingleShot(true);
        mResetCrashCountTimer.setInterval(60000);
        connect(&mResetCrashCountTimer, &QTimer::timeout,
                this, [=]() {
                    mCrashCount = 0;
                });
    }
}

113
void BackendManager::setMethod(BackendManager::Method m)
114
{
115
    if (mMethod == m) {
116
117
        return;
    }
118
    shutdownBackend();
119
120
    mMethod = m;
    initMethod();
121
122
}

123
BackendManager::Method BackendManager::method() const
124
{
125
    return mMethod;
126
127
}

128
129
BackendManager::~BackendManager()
{
130
131
132
    if (mMethod == InProcess) {
        shutdownBackend();
    }
133
134
}

135
QFileInfo BackendManager::preferredBackend(const QString &backend)
136
{
137
138
    /** this is the logic to pick a backend, in order of priority
     *
139
     * - backend argument is used if not empty
140
141
142
143
     * - env var KSCREEN_BACKEND is considered
     * - if platform is X11, the XRandR backend is picked
     * - if platform is wayland, KWayland backend is picked
     * - if neither is the case, QScreen backend is picked
Sebastian Kügler's avatar
Sebastian Kügler committed
144
     * - the QScreen backend is also used as fallback
145
146
147
     *
     */
    QString backendFilter;
Sebastian Kügler's avatar
Sebastian Kügler committed
148
    const auto env_kscreen_backend = qgetenv("KSCREEN_BACKEND");
149
150
151
    if (!backend.isEmpty()) {
        backendFilter = backend;
    } else if (!env_kscreen_backend.isEmpty()) {
152
153
154
155
156
157
158
159
160
161
        backendFilter = env_kscreen_backend;
    } else {
        if (QX11Info::isPlatformX11()) {
            backendFilter = QStringLiteral("XRandR");
        } else if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"))) {
            backendFilter = QStringLiteral("KWayland");
        } else {
            backendFilter = QStringLiteral("QScreen");
        }
    }
162
    QFileInfo fallback;
163
    Q_FOREACH (const QFileInfo &f, listBackends()) {
164
        // Here's the part where we do the match case-insensitive
165
        if (f.baseName().toLower() == QStringLiteral("ksc_%1").arg(backendFilter.toLower())) {
166
167
            return f;
        }
168
        if (f.baseName() == QLatin1String("KSC_QScreen")) {
169
170
            fallback = f;
        }
171
    }
Sebastian Kügler's avatar
Sebastian Kügler committed
172
173
//     qCWarning(KSCREEN) << "No preferred backend found. KSCREEN_BACKEND is set to " << env_kscreen_backend;
//     qCWarning(KSCREEN) << "falling back to " << fallback.fileName();
174
    return fallback;
175
176
177
178
179
}

QFileInfoList BackendManager::listBackends()
{
    // Compile a list of installed backends first
Sebastian Kügler's avatar
Sebastian Kügler committed
180
    const QString backendFilter = QStringLiteral("KSC_*");
181
182
    const QStringList paths = QCoreApplication::libraryPaths();
    QFileInfoList finfos;
Sebastian Kügler's avatar
Sebastian Kügler committed
183
    for (const QString &path : paths) {
184
185
        const QDir dir(path + QLatin1String("/kf5/kscreen/"),
                       backendFilter,
186
                       QDir::SortFlags(QDir::QDir::Name),
187
188
189
190
191
192
                       QDir::NoDotAndDotDot | QDir::Files);
        finfos.append(dir.entryInfoList());
    }
    return finfos;
}

193
KScreen::AbstractBackend *BackendManager::loadBackendPlugin(QPluginLoader *loader, const QString &name,
194
195
                                                     const QVariantMap &arguments)
{
196
    const auto finfo = preferredBackend(name);
197
198
199
200
201
202
203
204
205
206
207
208
209
210
    loader->setFileName(finfo.filePath());
    QObject *instance = loader->instance();
    if (!instance) {
        qCDebug(KSCREEN) << loader->errorString();
        return nullptr;
    }

    auto backend = qobject_cast<KScreen::AbstractBackend*>(instance);
    if (backend) {
        backend->init(arguments);
        if (!backend->isValid()) {
            qCDebug(KSCREEN) << "Skipping" << backend->name() << "backend";
            delete backend;
            return nullptr;
211
        }
Sebastian Kügler's avatar
Sebastian Kügler committed
212
        //qCDebug(KSCREEN) << "Loaded" << backend->name() << "backend";
213
214
215
        return backend;
    } else {
        qCDebug(KSCREEN) << finfo.fileName() << "does not provide valid KScreen backend";
216
217
    }

218
    return nullptr;
219
220
}

221
KScreen::AbstractBackend *BackendManager::loadBackendInProcess(const QString &name)
222
{
223
224
    Q_ASSERT(mMethod == InProcess);
    if (mMethod == OutOfProcess) {
Sebastian Kügler's avatar
Sebastian Kügler committed
225
        qCWarning(KSCREEN) << "You are trying to load a backend in process, while the BackendManager is set to use OutOfProcess communication. Use loadBackendPlugin() instead.";
226
227
        return nullptr;
    }
228
    if (m_inProcessBackend.first != nullptr && (name.isEmpty() || m_inProcessBackend.first->name() == name)) {
229
230
231
        return m_inProcessBackend.first;
    } else if (m_inProcessBackend.first != nullptr && m_inProcessBackend.first->name() != name) {
        shutdownBackend();
232
    }
233

234
235
236
    if (mLoader == nullptr) {
        mLoader = new QPluginLoader(this);
    }
237
238
239
240
241
    QVariantMap arguments;
    auto beargs = QString::fromLocal8Bit(qgetenv("KSCREEN_BACKEND_ARGS"));
    if (beargs.startsWith("TEST_DATA=")) {
        arguments["TEST_DATA"] = beargs.remove("TEST_DATA=");
    }
242
    auto backend = BackendManager::loadBackendPlugin(mLoader, name, arguments);
Sebastian Kügler's avatar
noise--    
Sebastian Kügler committed
243
    //qCDebug(KSCREEN) << "Connecting ConfigMonitor to backend.";
244
    ConfigMonitor::instance()->connectInProcessBackend(backend);
245
    m_inProcessBackend = qMakePair<KScreen::AbstractBackend*, QVariantMap>(backend, arguments);
246
    setConfig(backend->config());
247
    return backend;
248
}
249

250
251
void BackendManager::requestBackend()
{
252
    Q_ASSERT(mMethod == OutOfProcess);
253
    if (mInterface && mInterface->isValid()) {
254
255
        ++mRequestsCounter;
        QMetaObject::invokeMethod(this, "emitBackendReady", Qt::QueuedConnection);
256
257
258
        return;
    }

259
260
261
262
263
264
    // Another request already pending
    if (mRequestsCounter > 0) {
        return;
    }
    ++mRequestsCounter;

265
266
267
268
269
270
271
272
273
274
275
276
    const QByteArray args = qgetenv("KSCREEN_BACKEND_ARGS");
    QVariantMap arguments;
    if (!args.isEmpty()) {
        QList<QByteArray> arglist = args.split(';');
        Q_FOREACH (const QByteArray &arg, arglist) {
            const int pos = arg.indexOf('=');
            if (pos == -1) {
                continue;
            }
            arguments.insert(arg.left(pos), arg.mid(pos + 1));
        }
    }
Sebastian Kügler's avatar
Sebastian Kügler committed
277

278
    startBackend(QString::fromLatin1(qgetenv("KSCREEN_BACKEND")), arguments);
279
280
}

281
282
void BackendManager::emitBackendReady()
{
283
    Q_ASSERT(mMethod == OutOfProcess);
284
285
286
287
288
289
290
    Q_EMIT backendReady(mInterface);
    --mRequestsCounter;
    if (mShutdownLoop.isRunning()) {
        mShutdownLoop.quit();
    }
}

291
void BackendManager::startBackend(const QString &backend, const QVariantMap &arguments)
292
{
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
    // This will autostart the launcher if it's not running already, calling
    // requestBackend(backend) will:
    //   a) if the launcher is started it will force it to load the correct backend,
    //   b) if the launcher is already running it will make sure it's running with
    //      the same backend as the one we requested and send an error otherwise
    QDBusConnection conn = QDBusConnection::sessionBus();
    QDBusMessage call = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KScreen"),
                                                       QStringLiteral("/"),
                                                       QStringLiteral("org.kde.KScreen"),
                                                       QStringLiteral("requestBackend"));
    call.setArguments({ backend, arguments });
    QDBusPendingCall pending = conn.asyncCall(call);
    QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending);
    connect(watcher, &QDBusPendingCallWatcher::finished,
            this, &BackendManager::onBackendRequestDone);
308
309
}

310
void BackendManager::onBackendRequestDone(QDBusPendingCallWatcher *watcher)
311
{
312
    Q_ASSERT(mMethod == OutOfProcess);
313
    watcher->deleteLater();
314
315
316
317
318
    QDBusPendingReply<bool> reply = *watcher;
    // Most probably we requested an explicit backend that is different than the
    // one already loaded in the launcher
    if (reply.isError()) {
        qCWarning(KSCREEN) << "Failed to request backend:" << reply.error().name() << ":" << reply.error().message();
319
        invalidateInterface();
320
        emitBackendReady();
321
322
323
        return;
    }

324
325
326
327
328
    // Most probably request and explicit backend which is not available or failed
    // to initialize, or the launcher did not find any suitable backend for the
    // current platform.
    if (!reply.value()) {
        qCWarning(KSCREEN) << "Failed to request backend: unknown error";
329
        invalidateInterface();
330
        emitBackendReady();
331
        return;
332
333
    }

334
335
    // The launcher has successfully loaded the backend we wanted and registered
    // it to DBus (hopefuly), let's try to get an interface for the backend.
336
337
338
    if (mInterface) {
        invalidateInterface();
    }
339
340
341
    mInterface = new org::kde::kscreen::Backend(QStringLiteral("org.kde.KScreen"),
                                                QStringLiteral("/backend"),
                                                QDBusConnection::sessionBus());
342
    if (!mInterface->isValid()) {
343
344
345
        qCWarning(KSCREEN) << "Backend successfully requested, but we failed to obtain a valid DBus interface for it";
        invalidateInterface();
        emitBackendReady();
346
        return;
347
    }
348

349
350
    // The backend is GO, so let's watch for it's possible disappearance, so we
    // can invalidate the interface
351
352
    mServiceWatcher.addWatchedService(mBackendService);

353
354
355
356
    // Immediatelly request config
    connect(new GetConfigOperation(GetConfigOperation::NoEDID), &GetConfigOperation::finished,
            [&](ConfigOperation *op) {
                mConfig = qobject_cast<GetConfigOperation*>(op)->config();
357
                emitBackendReady();
358
            });
359
    // And listen for its change.
360
361
362
363
    connect(mInterface, &org::kde::kscreen::Backend::configChanged,
            [&](const QVariantMap &newConfig) {
                mConfig = KScreen::ConfigSerializer::deserializeConfig(newConfig);
            });
364
}
365
366
367

void BackendManager::backendServiceUnregistered(const QString &serviceName)
{
368
    Q_ASSERT(mMethod == OutOfProcess);
369
370
371
372
373
374
375
376
    mServiceWatcher.removeWatchedService(serviceName);

    invalidateInterface();
    requestBackend();
}

void BackendManager::invalidateInterface()
{
377
    Q_ASSERT(mMethod == OutOfProcess);
378
    delete mInterface;
Friedrich W. H. Kossebau's avatar
Friedrich W. H. Kossebau committed
379
    mInterface = nullptr;
380
381
    mBackendService.clear();
}
382
383
384
385
386

ConfigPtr BackendManager::config() const
{
    return mConfig;
}
387

388
389
void BackendManager::setConfig(ConfigPtr c)
{
Sebastian Kügler's avatar
Sebastian Kügler committed
390
    //qCDebug(KSCREEN) << "BackendManager::setConfig, outputs:" << c->outputs().count();
391
392
393
    mConfig = c;
}

394
395
void BackendManager::shutdownBackend()
{
396
    if (mMethod == InProcess) {
397
        delete mLoader;
398
        mLoader = nullptr;
399
400
401
        m_inProcessBackend.second.clear();
        delete m_inProcessBackend.first;
        m_inProcessBackend.first = nullptr;
402
    } else {
403

404
405
406
        if (mBackendService.isEmpty() && !mInterface) {
            return;
        }
407

408
409
410
411
412
        // If there are some currently pending requests, then wait for them to
        // finish before quitting
        while (mRequestsCounter > 0) {
            mShutdownLoop.exec();
        }
413

414
415
        mServiceWatcher.removeWatchedService(mBackendService);
        mShuttingDown = true;
416

417
418
419
420
421
422
423
        QDBusMessage call = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KScreen"),
                                                        QStringLiteral("/"),
                                                        QStringLiteral("org.kde.KScreen"),
                                                        QStringLiteral("quit"));
        // Call synchronously
        QDBusConnection::sessionBus().call(call);
        invalidateInterface();
424

425
426
427
        while (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.KScreen"))) {
            QThread::msleep(100);
        }
428
    }
429
}