From 3573663ab648ff793e6ad545b63eaa5979633e51 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 20 Jun 2024 15:39:49 -0700 Subject: [PATCH] service/greetd: add greetd service --- CMakeLists.txt | 2 + src/core/generation.cpp | 6 + src/core/generation.hpp | 10 +- src/services/CMakeLists.txt | 4 + src/services/greetd/CMakeLists.txt | 16 ++ src/services/greetd/connection.cpp | 263 +++++++++++++++++++++++++++++ src/services/greetd/connection.hpp | 74 ++++++++ src/services/greetd/module.md | 7 + src/services/greetd/qml.cpp | 46 +++++ src/services/greetd/qml.hpp | 95 +++++++++++ src/services/pam/CMakeLists.txt | 3 +- 11 files changed, 522 insertions(+), 4 deletions(-) create mode 100644 src/services/greetd/CMakeLists.txt create mode 100644 src/services/greetd/connection.cpp create mode 100644 src/services/greetd/connection.hpp create mode 100644 src/services/greetd/module.md create mode 100644 src/services/greetd/qml.cpp create mode 100644 src/services/greetd/qml.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 606256b..4865490 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) option(SERVICE_PIPEWIRE "PipeWire service" ON) option(SERVICE_MPRIS "Mpris service" ON) option(SERVICE_PAM "Pam service" ON) +option(SERVICE_GREETD "Greet service" ON) message(STATUS "Quickshell configuration") message(STATUS " Jemalloc: ${USE_JEMALLOC}") @@ -41,6 +42,7 @@ message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") message(STATUS " Mpris: ${SERVICE_MPRIS}") message(STATUS " Pam: ${SERVICE_PAM}") +message(STATUS " Greetd: ${SERVICE_GREETD}") message(STATUS " Hyprland: ${HYPRLAND}") if (HYPRLAND) message(STATUS " IPC: ${HYPRLAND_IPC}") diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 0a62432..2ca7a77 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -302,6 +302,12 @@ void EngineGeneration::assignIncubationController() { this->engine->setIncubationController(controller); } +EngineGeneration* EngineGeneration::currentGeneration() { + if (g_generations.size() == 1) { + return *g_generations.begin(); + } else return nullptr; +} + EngineGeneration* EngineGeneration::findEngineGeneration(QQmlEngine* engine) { return g_generations.value(engine); } diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 2f2fc5f..9bcb8b6 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -36,6 +36,10 @@ public: static EngineGeneration* findEngineGeneration(QQmlEngine* engine); static EngineGeneration* findObjectGeneration(QObject* object); + // Returns the current generation if there is only one generation, + // otherwise null. + static EngineGeneration* currentGeneration(); + RootWrapper* wrapper = nullptr; QDir rootPath; QmlScanner scanner; @@ -57,12 +61,14 @@ signals: void filesChanged(); void reloadFinished(); +public slots: + void quit(); + void exit(int code); + private slots: void onFileChanged(const QString& name); void onDirectoryChanged(); void incubationControllerDestroyed(); - void quit(); - void exit(int code); private: void postReload(); diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt index e8c05f4..8005f07 100644 --- a/src/services/CMakeLists.txt +++ b/src/services/CMakeLists.txt @@ -13,3 +13,7 @@ endif() if (SERVICE_PAM) add_subdirectory(pam) endif() + +if (SERVICE_GREETD) + add_subdirectory(greetd) +endif() diff --git a/src/services/greetd/CMakeLists.txt b/src/services/greetd/CMakeLists.txt new file mode 100644 index 0000000..3c8fcf3 --- /dev/null +++ b/src/services/greetd/CMakeLists.txt @@ -0,0 +1,16 @@ +qt_add_library(quickshell-service-greetd STATIC + qml.cpp + connection.cpp +) + +qt_add_qml_module(quickshell-service-greetd + URI Quickshell.Services.Greetd + VERSION 0.1 +) + +target_link_libraries(quickshell-service-greetd PRIVATE ${QT_DEPS}) + +qs_pch(quickshell-service-greetd) +qs_pch(quickshell-service-greetdplugin) + +target_link_libraries(quickshell PRIVATE quickshell-service-greetdplugin) diff --git a/src/services/greetd/connection.cpp b/src/services/greetd/connection.cpp new file mode 100644 index 0000000..4b59d79 --- /dev/null +++ b/src/services/greetd/connection.cpp @@ -0,0 +1,263 @@ +#include "connection.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/generation.hpp" + +Q_LOGGING_CATEGORY(logGreetd, "quickshell.service.greetd"); + +QString GreetdState::toString(GreetdState::Enum value) { + switch (value) { + case GreetdState::Inactive: return "Inactive"; + case GreetdState::Authenticating: return "Authenticating"; + case GreetdState::ReadyToLaunch: return "Ready to Launch"; + case GreetdState::Launching: return "Launching"; + case GreetdState::Launched: return "Launched"; + default: return "Invalid"; + } +} + +GreetdConnection::GreetdConnection() { + auto socket = qEnvironmentVariable("GREETD_SOCK"); + + if (socket.isEmpty()) { + this->mAvailable = false; + return; + } + + this->mAvailable = true; + + // clang-format off + QObject::connect(&this->socket, &QLocalSocket::connected, this, &GreetdConnection::onSocketConnected); + QObject::connect(&this->socket, &QLocalSocket::readyRead, this, &GreetdConnection::onSocketReady); + // clang-format on + + this->socket.connectToServer(socket, QLocalSocket::ReadWrite); +} + +void GreetdConnection::createSession(QString user) { + if (!this->mAvailable) { + qCCritical(logGreetd) << "Greetd is not available."; + return; + } + + if (user != this->mUser) { + this->mUser = std::move(user); + emit this->userChanged(); + } + + this->setActive(true); +} + +void GreetdConnection::cancelSession() { this->setActive(false); } + +void GreetdConnection::respond(QString response) { + if (!this->mResponseRequired) { + qCCritical(logGreetd) << "Cannot respond to greetd as a response is not currently required."; + return; + } + + this->sendRequest({ + {"type", "post_auth_message_response"}, + {"response", response}, + }); + + this->mResponseRequired = false; +} + +void GreetdConnection::launch( + const QList& command, + const QList& environment, + bool exit +) { + if (this->mState != GreetdState::ReadyToLaunch) { + qCCritical(logGreetd) << "Cannot call launch() as state is not currently ReadyToLaunch."; + return; + } + + this->mState = GreetdState::Launching; + this->mExitAfterLaunch = exit; + + this->sendRequest({ + {"type", "start_session"}, + {"cmd", QJsonArray::fromStringList(command)}, + {"env", QJsonArray::fromStringList(environment)}, + }); +} + +bool GreetdConnection::isAvailable() const { return this->mAvailable; } +GreetdState::Enum GreetdConnection::state() const { return this->mState; } + +void GreetdConnection::setActive(bool active) { + if (this->socket.state() == QLocalSocket::ConnectedState) { + this->mTargetActive = active; + if (active == (this->mState != GreetdState::Inactive)) return; + + if (active) { + if (this->mUser.isEmpty()) { + qCCritical(logGreetd) << "Cannot activate greetd with unset user."; + this->setActive(false); + return; + } + + this->sendRequest({ + {"type", "create_session"}, + {"username", this->mUser}, + }); + + this->mState = GreetdState::Authenticating; + emit this->stateChanged(); + } else { + this->sendRequest({ + {"type", "cancel_session"}, + }); + + this->setInactive(); + } + } else { + if (active != this->mTargetActive) { + this->mTargetActive = active; + } + } +} + +void GreetdConnection::setInactive() { + this->mTargetActive = false; + this->mResponseRequired = false; + this->mState = GreetdState::Inactive; + emit this->stateChanged(); +} + +QString GreetdConnection::user() const { return this->mUser; } + +void GreetdConnection::onSocketConnected() { + qCDebug(logGreetd) << "Connected to greetd socket."; + + if (this->mTargetActive) { + this->setActive(true); + } +} + +void GreetdConnection::onSocketError(QLocalSocket::LocalSocketError error) { + qCCritical(logGreetd) << "Greetd socket encountered an error and cannot continue:" << error; + + this->mAvailable = false; + this->setActive(false); +} + +void GreetdConnection::onSocketReady() { + qint32 length = 0; + + this->socket.read( + reinterpret_cast(&length), // NOLINT + sizeof(qint32) + ); + + auto text = this->socket.read(length); + auto json = QJsonDocument::fromJson(text).object(); + auto type = json.value("type").toString(); + + qCDebug(logGreetd).noquote() << "Received greetd response:" << text; + + if (type == "success") { + switch (this->mState) { + case GreetdState::Authenticating: + qCDebug(logGreetd) << "Authentication complete."; + this->mState = GreetdState::ReadyToLaunch; + emit this->stateChanged(); + emit this->readyToLaunch(); + break; + case GreetdState::Launching: + qCDebug(logGreetd) << "Target session set successfully."; + this->mState = GreetdState::Launched; + emit this->stateChanged(); + emit this->launched(); + + if (this->mExitAfterLaunch) { + qCDebug(logGreetd) << "Quitting."; + EngineGeneration::currentGeneration()->quit(); + } + + break; + default: goto unexpected; + } + } else if (type == "error") { + auto errorType = json.value("error_type").toString(); + auto desc = json.value("description").toString(); + + // Special case this error in case a session was already running. + // This cancels and restarts the session. + if (errorType == "error" && desc == "a session is already being configured") { + qCDebug(logGreetd + ) << "A session was already in progress, cancelling it and starting a new one."; + this->setActive(false); + this->setActive(true); + return; + } + + if (errorType == "auth_error") { + emit this->authFailure(desc); + this->setActive(false); + } else if (errorType == "error") { + qCWarning(logGreetd) << "Greetd error occurred" << desc; + emit this->error(desc); + } else goto unexpected; + + // errors terminate the session + this->setInactive(); + } else if (type == "auth_message") { + auto message = json.value("auth_message").toString(); + auto type = json.value("auth_message_type").toString(); + auto error = type == "error"; + auto responseRequired = type == "visible" || type == "secret"; + auto echoResponse = type != "secret"; + + this->mResponseRequired = responseRequired; + emit this->authMessage(message, error, responseRequired, echoResponse); + } else goto unexpected; + + return; +unexpected: + qCCritical(logGreetd) << "Received unexpected greetd response" << text; + this->setActive(false); +} + +void GreetdConnection::sendRequest(const QJsonObject& json) { + auto text = QJsonDocument(json).toJson(QJsonDocument::Compact); + auto length = static_cast(text.length()); + + if (logGreetd().isDebugEnabled()) { + auto debugJson = json; + + if (json.value("type").toString() == "post_auth_message_response") { + debugJson["response"] = ""; + } + + qCDebug(logGreetd).noquote() << "Sending greetd request:" + << QJsonDocument(debugJson).toJson(QJsonDocument::Compact); + } + + this->socket.write( + reinterpret_cast(&length), // NOLINT + sizeof(qint32) + ); + + this->socket.write(text); + this->socket.flush(); +} + +GreetdConnection* GreetdConnection::instance() { + static auto* instance = new GreetdConnection(); // NOLINT + return instance; +} diff --git a/src/services/greetd/connection.hpp b/src/services/greetd/connection.hpp new file mode 100644 index 0000000..76b7514 --- /dev/null +++ b/src/services/greetd/connection.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class GreetdState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum { + Inactive = 0, + Authenticating = 1, + ReadyToLaunch = 2, + Launching = 3, + Launched = 4, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(GreetdState::Enum value); +}; + +class GreetdConnection: public QObject { + Q_OBJECT; + +public: + void createSession(QString user); + void cancelSession(); + void respond(QString response); + void launch(const QList& command, const QList& environment, bool exit); + + [[nodiscard]] bool isAvailable() const; + [[nodiscard]] GreetdState::Enum state() const; + + [[nodiscard]] QString user() const; + + static GreetdConnection* instance(); + +signals: + void authMessage(QString message, bool error, bool responseRequired, bool echoResponse); + void authFailure(QString message); + void readyToLaunch(); + void launched(); + void error(QString error); + + void stateChanged(); + void userChanged(); + +private slots: + void onSocketConnected(); + void onSocketError(QLocalSocket::LocalSocketError error); + void onSocketReady(); + +private: + explicit GreetdConnection(); + + void sendRequest(const QJsonObject& json); + void setActive(bool active); + void setInactive(); + + bool mAvailable = false; + GreetdState::Enum mState = GreetdState::Inactive; + bool mTargetActive = false; + bool mExitAfterLaunch = false; + QString mMessage; + bool mResponseRequired = false; + QString mUser; + QLocalSocket socket; +}; diff --git a/src/services/greetd/module.md b/src/services/greetd/module.md new file mode 100644 index 0000000..a3ec540 --- /dev/null +++ b/src/services/greetd/module.md @@ -0,0 +1,7 @@ +name = "Quickshell.Services.Greetd" +description = "Greetd integration" +headers = [ + "qml.hpp", + "connection.hpp", +] +----- diff --git a/src/services/greetd/qml.cpp b/src/services/greetd/qml.cpp new file mode 100644 index 0000000..faebaa1 --- /dev/null +++ b/src/services/greetd/qml.cpp @@ -0,0 +1,46 @@ +#include "qml.hpp" +#include + +#include +#include + +#include "connection.hpp" + +Greetd::Greetd(QObject* parent): QObject(parent) { + auto* connection = GreetdConnection::instance(); + + QObject::connect(connection, &GreetdConnection::authMessage, this, &Greetd::authMessage); + QObject::connect(connection, &GreetdConnection::authFailure, this, &Greetd::authFailure); + QObject::connect(connection, &GreetdConnection::readyToLaunch, this, &Greetd::readyToLaunch); + QObject::connect(connection, &GreetdConnection::launched, this, &Greetd::launched); + QObject::connect(connection, &GreetdConnection::error, this, &Greetd::error); + + QObject::connect(connection, &GreetdConnection::stateChanged, this, &Greetd::stateChanged); + QObject::connect(connection, &GreetdConnection::userChanged, this, &Greetd::userChanged); +} + +void Greetd::createSession(QString user) { + GreetdConnection::instance()->createSession(std::move(user)); +} + +void Greetd::cancelSession() { GreetdConnection::instance()->cancelSession(); } + +void Greetd::respond(QString response) { + GreetdConnection::instance()->respond(std::move(response)); +} + +void Greetd::launch(const QList& command) { + GreetdConnection::instance()->launch(command, {}, true); +} + +void Greetd::launch(const QList& command, const QList& environment) { + GreetdConnection::instance()->launch(command, environment, true); +} + +void Greetd::launch(const QList& command, const QList& environment, bool quit) { + GreetdConnection::instance()->launch(command, environment, quit); +} + +bool Greetd::isAvailable() { return GreetdConnection::instance()->isAvailable(); } +GreetdState::Enum Greetd::state() { return GreetdConnection::instance()->state(); } +QString Greetd::user() { return GreetdConnection::instance()->user(); } diff --git a/src/services/greetd/qml.hpp b/src/services/greetd/qml.hpp new file mode 100644 index 0000000..b03d181 --- /dev/null +++ b/src/services/greetd/qml.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include + +#include "connection.hpp" + +/// This object provides access to a running greetd instance if present. +/// With it you can authenticate a user and launch a session. +/// +/// See [the greetd wiki] for instructions on how to set up a graphical greeter. +/// +/// [the greetd wiki]: https://man.sr.ht/~kennylevinsen/greetd/#setting-up-greetd-with-gtkgreet +class Greetd: public QObject { + Q_OBJECT; + /// If the greetd socket is available. + Q_PROPERTY(bool available READ isAvailable CONSTANT); + /// The current state of the greetd connection. + Q_PROPERTY(GreetdState::Enum state READ state NOTIFY stateChanged); + /// The currently authenticating user. + Q_PROPERTY(QString user READ user NOTIFY userChanged); + QML_ELEMENT; + QML_SINGLETON; + +public: + explicit Greetd(QObject* parent = nullptr); + + /// Create a greetd session for the given user. + Q_INVOKABLE static void createSession(QString user); + /// Cancel the active greetd session. + Q_INVOKABLE static void cancelSession(); + /// Respond to an authentication message. + /// + /// May only be called in response to an `authMessage` with responseRequired set to true. + Q_INVOKABLE static void respond(QString response); + + // docgen currently can't handle default params + + // clang-format off + /// Launch the session, exiting quickshell. + /// `readyToLaunch` must be true to call this function. + Q_INVOKABLE static void launch(const QList& command); + /// Launch the session, exiting quickshell. + /// `readyToLaunch` must be true to call this function. + Q_INVOKABLE static void launch(const QList& command, const QList& environment); + /// Launch the session, exiting quickshell if `quit` is true. + /// `readyToLaunch` must be true to call this function. + /// + /// The `launched` signal can be used to perform an action after greetd has acknowledged + /// the desired session. + /// + /// > [!WARNING] Note that greetd expects the greeter to terminate as soon as possible + /// > after setting a target session, and waiting too long may lead to unexpected behavior + /// > such as the greeter restarting. + /// > + /// > Performing animations and such should be done *before* calling `launch`. + Q_INVOKABLE static void launch(const QList& command, const QList& environment, bool quit); + // clang-format on + + [[nodiscard]] static bool isAvailable(); + [[nodiscard]] static GreetdState::Enum state(); + [[nodiscard]] static QString user(); + +signals: + /// An authentication message has been sent by greetd. + /// - `message` - the text of the message + /// - `error` - if the message should be displayed as an error + /// - `responseRequired` - if a response via `respond()` is required for this message + /// - `echoResponse` - if the response should be displayed in clear text to the user + /// + /// Note that `error` and `responseRequired` are mutually exclusive. + /// + /// Errors are sent through `authMessage` when they are recoverable, such as a fingerprint scanner + /// not being able to read a finger correctly, while definite failures such as a bad password are + /// sent through `authFailure`. + void authMessage(QString message, bool error, bool responseRequired, bool echoResponse); + /// Authentication has failed an the session has terminated. + /// + /// Usually this is something like a timeout or a failed password entry. + void authFailure(QString message); + /// Authentication has finished successfully and greetd can now launch a session. + void readyToLaunch(); + /// Greetd has acknowledged the launch request and the greeter should quit as soon as possible. + /// + /// This signal is sent right before quickshell exits automatically if the launch was not specifically + /// requested not to exit. You usually don't need to use this signal. + void launched(); + /// Greetd has encountered an error. + void error(QString error); + + void stateChanged(); + void userChanged(); +}; diff --git a/src/services/pam/CMakeLists.txt b/src/services/pam/CMakeLists.txt index 3de9b5d..fef2357 100644 --- a/src/services/pam/CMakeLists.txt +++ b/src/services/pam/CMakeLists.txt @@ -1,11 +1,10 @@ -#find_package(PAM REQUIRED) - qt_add_library(quickshell-service-pam STATIC qml.cpp conversation.cpp ipc.cpp subprocess.cpp ) + qt_add_qml_module(quickshell-service-pam URI Quickshell.Services.Pam VERSION 0.1