From 5699158bb172447fce0ace7c30b8f384f09ba0a7 Mon Sep 17 00:00:00 2001 From: Felitendo Date: Sat, 26 Sep 2026 19:36:17 +0200 Subject: [PATCH] feat: open gtklock from outside once it can --- README.md | 5 +++-- doc/face-unlock.1.scd | 16 ++++++++++------ src/agent/lockcontroller.cpp | 10 ++++++++++ src/agent/lockcontroller.h | 2 ++ src/agent/lockers.cpp | 17 +++++++++++++---- src/agent/lockers.h | 5 ++++- src/agent/lockwatcher.cpp | 8 +++++++- src/agent/lockwatcher.h | 15 ++++++++++----- tests/test_lockers.cpp | 2 ++ 9 files changed, 61 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index bf47570..38a5948 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,9 @@ switches it on; right after installing, log out and back in once. On Hyprland and Niri the lock screen is a program of your choice, and only it can open itself. So face-unlock goes into its password check, as with sudo: press Enter on the empty password field to scan. hyprlock and swaylock also -scan by themselves when you come back. Those lock screens cover the bubble, -which shows the tick once they are gone. +scan by themselves when you come back, and so does gtklock after 4.0.0 (the +first to open from outside). Those lock screens cover the bubble, which shows +the tick once they are gone. gtklock shows face unlock's messages. For the bubble on the lock screen, use face-unlock's own: `face-unlock lock`. It shows the wallpaper of your desktop (from swaybg, awww, hyprpaper or diff --git a/doc/face-unlock.1.scd b/doc/face-unlock.1.scd index ac105cc..da015d3 100644 --- a/doc/face-unlock.1.scd +++ b/doc/face-unlock.1.scd @@ -201,8 +201,9 @@ does not start a scan with every key. When the face matches, the agent unlocks the session the way its lock screen wants it: through logind, as *loginctl unlock-session* does, on Plasma and -GNOME, by itself on its own lock screen, and with SIGUSR1 for hyprlock and -swaylock. Any other lock screen opens itself, through PAM (see *DESKTOPS*). +GNOME, by itself on its own lock screen, and with SIGUSR1 for hyprlock, +swaylock and gtklock after 4.0.0. Any other lock screen opens itself, through +PAM (see *DESKTOPS*). Unlocked through logind, Plasma's lock screen cuts off its own password prompt and counts that as a wrong password. After a face unlock the daemon resets the @@ -241,10 +242,13 @@ so does the bubble, above the lock screen too. open itself. hyprlock asks PAM the moment it starts; that first time is skipped, so a screen locked on purpose does not open again at once. - hyprlock and swaylock can also be opened from outside: the agent finds - them among the user's processes (niri also sets *LockedHint*), scans - when somebody comes back, and opens them with SIGUSR1. Those lock - screens cover the bubble, which shows the tick once they are gone. + hyprlock, swaylock and gtklock (after 4.0.0) can also be opened from + outside: the agent finds them among the user's processes (niri also + sets *LockedHint*), scans when somebody comes back, and opens them with + SIGUSR1. It only does so once the lock screen handles SIGUSR1: before + it has locked, the signal would kill it. Those lock screens cover the + bubble, which shows the tick once they are gone. gtklock shows the + messages of the PAM module. For the bubble on the lock screen there is face-unlock's own: *lock*. It shows the time, a password field (PAM service _face-unlock-lock_ diff --git a/src/agent/lockcontroller.cpp b/src/agent/lockcontroller.cpp index cb82f66..6f5a579 100644 --- a/src/agent/lockcontroller.cpp +++ b/src/agent/lockcontroller.cpp @@ -42,6 +42,14 @@ LockController::LockController(BubbleController *bubble, UserConfig *config, Ses }); connect(&m_input, &InputWatcher::input, this, &LockController::onResume); connect(&m_lock, &LockWatcher::lockedChanged, this, &LockController::onLockedChanged); + // A lock screen that is only starting cannot be opened yet: the scan + // that was due when it locked follows once it can. + connect(&m_lock, &LockWatcher::unlockable, this, [this] { + if (m_lockScanDue) { + m_lockScanDue = false; + startScan(QStringLiteral("lock")); + } + }); QDBusConnection::systemBus().connect(QStringLiteral("org.freedesktop.login1"), QStringLiteral("/org/freedesktop/login1"), @@ -58,6 +66,7 @@ void LockController::onLockedChanged(bool locked) } if (!locked) { m_locked = false; + m_lockScanDue = false; m_armTimer.stop(); m_input.stop(); if (m_scan) { @@ -90,6 +99,7 @@ void LockController::onLockedChanged(bool locked) if (m_config->scanOnLock()) { QTimer::singleShot(400, this, [this] { + m_lockScanDue = m_locked && !m_lock.canUnlock(); startScan(QStringLiteral("lock")); }); } else { diff --git a/src/agent/lockcontroller.h b/src/agent/lockcontroller.h index 1244550..d32bfd6 100644 --- a/src/agent/lockcontroller.h +++ b/src/agent/lockcontroller.h @@ -59,6 +59,8 @@ private: LockScreenController *m_screen; bool m_locked = false; bool m_stopped = false; + // Scan right after locking, once the lock screen can be opened. + bool m_lockScanDue = false; QElapsedTimer m_lockedFor; QPointer m_scan; QTimer m_armTimer; diff --git a/src/agent/lockers.cpp b/src/agent/lockers.cpp index d356695..62cba77 100644 --- a/src/agent/lockers.cpp +++ b/src/agent/lockers.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -14,7 +15,7 @@ namespace { bool isLocker(const QByteArray &name) { - return name == "hyprlock" || name == "swaylock"; + return name == "hyprlock" || name == "swaylock" || name == "gtklock"; } QByteArray readFile(const QString &path) @@ -33,9 +34,9 @@ pid_t parentOf(pid_t pid) // 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. +// of a lock screen (swaylock and gtklock check 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 findAll() { QList found; @@ -104,6 +105,14 @@ QList Lockers::find(const QByteArray &name) return found; } +bool Lockers::ready(int sig) +{ + const QList lockers = find(); + return std::any_of(lockers.cbegin(), lockers.cend(), [sig](pid_t pid) { + return catches(pid, sig); + }); +} + int Lockers::signal(int sig, const QByteArray &name) { int waiting = 0; diff --git a/src/agent/lockers.h b/src/agent/lockers.h index 393ddfd..7e78668 100644 --- a/src/agent/lockers.h +++ b/src/agent/lockers.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // // The lock screens that are programs of their own and take signals from -// outside: hyprlock and swaylock open on SIGUSR1. +// outside: hyprlock, swaylock and gtklock open on SIGUSR1. #pragma once @@ -19,4 +19,7 @@ QList find(const QByteArray &name = {}); // that, the signal kills it, and the compositor keeps the screen locked with // nobody to open it. int signal(int sig, const QByteArray &name = {}); +// Whether one of them handles sig: gtklock only opens on SIGUSR1 since 2025, +// and none does before it has locked. +bool ready(int sig); } diff --git a/src/agent/lockwatcher.cpp b/src/agent/lockwatcher.cpp index 2a59bdd..bb3cea7 100644 --- a/src/agent/lockwatcher.cpp +++ b/src/agent/lockwatcher.cpp @@ -160,7 +160,13 @@ void LockWatcher::onSessionProperties(const QString &interface, const QVariantMa 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) @@ -171,7 +177,7 @@ void LockWatcher::setOwnLock(SessionLock *lock) bool LockWatcher::canUnlock() const { - return (m_own && m_own->isLocked()) || !m_ownLockers || m_lockerRunning; + return (m_own && m_own->isLocked()) || !m_ownLockers || m_lockerReady; } void LockWatcher::update() diff --git a/src/agent/lockwatcher.h b/src/agent/lockwatcher.h index 07e72a7..024f75d 100644 --- a/src/agent/lockwatcher.h +++ b/src/agent/lockwatcher.h @@ -7,11 +7,12 @@ // 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). +// user's processes (hyprlock, swaylock, gtklock) 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. +// hyprlock, swaylock and gtklock do not listen to logind. They unlock on +// SIGUSR1. // face-unlock's own lock screen (SessionLock) is simply told to. Any other // lock screen of that kind only opens itself: it gets the face through the // PAM module in its own stack (see src/lib/pam.sh), when Enter is pressed. @@ -41,13 +42,16 @@ public: return m_locked; } // Whether unlock() can open the lock screen that is up: always where - // logind unlocks (Plasma, GNOME), only hyprlock and swaylock elsewhere. + // logind unlocks (Plasma, GNOME), elsewhere only hyprlock, swaylock and + // gtklock, once they take SIGUSR1. bool canUnlock() const; void unlock(); void setOwnLock(SessionLock *lock); Q_SIGNALS: void lockedChanged(bool locked); + // canUnlock() came true: the lock screen that is up now takes SIGUSR1. + void unlockable(); private Q_SLOTS: void onScreenSaver(bool active); @@ -63,6 +67,7 @@ private: bool m_screenSaver = false; bool m_lockedHint = false; bool m_lockerRunning = false; + bool m_lockerReady = false; bool m_locked = false; // The compositor's lock screen is a program of its own. bool m_ownLockers = false; diff --git a/tests/test_lockers.cpp b/tests/test_lockers.cpp index 811f851..c7825c9 100644 --- a/tests/test_lockers.cpp +++ b/tests/test_lockers.cpp @@ -95,6 +95,7 @@ int main(int, char **argv) pid_t pid = fakeHyprlock(false, &from); check(Lockers::find("hyprlock") == QList{pid}, "finds the lock screen on this display"); check(Lockers::find("swaylock").isEmpty(), "and only by that name"); + check(!Lockers::ready(SIGUSR2), "not ready while it does not handle the signal"); check(Lockers::signal(SIGUSR2, "hyprlock") == 1, "one not ready yet"); usleep(200 * 1000); check(alive(pid), "and it was left alone, alive"); @@ -103,6 +104,7 @@ int main(int, char **argv) close(from); pid = fakeHyprlock(true, &from); + check(Lockers::ready(SIGUSR2), "ready once it does"); check(Lockers::signal(SIGUSR2, "hyprlock") == 0, "a ready one gets it"); check(next(from) == 's', "and handles it"); check(alive(pid), "and lives on");