feat: support gnome, hyprland and niri
This commit is contained in:
1 parent
bccd1f5510
commit
eaec9325af
48 files changed
+4030
-2131
No files matched your search
@@ -51,6 +51,13 @@ QString AgentSocket::path()
|
||||
return QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + QStringLiteral("/face-unlock/agent.socket");
|
||||
}
|
||||
|
||||
bool AgentSocket::running()
|
||||
{
|
||||
QLocalSocket probe;
|
||||
probe.connectToServer(path());
|
||||
return probe.waitForConnected(500);
|
||||
}
|
||||
|
||||
bool AgentSocket::listen()
|
||||
{
|
||||
const QString p = path();
|
||||
|
||||
@@ -22,6 +22,10 @@ public:
|
||||
explicit AgentSocket(QObject *parent = nullptr);
|
||||
bool listen();
|
||||
|
||||
// Whether another agent already listens here. Hyprland without a systemd
|
||||
// session starts the agent from its own config, and that must not make
|
||||
// two of them.
|
||||
static bool running();
|
||||
static QString path();
|
||||
|
||||
Q_SIGNALS:
|
||||
|
||||
@@ -101,7 +101,8 @@ void BubbleWindow::allowOverLockscreen()
|
||||
return;
|
||||
}
|
||||
if (!m_overlay->isActive()) {
|
||||
if (!warned) {
|
||||
// Only KWin has the protocol. Anywhere else there is nothing to fix.
|
||||
if (!warned && qEnvironmentVariable("XDG_CURRENT_DESKTOP").split(u':').contains(u"KDE")) {
|
||||
warned = true;
|
||||
qWarning("KWin does not let this program show above the lock screen; is its desktop file installed?");
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
// file lists the interface, which the one installed with this does. The
|
||||
// request has to be made for every new surface role, before it is mapped, so
|
||||
// it is repeated each time the window is shown again.
|
||||
//
|
||||
// Other compositors have no such thing. On Hyprland and Niri the lock screen
|
||||
// covers the bubble, which shows again with the tick once it is gone. GNOME
|
||||
// has no layer-shell at all, so there is no bubble there (see main.cpp).
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
+127
-2
@@ -2,6 +2,10 @@
|
||||
|
||||
#include "inputwatcher.h"
|
||||
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusConnectionInterface>
|
||||
#include <QDBusMessage>
|
||||
#include <QDBusPendingCallWatcher>
|
||||
#include <QGuiApplication>
|
||||
#include <QWaylandClientExtensionTemplate>
|
||||
#include <QtGui/qguiapplication_platform.h>
|
||||
@@ -54,6 +58,111 @@ private:
|
||||
bool m_fired = false;
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
const QString MutterService = QStringLiteral("org.gnome.Mutter.IdleMonitor");
|
||||
const QString MutterPath = QStringLiteral("/org/gnome/Mutter/IdleMonitor/Core");
|
||||
} // namespace
|
||||
|
||||
// GNOME's way: a watch that fires once nothing was touched for the calm,
|
||||
// then one that fires at the next input. Each fires once and is gone.
|
||||
class MutterIdle : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
MutterIdle(std::function<void()> input, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_input(std::move(input))
|
||||
{
|
||||
QDBusConnection::sessionBus().connect(MutterService, MutterPath, MutterService, QStringLiteral("WatchFired"), this, SLOT(onFired(uint)));
|
||||
}
|
||||
~MutterIdle() override
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
void watch(int calmMs)
|
||||
{
|
||||
stop();
|
||||
const int generation = m_generation;
|
||||
if (calmMs <= 0) {
|
||||
addWatch(QStringLiteral("AddUserActiveWatch"), {}, generation, &m_active);
|
||||
return;
|
||||
}
|
||||
// Already calm for that long (the scan itself took a while): the
|
||||
// idle watch would wait for the next calm, so go straight on.
|
||||
call(QStringLiteral("GetIdletime"), {}, [this, generation, calmMs](const QDBusMessage &reply) {
|
||||
if (generation != m_generation) {
|
||||
return;
|
||||
}
|
||||
if (reply.arguments().value(0).toULongLong() >= quint64(calmMs)) {
|
||||
addWatch(QStringLiteral("AddUserActiveWatch"), {}, generation, &m_active);
|
||||
} else {
|
||||
addWatch(QStringLiteral("AddIdleWatch"), {QVariant::fromValue(quint64(calmMs))}, generation, &m_idle);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
++m_generation;
|
||||
remove(m_idle);
|
||||
remove(m_active);
|
||||
m_idle = m_active = 0;
|
||||
}
|
||||
|
||||
private Q_SLOTS:
|
||||
void onFired(uint id)
|
||||
{
|
||||
if (id != 0 && id == m_idle) {
|
||||
remove(m_idle);
|
||||
m_idle = 0;
|
||||
addWatch(QStringLiteral("AddUserActiveWatch"), {}, m_generation, &m_active);
|
||||
} else if (id != 0 && id == m_active) {
|
||||
m_active = 0;
|
||||
m_input();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
template<typename Done>
|
||||
void call(const QString &method, const QVariantList &args, Done done)
|
||||
{
|
||||
QDBusMessage msg = QDBusMessage::createMethodCall(MutterService, MutterPath, MutterService, method);
|
||||
msg.setArguments(args);
|
||||
auto *watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(msg), this);
|
||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [watcher, done] {
|
||||
watcher->deleteLater();
|
||||
done(watcher->reply());
|
||||
});
|
||||
}
|
||||
|
||||
void addWatch(const QString &method, const QVariantList &args, int generation, uint *slot)
|
||||
{
|
||||
call(method, args, [this, generation, slot](const QDBusMessage &reply) {
|
||||
const uint id = reply.arguments().value(0).toUInt();
|
||||
if (generation != m_generation) {
|
||||
// Stopped in the meantime.
|
||||
remove(id);
|
||||
return;
|
||||
}
|
||||
*slot = id;
|
||||
});
|
||||
}
|
||||
|
||||
void remove(uint id)
|
||||
{
|
||||
if (id != 0) {
|
||||
call(QStringLiteral("RemoveWatch"), {QVariant::fromValue(id)}, [](const QDBusMessage &) { });
|
||||
}
|
||||
}
|
||||
|
||||
std::function<void()> m_input;
|
||||
int m_generation = 0;
|
||||
uint m_idle = 0;
|
||||
uint m_active = 0;
|
||||
};
|
||||
|
||||
InputWatcher::InputWatcher(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_notifier(std::make_unique<IdleNotifier>())
|
||||
@@ -64,10 +173,21 @@ InputWatcher::~InputWatcher() = default;
|
||||
|
||||
void InputWatcher::watch(int calmMs)
|
||||
{
|
||||
m_notification.reset();
|
||||
stop();
|
||||
auto *wayland = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>();
|
||||
if (!m_notifier->isActive() || !wayland || !wayland->seat()) {
|
||||
qWarning("no ext_idle_notifier_v1, so no telling when somebody comes back");
|
||||
if (!m_mutter && QDBusConnection::sessionBus().interface()->isServiceRegistered(MutterService)) {
|
||||
m_mutter = new MutterIdle(
|
||||
[this] {
|
||||
QMetaObject::invokeMethod(this, &InputWatcher::input, Qt::QueuedConnection);
|
||||
},
|
||||
this);
|
||||
}
|
||||
if (m_mutter) {
|
||||
m_mutter->watch(calmMs);
|
||||
} else {
|
||||
qWarning("no ext_idle_notifier_v1 and no Mutter IdleMonitor, so no telling when somebody comes back");
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Version 1 has only the notification that inhibitors hold back.
|
||||
@@ -84,4 +204,9 @@ void InputWatcher::watch(int calmMs)
|
||||
void InputWatcher::stop()
|
||||
{
|
||||
m_notification.reset();
|
||||
if (m_mutter) {
|
||||
m_mutter->stop();
|
||||
}
|
||||
}
|
||||
|
||||
#include "inputwatcher.moc"
|
||||
@@ -5,7 +5,8 @@
|
||||
// From ext_idle_notifier_v1, like KIdleTime, but with the input notification
|
||||
// of version 2. KIdleTime's own honours idle inhibitors: with a video playing
|
||||
// or an app keeping the screen on, no key reached it and the lock screen never
|
||||
// scanned.
|
||||
// scanned. GNOME has no ext_idle_notifier_v1; there it is Mutter's own
|
||||
// IdleMonitor on the session bus, which knows nothing of inhibitors either.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -15,6 +16,7 @@
|
||||
|
||||
class IdleNotifier;
|
||||
class IdleNotification;
|
||||
class MutterIdle;
|
||||
|
||||
class InputWatcher : public QObject
|
||||
{
|
||||
@@ -34,4 +36,5 @@ Q_SIGNALS:
|
||||
private:
|
||||
std::unique_ptr<IdleNotifier> m_notifier;
|
||||
std::unique_ptr<IdleNotification> m_notification;
|
||||
MutterIdle *m_mutter = nullptr;
|
||||
};
|
||||
@@ -7,11 +7,7 @@
|
||||
#include "userconfig.h"
|
||||
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
#include <QDBusPendingCallWatcher>
|
||||
#include <QDBusPendingReply>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -37,14 +33,7 @@ LockController::LockController(BubbleController *bubble, UserConfig *config, QOb
|
||||
arm(0);
|
||||
});
|
||||
connect(&m_input, &InputWatcher::input, this, &LockController::onResume);
|
||||
|
||||
QDBusConnection session = QDBusConnection::sessionBus();
|
||||
session.connect(QStringLiteral("org.freedesktop.ScreenSaver"),
|
||||
QStringLiteral("/ScreenSaver"),
|
||||
QStringLiteral("org.freedesktop.ScreenSaver"),
|
||||
QStringLiteral("ActiveChanged"),
|
||||
this,
|
||||
SLOT(onActiveChanged(bool)));
|
||||
connect(&m_lock, &LockWatcher::lockedChanged, this, &LockController::onLockedChanged);
|
||||
|
||||
QDBusConnection::systemBus().connect(QStringLiteral("org.freedesktop.login1"),
|
||||
QStringLiteral("/org/freedesktop/login1"),
|
||||
@@ -52,28 +41,14 @@ LockController::LockController(BubbleController *bubble, UserConfig *config, QOb
|
||||
QStringLiteral("PrepareForSleep"),
|
||||
this,
|
||||
SLOT(onPrepareForSleep(bool)));
|
||||
|
||||
// Started while the screen is already locked (the agent restarted).
|
||||
const QDBusMessage get = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"),
|
||||
QStringLiteral("/ScreenSaver"),
|
||||
QStringLiteral("org.freedesktop.ScreenSaver"),
|
||||
QStringLiteral("GetActive"));
|
||||
auto *watcher = new QDBusPendingCallWatcher(session.asyncCall(get), this);
|
||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher] {
|
||||
watcher->deleteLater();
|
||||
const QDBusPendingReply<bool> reply = *watcher;
|
||||
if (reply.isValid() && reply.value()) {
|
||||
onActiveChanged(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void LockController::onActiveChanged(bool active)
|
||||
void LockController::onLockedChanged(bool locked)
|
||||
{
|
||||
if (active == m_locked) {
|
||||
if (locked == m_locked) {
|
||||
return;
|
||||
}
|
||||
if (!active) {
|
||||
if (!locked) {
|
||||
m_locked = false;
|
||||
m_armTimer.stop();
|
||||
m_input.stop();
|
||||
@@ -189,7 +164,7 @@ void LockController::onScanFinished(const QJsonObject &result)
|
||||
// moment to go, and the bubble stays above the desktop, so the rings
|
||||
// and the tick play on over it.
|
||||
m_bubble->succeeded();
|
||||
unlock();
|
||||
m_lock.unlock();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -207,30 +182,3 @@ void LockController::onScanFinished(const QJsonObject &result)
|
||||
}
|
||||
arm(CalmBeforeRetryMs);
|
||||
}
|
||||
|
||||
void LockController::unlock()
|
||||
{
|
||||
if (!m_locked) {
|
||||
return;
|
||||
}
|
||||
// "auto" is the caller's own session, or for a program outside any
|
||||
// session (this one runs as a user service) the session on the display.
|
||||
const QDBusMessage call = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.login1"),
|
||||
QStringLiteral("/org/freedesktop/login1/session/auto"),
|
||||
QStringLiteral("org.freedesktop.login1.Session"),
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -4,22 +4,20 @@
|
||||
//
|
||||
// When the screen locks, this waits for somebody to come back: a key, the
|
||||
// mouse, the lid opening, the machine waking from sleep. Then it scans, and
|
||||
// when the face matches it asks logind to unlock the session, which is the
|
||||
// same request `loginctl unlock-session` makes and which Plasma's screen
|
||||
// locker has always honoured.
|
||||
// when the face matches it unlocks the session the way that desktop's lock
|
||||
// screen wants it (see LockWatcher).
|
||||
//
|
||||
// Why not a PAM module in the lock screen, like fingerprints? Plasma runs its
|
||||
// fingerprint stack in parallel with the password, but only starts it once per
|
||||
// lock, gives up for good the first time it fails, and labels it "scan your
|
||||
// fingerprint". Doing it from here means scanning again every time somebody
|
||||
// sits back down, no wrong label, and a bubble that knows what is going on.
|
||||
// It does not lower the bar either: any program running as this user can
|
||||
// already unlock this user's session through logind. Face data and the
|
||||
// decision stay with the daemon, which runs as root.
|
||||
// fingerprint". GNOME's, hyprlock's and swaylock's only ask once the password
|
||||
// is typed. Doing it from here means scanning again every time somebody sits
|
||||
// back down, no wrong label, and a bubble that knows what is going on.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "inputwatcher.h"
|
||||
#include "lockwatcher.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QObject>
|
||||
@@ -42,7 +40,7 @@ public:
|
||||
}
|
||||
|
||||
private Q_SLOTS:
|
||||
void onActiveChanged(bool active);
|
||||
void onLockedChanged(bool locked);
|
||||
void onPrepareForSleep(bool sleeping);
|
||||
|
||||
private:
|
||||
@@ -51,7 +49,6 @@ private:
|
||||
void onResume();
|
||||
void startScan(const QString &why);
|
||||
void onScanFinished(const QJsonObject &result);
|
||||
void unlock();
|
||||
void warmUp();
|
||||
|
||||
BubbleController *m_bubble;
|
||||
@@ -62,4 +59,5 @@ private:
|
||||
QPointer<DaemonRequest> m_scan;
|
||||
QTimer m_armTimer;
|
||||
InputWatcher m_input;
|
||||
LockWatcher m_lock;
|
||||
};
|
||||
@@ -0,0 +1,268 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "lockwatcher.h"
|
||||
|
||||
#include "wayland.h"
|
||||
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
#include <QDBusObjectPath>
|
||||
#include <QDBusPendingCallWatcher>
|
||||
#include <QDBusPendingReply>
|
||||
#include <QDBusVariant>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
|
||||
#include <dirent.h>
|
||||
#include <signal.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
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;
|
||||
|
||||
// The lock screens that unlock on SIGUSR1.
|
||||
bool isLocker(const QByteArray &name)
|
||||
{
|
||||
return name == "hyprlock" || name == "swaylock";
|
||||
}
|
||||
|
||||
QByteArray readFile(const QString &path)
|
||||
{
|
||||
QFile f(path);
|
||||
return f.open(QIODevice::ReadOnly) ? f.readAll() : QByteArray();
|
||||
}
|
||||
|
||||
pid_t parentOf(pid_t pid)
|
||||
{
|
||||
// The fields after the name in brackets, which can hold anything.
|
||||
const QByteArray stat = readFile(QStringLiteral("/proc/%1/stat").arg(pid));
|
||||
const qsizetype close = stat.lastIndexOf(')');
|
||||
return close < 0 ? 0 : pid_t(stat.mid(close + 2).split(' ').value(1).toInt());
|
||||
}
|
||||
|
||||
// The lock screens of this user on this display. Another session of the same
|
||||
// user has a display of its own, and its lock screen is left alone. A child
|
||||
// of a lock screen (swaylock checks the password in one) is left out: killed
|
||||
// by the signal before its parent unlocks, it would take the parent down
|
||||
// with it, and the screen would stay locked.
|
||||
QList<pid_t> findLockers()
|
||||
{
|
||||
QList<pid_t> found;
|
||||
DIR *proc = ::opendir("/proc");
|
||||
if (!proc) {
|
||||
return found;
|
||||
}
|
||||
QByteArray display = qgetenv("WAYLAND_DISPLAY");
|
||||
if (display.isEmpty()) {
|
||||
display = "wayland-0";
|
||||
}
|
||||
const uid_t me = ::getuid();
|
||||
while (dirent *entry = ::readdir(proc)) {
|
||||
const pid_t pid = pid_t(atoi(entry->d_name));
|
||||
struct stat st;
|
||||
if (pid <= 0 || ::fstatat(::dirfd(proc), entry->d_name, &st, 0) != 0 || st.st_uid != me) {
|
||||
continue;
|
||||
}
|
||||
const QString dir = QStringLiteral("/proc/%1/").arg(pid);
|
||||
if (!isLocker(readFile(dir + QStringLiteral("comm")).trimmed())) {
|
||||
continue;
|
||||
}
|
||||
bool sameDisplay = true;
|
||||
for (const QByteArray &var : readFile(dir + QStringLiteral("environ")).split('\0')) {
|
||||
if (var.startsWith("WAYLAND_DISPLAY=")) {
|
||||
sameDisplay = var.mid(16) == display;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sameDisplay) {
|
||||
found.append(pid);
|
||||
}
|
||||
}
|
||||
::closedir(proc);
|
||||
QList<pid_t> top;
|
||||
for (const pid_t pid : std::as_const(found)) {
|
||||
if (!found.contains(parentOf(pid))) {
|
||||
top.append(pid);
|
||||
}
|
||||
}
|
||||
return top;
|
||||
}
|
||||
} // 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<bool> reply = *watcher;
|
||||
if (reply.isValid() && reply.value()) {
|
||||
onScreenSaver(true);
|
||||
}
|
||||
});
|
||||
|
||||
findSession();
|
||||
|
||||
if (Wayland::hasGlobal("ext_session_lock_manager_v1")) {
|
||||
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<QDBusObjectPath> 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<QDBusVariant> 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<QDBusObjectPath> 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<QDBusVariant> 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 = !findLockers().isEmpty();
|
||||
update();
|
||||
}
|
||||
|
||||
void LockWatcher::update()
|
||||
{
|
||||
const bool locked = m_screenSaver || m_lockedHint || m_lockerRunning;
|
||||
if (locked != m_locked) {
|
||||
m_locked = locked;
|
||||
Q_EMIT lockedChanged(locked);
|
||||
}
|
||||
}
|
||||
|
||||
void LockWatcher::unlock()
|
||||
{
|
||||
// Looked up again rather than taken from the last poll: a second is
|
||||
// long enough for a pid to belong to something else.
|
||||
for (const pid_t pid : findLockers()) {
|
||||
::kill(pid, 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Whether the screen is locked, and how to unlock it, on each desktop.
|
||||
//
|
||||
// Plasma org.freedesktop.ScreenSaver and logind's LockedHint. Unlocked
|
||||
// through logind.
|
||||
// GNOME logind's LockedHint. Unlocked through logind.
|
||||
// Niri logind's LockedHint, which niri sets for any lock screen.
|
||||
// Hyprland sets no LockedHint, so the lock screen is looked for among this
|
||||
// user's processes (hyprlock, swaylock) once a second. Only on
|
||||
// compositors where the lock screen is a program of its own
|
||||
// (ext-session-lock).
|
||||
//
|
||||
// hyprlock and swaylock do not listen to logind. They unlock on SIGUSR1.
|
||||
//
|
||||
// None of this lowers the bar: any program running as this user can already
|
||||
// ask logind to unlock the session, or send its own lock screen a signal. The
|
||||
// face data and the decision stay with the daemon, which runs as root.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
class LockWatcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LockWatcher(QObject *parent = nullptr);
|
||||
|
||||
bool locked() const
|
||||
{
|
||||
return m_locked;
|
||||
}
|
||||
void unlock();
|
||||
|
||||
Q_SIGNALS:
|
||||
void lockedChanged(bool locked);
|
||||
|
||||
private Q_SLOTS:
|
||||
void onScreenSaver(bool active);
|
||||
void onSessionProperties(const QString &interface, const QVariantMap &changed, const QStringList &invalidated);
|
||||
|
||||
private:
|
||||
void findSession();
|
||||
void watchSession(const QString &path);
|
||||
void readLockedHint();
|
||||
void pollLockers();
|
||||
void update();
|
||||
|
||||
bool m_screenSaver = false;
|
||||
bool m_lockedHint = false;
|
||||
bool m_lockerRunning = false;
|
||||
bool m_locked = false;
|
||||
QString m_session;
|
||||
QTimer m_poll;
|
||||
};
|
||||
+17
-2
@@ -14,6 +14,7 @@
|
||||
#include "enrollcontroller.h"
|
||||
#include "lockcontroller.h"
|
||||
#include "userconfig.h"
|
||||
#include "wayland.h"
|
||||
|
||||
#include "buildconfig.h"
|
||||
|
||||
@@ -82,7 +83,7 @@ int main(int argc, char **argv)
|
||||
KLocalizedString::setApplicationDomain(FU_NAME);
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription(i18n("Face unlock for KDE Plasma"));
|
||||
parser.setApplicationDescription(i18n("Face unlock for the lock screen, sudo and admin prompts"));
|
||||
parser.addHelpOption();
|
||||
parser.addVersionOption();
|
||||
const QCommandLineOption enrollOpt(QStringLiteral("enroll"), i18n("Set up a face."));
|
||||
@@ -110,14 +111,28 @@ int main(int argc, char **argv)
|
||||
config.overrideStyle(parser.value(styleOpt));
|
||||
}
|
||||
BubbleController bubble(&config);
|
||||
BubbleWindow window(&engine, &bubble);
|
||||
// The bubble floats above everything as a layer-shell surface. GNOME has
|
||||
// none, and a normal window cannot stay on top or pick its place, so
|
||||
// there it does without.
|
||||
std::unique_ptr<BubbleWindow> window;
|
||||
if (Wayland::hasGlobal("zwlr_layer_shell_v1")) {
|
||||
window = std::make_unique<BubbleWindow>(&engine, &bubble);
|
||||
}
|
||||
|
||||
if (parser.isSet(demoOpt)) {
|
||||
if (!window) {
|
||||
qWarning("this desktop has no layer-shell, so there is no bubble to show");
|
||||
return 1;
|
||||
}
|
||||
scheduleDemo(&bubble);
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
AgentSocket socket;
|
||||
if (AgentSocket::running()) {
|
||||
qInfo("an agent is already running in this session");
|
||||
return 0;
|
||||
}
|
||||
socket.listen();
|
||||
QObject::connect(&socket, &AgentSocket::scanEvent, &bubble, &BubbleController::daemonEvent);
|
||||
|
||||
|
||||
@@ -13,10 +13,15 @@ Window {
|
||||
|
||||
required property var controller
|
||||
|
||||
// One fixed size: the layout needs no more room, and tiling compositors
|
||||
// (Hyprland, Niri) float a window that cannot be resized instead of
|
||||
// stretching it over half the screen.
|
||||
width: 520
|
||||
height: 680
|
||||
minimumWidth: 460
|
||||
minimumHeight: 620
|
||||
minimumWidth: width
|
||||
maximumWidth: width
|
||||
minimumHeight: height
|
||||
maximumHeight: height
|
||||
visible: true
|
||||
color: Theme.panel
|
||||
title: i18n("Set up Face Unlock")
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "wayland.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QGuiApplication>
|
||||
#include <QSet>
|
||||
|
||||
#include <wayland-client.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
void onGlobal(void *data, wl_registry *, uint32_t, const char *interface, uint32_t)
|
||||
{
|
||||
static_cast<QSet<QByteArray> *>(data)->insert(QByteArray(interface));
|
||||
}
|
||||
|
||||
void onGlobalRemove(void *, wl_registry *, uint32_t)
|
||||
{
|
||||
}
|
||||
|
||||
const wl_registry_listener listener = {onGlobal, onGlobalRemove};
|
||||
|
||||
const QSet<QByteArray> &globals()
|
||||
{
|
||||
static QSet<QByteArray> names;
|
||||
static bool asked = false;
|
||||
if (asked) {
|
||||
return names;
|
||||
}
|
||||
asked = true;
|
||||
|
||||
auto *app = qGuiApp ? qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>() : nullptr;
|
||||
wl_display *display = app ? app->display() : nullptr;
|
||||
if (!display) {
|
||||
return names;
|
||||
}
|
||||
wl_event_queue *queue = wl_display_create_queue(display);
|
||||
auto *wrapped = static_cast<wl_display *>(wl_proxy_create_wrapper(display));
|
||||
wl_proxy_set_queue(reinterpret_cast<wl_proxy *>(wrapped), queue);
|
||||
wl_registry *registry = wl_display_get_registry(wrapped);
|
||||
wl_registry_add_listener(registry, &listener, &names);
|
||||
wl_display_roundtrip_queue(display, queue);
|
||||
wl_registry_destroy(registry);
|
||||
wl_proxy_wrapper_destroy(wrapped);
|
||||
wl_event_queue_destroy(queue);
|
||||
return names;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool Wayland::hasGlobal(const char *interface)
|
||||
{
|
||||
return globals().contains(QByteArray(interface));
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// What the compositor offers. Not every desktop has everything: GNOME has no
|
||||
// layer-shell (so no bubble) and no ext-idle-notify, and only compositors
|
||||
// whose lock screen is a program of its own have ext-session-lock.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Wayland
|
||||
{
|
||||
// Whether the compositor announces this interface, for example
|
||||
// "zwlr_layer_shell_v1". Asked once, on an event queue of its own, so Qt's
|
||||
// own handling of the connection is not disturbed.
|
||||
bool hasGlobal(const char *interface);
|
||||
} // namespace Wayland
|
||||
+1
-1
@@ -71,7 +71,7 @@ int main(int argc, char **argv)
|
||||
qSetMessagePattern(QStringLiteral("%{if-warning}warning: %{endif}%{if-critical}error: %{endif}%{message}"));
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription(QStringLiteral("Face unlock daemon for KDE Plasma"));
|
||||
parser.setApplicationDescription(QStringLiteral("Face unlock daemon"));
|
||||
parser.addHelpOption();
|
||||
parser.addVersionOption();
|
||||
const QCommandLineOption socketOpt(QStringLiteral("socket"), QStringLiteral("Listen here instead of the system socket."), QStringLiteral("path"),
|
||||
|
||||
+3
-3
@@ -4,9 +4,9 @@
|
||||
//
|
||||
// Adding a face is adding a way in, so it asks for the password first, the
|
||||
// same way a phone asks for its code before it sets up a face. The question
|
||||
// goes to the polkit agent of the person's own session (on Plasma, the
|
||||
// familiar password dialog), which is why the daemon never sees the
|
||||
// password.
|
||||
// goes to the polkit agent of the person's own session (the desktop's usual
|
||||
// password dialog), which is why the daemon never sees the password.
|
||||
// Hyprland and Niri have none of their own: one has to be running there.
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# face-unlock: face unlock for KDE Plasma
|
||||
# face-unlock: face unlock for Plasma, GNOME, Hyprland and Niri
|
||||
#
|
||||
# Look at the screen and it unlocks, the way a phone does. The lock screen,
|
||||
# sudo in a terminal and the admin password prompts can all take a face
|
||||
@@ -64,7 +64,7 @@ fu_do_setup() {
|
||||
fu_ensure_service || return 1
|
||||
|
||||
if [[ -z ${WAYLAND_DISPLAY:-} && -z ${DISPLAY:-} ]]; then
|
||||
fu_bad "$(fu_msg "Setting up a face needs the camera picture on screen. Run this inside your Plasma session.")"
|
||||
fu_bad "$(fu_msg "Setting up a face needs the camera picture on screen. Run this inside your desktop session.")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -102,6 +102,7 @@ fu_do_enable() {
|
||||
|
||||
if fu_agent_available; then
|
||||
fu_agent_enable || fu_bad "$(fu_msg "Could not start the lock screen agent.")"
|
||||
fu_agent_autostarts || fu_agent_hint
|
||||
else
|
||||
fu_note "$(fu_msg "No systemd user session, so the lock screen agent was not started.")"
|
||||
fi
|
||||
|
||||
@@ -31,6 +31,15 @@ FU_SYSCONFIG="${FU_SYSCONFIG:-/etc/${FU_NAME}/config}"
|
||||
FU_UNIT_SOCKET="face-unlockd.socket"
|
||||
FU_UNIT_AGENT="face-unlock-agent.service"
|
||||
|
||||
# The desktop this runs in: plasma, gnome, hyprland, niri or other.
|
||||
case ":${XDG_CURRENT_DESKTOP:-}:" in
|
||||
*:KDE:*) FU_DESKTOP=plasma ;;
|
||||
*:GNOME:*) FU_DESKTOP=gnome ;;
|
||||
*:Hyprland:*) FU_DESKTOP=hyprland ;;
|
||||
*:niri:*) FU_DESKTOP=niri ;;
|
||||
*) FU_DESKTOP=other ;;
|
||||
esac
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Translations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
+18
-4
@@ -211,7 +211,7 @@ fu_value_label() {
|
||||
# fu_ui_status
|
||||
# Shared by the `status` subcommand and the menu header.
|
||||
fu_ui_status() {
|
||||
local names='' i camera
|
||||
local names='' i camera agent=yes
|
||||
|
||||
fu_config_load
|
||||
fu_status_load
|
||||
@@ -246,6 +246,9 @@ fu_ui_status() {
|
||||
_fu_row "$(fu_msg "Camera")" "$camera"
|
||||
|
||||
_fu_row "$(fu_msg "Lock screen")" "$(_fu_small_onoff "$( [[ $CFG_ENABLED == yes && $CFG_LOCK == yes ]] && echo yes || echo no)")"
|
||||
if [[ $CFG_ENABLED == yes && $CFG_LOCK == yes ]] && fu_agent_available && ! fu_agent_running; then
|
||||
agent=no
|
||||
fi
|
||||
_fu_row "$(fu_msg "sudo")" "$(_fu_small_onoff "$(fu_pam_enabled sudo && echo yes || echo no)")"
|
||||
_fu_row "$(fu_msg "Admin prompts")" "$(_fu_small_onoff "$(fu_pam_enabled polkit-1 && echo yes || echo no)")"
|
||||
_fu_row "$(fu_msg "Photo check")" "$(fu_value_label Liveness "$CFG_LIVENESS")"
|
||||
@@ -258,6 +261,10 @@ fu_ui_status() {
|
||||
if [[ $FU_ST_MODELS != true ]]; then
|
||||
printf '\n %s%s%s\n' "$FU_C_RED" "$(fu_msg "The recognition models are missing. Reinstall the package.")" "$FU_C_RESET"
|
||||
fi
|
||||
if [[ $agent == no ]]; then
|
||||
printf '\n %s%s%s\n' "$FU_C_YELLOW" "$(fu_msg "The lock screen agent is not running.")" "$FU_C_RESET"
|
||||
fu_agent_autostarts || fu_agent_hint
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -269,7 +276,8 @@ fu_ui_status() {
|
||||
# heading, with only the label after it
|
||||
# type bool, choice (steps through the choices) or camera
|
||||
# needs a bool setting this one does nothing without; it is dimmed while
|
||||
# that is off
|
||||
# that is off. Layers is no setting: whether the desktop can show
|
||||
# the bubble at all (GNOME cannot).
|
||||
FU_SETTINGS=(
|
||||
"group|Lock screen"
|
||||
"user|LockScreen|bool|yes|Unlock with your face"
|
||||
@@ -287,7 +295,7 @@ FU_SETTINGS=(
|
||||
"sys|Adapt|bool|yes|Learn from every unlock"
|
||||
"sys|SkipLidClosed|bool|yes|Not when the lid is closed"
|
||||
"group|Bubble"
|
||||
"user|Bubble|bool|yes|Show the bubble"
|
||||
"user|Bubble|bool|yes|Show the bubble||Layers"
|
||||
"user|BubbleStyle|choice|full|Style|full,minimal|Bubble"
|
||||
"user|AnimationSpeed|choice|normal|Animation speed|slow,normal,fast|Bubble"
|
||||
"user|BubbleForPrompts|bool|yes|Also for sudo and admin prompts||Bubble"
|
||||
@@ -296,12 +304,16 @@ FU_SETTINGS=(
|
||||
# fu_setting_help <Key> <value>
|
||||
# What a setting does, shown under the list for the selected one.
|
||||
fu_setting_help() {
|
||||
if [[ $FU_DESKTOP == gnome && $1 =~ ^(Bubble|BubbleStyle|AnimationSpeed|BubbleForPrompts)$ ]]; then
|
||||
fu_msg "GNOME does not let other programs show above its windows, so there is no bubble on GNOME."
|
||||
return
|
||||
fi
|
||||
case "$1:$2" in
|
||||
LockScreen:*) fu_msg "Unlocks the lock screen when it sees your face. Off: only your password works there." ;;
|
||||
ScanOnWake:*) fu_msg "Scans when you press a key or move the mouse on the lock screen, and when the computer wakes up." ;;
|
||||
ScanOnLock:*) fu_msg "Scans as soon as the screen locks. Off by default: if you lock it yourself, it would unlock again right away." ;;
|
||||
sudo:*) fu_msg "sudo takes your face instead of the password. No match: you type the password as usual." ;;
|
||||
polkit-1:*) fu_msg "The password windows of Plasma and apps, for example when you install software. No match: you type the password." ;;
|
||||
polkit-1:*) fu_msg "The password windows of your desktop and apps, for example when you install software. No match: you type the password." ;;
|
||||
Liveness:heavy) fu_msg "You have to blink or turn your head a little. This also stops printed photos." ;;
|
||||
Liveness:off) fu_msg "No check at all. Only for trying out a camera." ;;
|
||||
Liveness:*) fu_msg "Stops photos on a phone, a tablet or glossy paper. You do not have to blink. A matte printed photo can get through." ;;
|
||||
@@ -511,6 +523,8 @@ fu_ui_settings() {
|
||||
values[i]="$FU_SETTING_VALUE"
|
||||
current[${keys[i]}]="$FU_SETTING_VALUE"
|
||||
done
|
||||
current[Layers]=yes
|
||||
[[ $FU_DESKTOP == gnome ]] && current[Layers]=no current[Bubble]=no
|
||||
dirty=0
|
||||
fi
|
||||
|
||||
|
||||
@@ -117,6 +117,21 @@ fu_agent_enable() {
|
||||
systemctl --user enable --now "$FU_UNIT_AGENT" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# fu_agent_autostarts
|
||||
# Whether the agent's service starts with the session. It is wanted by
|
||||
# graphical-session.target, which Hyprland only reaches when it runs under
|
||||
# uwsm. Plasma, GNOME and niri-session always do.
|
||||
fu_agent_autostarts() {
|
||||
[[ $FU_DESKTOP != hyprland ]] || systemctl --user is-active --quiet graphical-session.target
|
||||
}
|
||||
|
||||
# fu_agent_hint
|
||||
fu_agent_hint() {
|
||||
# shellcheck disable=SC2088 # shown to the user, not a path to open
|
||||
fu_note "$(fu_msg "Hyprland does not start the lock screen agent by itself. Add this line to %s:" "~/.config/hypr/hyprland.conf")"
|
||||
fu_say " exec-once = systemctl --user start $FU_UNIT_AGENT"
|
||||
}
|
||||
|
||||
fu_agent_disable() {
|
||||
systemctl --user disable --now "$FU_UNIT_AGENT" > /dev/null 2>&1 || true
|
||||
}
|
||||
Reference in new issue
Block a user