// SPDX-License-Identifier: GPL-3.0-or-later #include "lockwatcher.h" #include "lockers.h" #include "sessionlock.h" #include "wayland.h" #include #include #include #include #include #include #include #include #include namespace { const QString Login1 = QStringLiteral("org.freedesktop.login1"); const QString SessionInterface = QStringLiteral("org.freedesktop.login1.Session"); const QString Properties = QStringLiteral("org.freedesktop.DBus.Properties"); constexpr int PollMs = 1000; } // namespace LockWatcher::LockWatcher(QObject *parent) : QObject(parent) { QDBusConnection session = QDBusConnection::sessionBus(); session.connect(QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("/ScreenSaver"), QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("ActiveChanged"), this, SLOT(onScreenSaver(bool))); // Started while the screen is already locked (the agent restarted). QDBusMessage get = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("/ScreenSaver"), QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("GetActive")); get.setAutoStartService(false); auto *watcher = new QDBusPendingCallWatcher(session.asyncCall(get), this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher] { watcher->deleteLater(); const QDBusPendingReply reply = *watcher; if (reply.isValid() && reply.value()) { onScreenSaver(true); } }); findSession(); m_ownLockers = Wayland::hasGlobal("ext_session_lock_manager_v1"); if (m_ownLockers) { connect(&m_poll, &QTimer::timeout, this, &LockWatcher::pollLockers); m_poll.start(PollMs); // Not from here: nobody is connected yet to hear about a lock screen // that is already up. QTimer::singleShot(0, this, &LockWatcher::pollLockers); } } void LockWatcher::onScreenSaver(bool active) { m_screenSaver = active; update(); } void LockWatcher::findSession() { // Niri and some others hand their session on to user services. Without // it, "auto" is the session on the display. const QString id = qEnvironmentVariable("XDG_SESSION_ID"); if (!id.isEmpty()) { QDBusMessage call = QDBusMessage::createMethodCall(Login1, QStringLiteral("/org/freedesktop/login1"), QStringLiteral("org.freedesktop.login1.Manager"), QStringLiteral("GetSession")); call << id; auto *watcher = new QDBusPendingCallWatcher(QDBusConnection::systemBus().asyncCall(call), this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher] { watcher->deleteLater(); const QDBusPendingReply reply = *watcher; watchSession(reply.isValid() ? reply.value().path() : QStringLiteral("/org/freedesktop/login1/session/auto")); }); return; } QDBusMessage call = QDBusMessage::createMethodCall(Login1, QStringLiteral("/org/freedesktop/login1/session/auto"), Properties, QStringLiteral("Get")); call << SessionInterface << QStringLiteral("Id"); auto *watcher = new QDBusPendingCallWatcher(QDBusConnection::systemBus().asyncCall(call), this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher] { watcher->deleteLater(); const QDBusPendingReply reply = *watcher; if (!reply.isValid()) { qWarning("logind knows no session for this agent: %s", qPrintable(reply.error().message())); return; } // The signals come from the session's real path, not from "auto". QDBusMessage call = QDBusMessage::createMethodCall(Login1, QStringLiteral("/org/freedesktop/login1"), QStringLiteral("org.freedesktop.login1.Manager"), QStringLiteral("GetSession")); call << reply.value().variant().toString(); auto *next = new QDBusPendingCallWatcher(QDBusConnection::systemBus().asyncCall(call), this); connect(next, &QDBusPendingCallWatcher::finished, this, [this, next] { next->deleteLater(); const QDBusPendingReply path = *next; if (path.isValid()) { watchSession(path.value().path()); } }); }); } void LockWatcher::watchSession(const QString &path) { m_session = path; QDBusConnection::systemBus().connect(Login1, path, Properties, QStringLiteral("PropertiesChanged"), this, SLOT(onSessionProperties(QString, QVariantMap, QStringList))); readLockedHint(); } void LockWatcher::readLockedHint() { QDBusMessage call = QDBusMessage::createMethodCall(Login1, m_session, Properties, QStringLiteral("Get")); call << SessionInterface << QStringLiteral("LockedHint"); auto *watcher = new QDBusPendingCallWatcher(QDBusConnection::systemBus().asyncCall(call), this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher] { watcher->deleteLater(); const QDBusPendingReply reply = *watcher; if (reply.isValid()) { m_lockedHint = reply.value().variant().toBool(); update(); } }); } void LockWatcher::onSessionProperties(const QString &interface, const QVariantMap &changed, const QStringList &invalidated) { if (interface != SessionInterface) { return; } if (changed.contains(QStringLiteral("LockedHint"))) { m_lockedHint = changed.value(QStringLiteral("LockedHint")).toBool(); update(); } else if (invalidated.contains(QStringLiteral("LockedHint"))) { readLockedHint(); } } void LockWatcher::pollLockers() { m_lockerRunning = !Lockers::find().isEmpty(); const bool ready = Lockers::ready(SIGUSR1); const bool became = ready && !m_lockerReady; m_lockerReady = ready; update(); if (became) { Q_EMIT unlockable(); } } void LockWatcher::setOwnLock(SessionLock *lock) { m_own = lock; connect(lock, &SessionLock::lockedChanged, this, &LockWatcher::update); } bool LockWatcher::canUnlock() const { return (m_own && m_own->isLocked()) || !m_ownLockers || m_lockerReady; } void LockWatcher::update() { const bool locked = m_screenSaver || m_lockedHint || m_lockerRunning || (m_own && m_own->isLocked()); if (locked != m_locked) { m_locked = locked; Q_EMIT lockedChanged(locked); } } void LockWatcher::unlock() { if (m_own && m_own->isLocked()) { m_own->unlock(); return; } // Looked up again rather than taken from the last poll: a second is // long enough for a pid to belong to something else. Lockers::signal(SIGUSR1); const QDBusMessage call = QDBusMessage::createMethodCall(Login1, m_session.isEmpty() ? QStringLiteral("/org/freedesktop/login1/session/auto") : m_session, SessionInterface, QStringLiteral("Unlock")); auto *watcher = new QDBusPendingCallWatcher(QDBusConnection::systemBus().asyncCall(call), this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher] { watcher->deleteLater(); const QDBusPendingReply<> reply = *watcher; if (reply.isError()) { qWarning("logind would not unlock: %s; trying loginctl", qPrintable(reply.error().message())); const QString id = qEnvironmentVariable("XDG_SESSION_ID"); QStringList args{QStringLiteral("unlock-session")}; if (!id.isEmpty()) { args << id; } QProcess::startDetached(QStringLiteral("loginctl"), args); } }); }