// SPDX-License-Identifier: GPL-3.0-or-later // // face-unlock-agent: the part in the user's session. // // (no arguments) stay in the background: unlock the lock screen by face, // and show the bubble for every scan // --enroll open the window that sets up a face // --lock lock the screen: with face-unlock's own lock screen // where the lock screen is a program of its own (Hyprland, // Niri), else through logind with the desktop's // --demo play the bubble's animations once, for trying out a // style (--bubble-style minimal) and for screenshots // --choose-lock-screen // open the window that asks which lock screen to use // where it is a program of its own, and print the answer // --has-own-lock for the menu: exits 0 when this build has face-unlock's // own lock screen (see SessionLock::built) #include "agentsocket.h" #include "bubblecontroller.h" #include "bubbleservice.h" #include "bubblewindow.h" #include "enrollcontroller.h" #include "lockcontroller.h" #include "lockpreview.h" #include "lockscreencontroller.h" #include "locktext.h" #include "sessionlock.h" #include "userconfig.h" #include "wallpaper.h" #include "wayland.h" #include "buildconfig.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { int runEnroll(QGuiApplication &app, const QString &name) { app.setQuitOnLastWindowClosed(true); EnrollController controller; controller.setName(name); QQmlApplicationEngine engine; KLocalization::setupLocalizedContext(&engine); engine.setInitialProperties({{QStringLiteral("controller"), QVariant::fromValue(&controller)}}); engine.loadFromModule(QStringLiteral("FaceUnlock"), QStringLiteral("Enroll")); if (engine.rootObjects().isEmpty()) { return 2; } int code = 1; QObject::connect(&controller, &EnrollController::completed, &app, [&code](bool ok) { code = ok ? 0 : 1; QCoreApplication::quit(); }); app.exec(); return controller.state() == u"done" ? 0 : code; } // The window of --choose-lock-screen. Prints "own" or "yours" for the menu, // which carries it out. int runChooseLockScreen(QGuiApplication &app) { app.setQuitOnLastWindowClosed(true); UserConfig config; // Both choices show this screen as it would look: face-unlock's lock // screen as it is, with a scan going on, and the user's own rebuilt. const QScreen *screen = QGuiApplication::primaryScreen(); const QString output = screen ? screen->name() : QString(); const QSize size = screen ? screen->size() : QSize(1920, 1080); const QUrl desktop = Wallpaper::pick(Wallpaper::forLock({}), output); const QUrl ownWallpaper = config.lockWallpaper().isEmpty() ? desktop : Wallpaper::pick(Wallpaper::forLock(config.lockWallpaper()), output); const QString locker = LockPreview::locker(); LockScreenController lock; lock.setBlur(config.lockBlur()); BubbleController bubble(&config); bubble.scanStarted(); // A scan that goes on: the bubble closes by itself after a while. QTimer rescan; QObject::connect(&rescan, &QTimer::timeout, &bubble, &BubbleController::scanStarted); rescan.start(20000); QQmlApplicationEngine engine; KLocalization::setupLocalizedContext(&engine); engine.setInitialProperties({{QStringLiteral("current"), config.lockScreenStyle()}, {QStringLiteral("locker"), locker}, {QStringLiteral("widgets"), LockPreview::widgets(locker, size, output, desktop)}, {QStringLiteral("screenSize"), size}, {QStringLiteral("ownWallpaper"), ownWallpaper}, {QStringLiteral("lock"), QVariant::fromValue(&lock)}, {QStringLiteral("bubble"), QVariant::fromValue(&bubble)}}); engine.loadFromModule(QStringLiteral("FaceUnlock"), QStringLiteral("LockChoice")); auto *window = engine.rootObjects().isEmpty() ? nullptr : qobject_cast(engine.rootObjects().constFirst()); if (!window) { return 2; } // Shown only now, at the size its texts need (see LockChoice.qml). window->show(); app.exec(); const QString answer = window->property("answer").toString(); if (answer.isEmpty()) { return 1; } std::printf("%s\n", qPrintable(answer)); return 0; } // Where --lock tells the command that ran it that the screen is locked, when // it forked to stay behind as the lock screen. -1: it did not. int readyFd = -1; void signalReady(bool ok) { if (readyFd >= 0) { if (ok) { [[maybe_unused]] const ssize_t n = write(readyFd, "1", 1); } close(readyFd); readyFd = -1; } } // Whether an agent listens on its socket. Asked before Qt is up, to decide // whether to fork. bool agentListening() { const char *runtime = getenv("XDG_RUNTIME_DIR"); if (!runtime) { return false; } sockaddr_un addr{}; addr.sun_family = AF_UNIX; const int len = snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/face-unlock/agent.socket", runtime); if (len <= 0 || size_t(len) >= sizeof(addr.sun_path)) { return false; } const int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); const bool ok = fd >= 0 && connect(fd, reinterpret_cast(&addr), sizeof(addr)) == 0; if (fd >= 0) { close(fd); } return ok; } // With no agent running, --lock stays behind as the lock screen until it is // unlocked. The command that asked for it returns as soon as the screen is // locked, the way swaylock -f does: an idle daemon locking before sleep // waits for that, and not a moment longer. void forkForLock(int argc, char **argv) { bool lock = false; for (int i = 1; i < argc; ++i) { lock = lock || std::strcmp(argv[i], "--lock") == 0; } int fds[2]; if (!lock || agentListening() || pipe(fds) != 0) { return; } const pid_t pid = fork(); if (pid < 0) { close(fds[0]); close(fds[1]); return; } if (pid > 0) { close(fds[1]); char c = 0; const bool locked = read(fds[0], &c, 1) == 1; _exit(locked ? 0 : 1); } close(fds[0]); setsid(); readyFd = fds[1]; } // Plasma and GNOME lock with their own lock screen when logind asks them to, // as `loginctl lock-session` does. int lockThroughLogind() { const QString id = qEnvironmentVariable("XDG_SESSION_ID"); QDBusMessage call = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.login1"), QStringLiteral("/org/freedesktop/login1"), QStringLiteral("org.freedesktop.login1.Manager"), QStringLiteral("LockSession")); call << (id.isEmpty() ? QStringLiteral("auto") : id); const QDBusMessage reply = QDBusConnection::systemBus().call(call); if (reply.type() == QDBusMessage::ErrorMessage) { qWarning("logind would not lock: %s", qPrintable(reply.errorMessage())); return 1; } return 0; } // The whole life of a bubble, twice: a face that is recognised after a blink, // then one that is not. Last, a camera another program has. void scheduleDemo(BubbleController *bubble) { const QList>> steps = { {300, [bubble] { bubble->scanStarted(); }}, {900, [bubble] { bubble->faceFound(); }}, {1900, [bubble] { bubble->hint(QStringLiteral("blink")); }}, {3000, [bubble] { bubble->succeeded(); }}, {5600, [bubble] { bubble->scanStarted(); }}, {6200, [bubble] { bubble->faceFound(); }}, {7800, [bubble] { bubble->failed(QStringLiteral("mismatch")); }}, {11000, [bubble] { bubble->failed(QStringLiteral("camera-busy")); }}, {14000, [] { QCoreApplication::quit(); }}, }; for (const auto &[at, what] : steps) { QTimer::singleShot(at, bubble, what); } } } // namespace int main(int argc, char **argv) { if (argc == 2 && std::strcmp(argv[1], "--has-own-lock") == 0) { return SessionLock::built ? 0 : 1; } forkForLock(argc, argv); QGuiApplication app(argc, argv); app.setApplicationName(QStringLiteral("face-unlock-agent")); app.setApplicationVersion(QStringLiteral(FU_VERSION)); app.setDesktopFileName(QStringLiteral("io.github.loonixtools.face-unlock-agent")); KLocalizedString::setApplicationDomain(FU_NAME); QCommandLineParser parser; 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.")); const QCommandLineOption nameOpt(QStringLiteral("name"), i18n("What to call the new face."), QStringLiteral("name")); const QCommandLineOption lockOpt(QStringLiteral("lock"), i18n("Lock the screen.")); const QCommandLineOption demoOpt(QStringLiteral("demo"), i18n("Play the bubble's animations once.")); // Not "--style": QGuiApplication takes that one for itself. const QCommandLineOption styleOpt(QStringLiteral("bubble-style"), i18n("Bubble style for the demo: full or minimal."), QStringLiteral("style")); const QCommandLineOption chooseOpt(QStringLiteral("choose-lock-screen"), i18n("Ask which lock screen to use, and print the answer.")); parser.addOptions({enrollOpt, nameOpt, lockOpt, demoOpt, styleOpt, chooseOpt}); parser.process(app); if (parser.isSet(chooseOpt)) { return runChooseLockScreen(app); } if (parser.isSet(enrollOpt)) { QString name = parser.value(nameOpt); if (name.isEmpty()) { name = qEnvironmentVariable("USER"); } return runEnroll(app, name); } const bool lockNow = parser.isSet(lockOpt); if (lockNow && !SessionLock::available()) { const int rc = lockThroughLogind(); signalReady(rc == 0); return rc; } app.setQuitOnLastWindowClosed(false); QQmlEngine engine; KLocalization::setupLocalizedContext(&engine); UserConfig config; if (parser.isSet(styleOpt)) { config.overrideStyle(parser.value(styleOpt)); } BubbleController bubble(&config); // 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: there // face-unlock's GNOME Shell extension draws it, from what this tells it. std::unique_ptr window; if (Wayland::hasGlobal("zwlr_layer_shell_v1")) { window = std::make_unique(&engine, &bubble); } if (parser.isSet(demoOpt)) { if (!window) { qWarning("this desktop has no layer-shell; on GNOME the extension draws the bubble"); return 1; } scheduleDemo(&bubble); return app.exec(); } AgentSocket socket; if (AgentSocket::running()) { if (lockNow) { return AgentSocket::requestLock() ? 0 : 1; } qInfo("an agent is already running in this session"); return 0; } socket.listen(); QObject::connect(&socket, &AgentSocket::scanEvent, &bubble, &BubbleController::daemonEvent); std::unique_ptr service; if (!window) { service = std::make_unique(&bubble); } LockScreenController screen; std::unique_ptr sessionLock; if (SessionLock::available()) { sessionLock = std::make_unique(&engine, &bubble, &screen, &config); SessionLock *lock = sessionLock.get(); QObject::connect(&socket, &AgentSocket::lockRequested, lock, [lock, &socket] { lock->lock(); if (lock->isConfirmed()) { socket.confirmLock(); } }); QObject::connect(lock, &SessionLock::confirmed, &socket, &AgentSocket::confirmLock); } LockController lock(&bubble, &config, sessionLock.get(), &screen); // hyprlock covers the bubble: it can show it as a line of text instead. std::unique_ptr text; if (sessionLock && !lockNow) { text = std::make_unique(&bubble, &config); } if (lockNow) { // No agent was running, so this one is only here for the lock. It // goes once the lock is gone and the bubble has finished. QObject::connect(sessionLock.get(), &SessionLock::lockedChanged, &app, [&bubble](bool locked) { if (locked) { return; } if (!bubble.shown()) { QCoreApplication::quit(); } QObject::connect(&bubble, &BubbleController::shownChanged, qApp, [&bubble] { if (!bubble.shown()) { QCoreApplication::quit(); } }); QTimer::singleShot(5000, qApp, &QCoreApplication::quit); }); QObject::connect(sessionLock.get(), &SessionLock::confirmed, &app, [] { signalReady(true); }); sessionLock->lock(); if (!sessionLock->isLocked()) { signalReady(false); return 1; } } else if (sessionLock && QFile::exists(SessionLock::markerPath())) { // The last agent went away with the screen locked, and the compositor // kept it locked. Take the lock back, so it can be opened again. sessionLock->lock(); } return app.exec(); }