service/greetd: add greetd service

This commit is contained in:
outfoxxed 2024-06-20 15:39:49 -07:00
parent 72956185bd
commit 3573663ab6
Signed by: outfoxxed
GPG Key ID: 4C88A185FB89301E
11 changed files with 522 additions and 4 deletions

View File

@ -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}")

View File

@ -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);
}

View File

@ -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();

View File

@ -13,3 +13,7 @@ endif()
if (SERVICE_PAM)
add_subdirectory(pam)
endif()
if (SERVICE_GREETD)
add_subdirectory(greetd)
endif()

View File

@ -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)

View File

@ -0,0 +1,263 @@
#include "connection.hpp"
#include <utility>
#include <qcontainerfwd.h>
#include <qjsonarray.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qlocalsocket.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtenvironmentvariables.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#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<QString>& command,
const QList<QString>& 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<char*>(&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<qint32>(text.length());
if (logGreetd().isDebugEnabled()) {
auto debugJson = json;
if (json.value("type").toString() == "post_auth_message_response") {
debugJson["response"] = "<CENSORED>";
}
qCDebug(logGreetd).noquote() << "Sending greetd request:"
<< QJsonDocument(debugJson).toJson(QJsonDocument::Compact);
}
this->socket.write(
reinterpret_cast<char*>(&length), // NOLINT
sizeof(qint32)
);
this->socket.write(text);
this->socket.flush();
}
GreetdConnection* GreetdConnection::instance() {
static auto* instance = new GreetdConnection(); // NOLINT
return instance;
}

View File

@ -0,0 +1,74 @@
#pragma once
#include <qcontainerfwd.h>
#include <qjsonobject.h>
#include <qlocalsocket.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
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<QString>& command, const QList<QString>& 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;
};

View File

@ -0,0 +1,7 @@
name = "Quickshell.Services.Greetd"
description = "Greetd integration"
headers = [
"qml.hpp",
"connection.hpp",
]
-----

View File

@ -0,0 +1,46 @@
#include "qml.hpp"
#include <utility>
#include <qcontainerfwd.h>
#include <qobject.h>
#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<QString>& command) {
GreetdConnection::instance()->launch(command, {}, true);
}
void Greetd::launch(const QList<QString>& command, const QList<QString>& environment) {
GreetdConnection::instance()->launch(command, environment, true);
}
void Greetd::launch(const QList<QString>& command, const QList<QString>& 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(); }

View File

@ -0,0 +1,95 @@
#pragma once
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#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<QString>& command);
/// Launch the session, exiting quickshell.
/// `readyToLaunch` must be true to call this function.
Q_INVOKABLE static void launch(const QList<QString>& command, const QList<QString>& 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<QString>& command, const QList<QString>& 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();
};

View File

@ -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