mirror of https://github.com/cutefishos/launcher
feat: add the cutefish-dm display manager
commit
6dd683619b
@ -0,0 +1 @@
|
||||
build/
|
||||
@ -0,0 +1,62 @@
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
project(cutefish-dm
|
||||
VERSION 0.1.0
|
||||
DESCRIPTION "Cutefish display manager"
|
||||
LANGUAGES CXX
|
||||
)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
option(BUILD_TESTING "Build the test suite" OFF)
|
||||
include(CTest)
|
||||
|
||||
find_package(Qt6 6.8 REQUIRED COMPONENTS Core DBus Network)
|
||||
find_library(PAM_LIBRARY pam)
|
||||
if(NOT PAM_LIBRARY)
|
||||
message(FATAL_ERROR "cutefish-dm needs libpam")
|
||||
endif()
|
||||
|
||||
set(DM_HELPER_PATH ${CMAKE_INSTALL_FULL_LIBEXECDIR}/cutefish-dm-helper)
|
||||
set(DM_SESSION_WRAPPER ${CMAKE_INSTALL_FULL_LIBEXECDIR}/cutefish-dm-session)
|
||||
|
||||
add_library(cutefish-dm-common STATIC common/wire.cpp)
|
||||
target_link_libraries(cutefish-dm-common PUBLIC Qt6::Core)
|
||||
|
||||
add_executable(cutefish-dm
|
||||
daemon/config.cpp
|
||||
daemon/daemon.cpp
|
||||
daemon/greeterserver.cpp
|
||||
daemon/helperprocess.cpp
|
||||
daemon/main.cpp
|
||||
daemon/sessions.cpp
|
||||
daemon/state.cpp
|
||||
daemon/terminal.cpp
|
||||
)
|
||||
target_compile_definitions(cutefish-dm PRIVATE
|
||||
DM_HELPER_PATH="${DM_HELPER_PATH}"
|
||||
DM_SESSION_WRAPPER="${DM_SESSION_WRAPPER}"
|
||||
)
|
||||
target_link_libraries(cutefish-dm PRIVATE cutefish-dm-common Qt6::Core Qt6::DBus Qt6::Network)
|
||||
|
||||
# No Qt event loop and no threads: PAM modules and the session's fork run here.
|
||||
add_executable(cutefish-dm-helper helper/main.cpp)
|
||||
target_link_libraries(cutefish-dm-helper PRIVATE cutefish-dm-common ${PAM_LIBRARY})
|
||||
|
||||
install(TARGETS cutefish-dm DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
install(TARGETS cutefish-dm-helper DESTINATION ${CMAKE_INSTALL_LIBEXECDIR})
|
||||
install(PROGRAMS data/cutefish-dm-session DESTINATION ${CMAKE_INSTALL_LIBEXECDIR})
|
||||
install(FILES data/cutefish-dm.service DESTINATION /usr/lib/systemd/system)
|
||||
install(FILES data/cutefish-dm.pam DESTINATION ${CMAKE_INSTALL_FULL_SYSCONFDIR}/pam.d
|
||||
RENAME cutefish-dm)
|
||||
install(FILES data/cutefish-dm-greeter.pam DESTINATION ${CMAKE_INSTALL_FULL_SYSCONFDIR}/pam.d
|
||||
RENAME cutefish-dm-greeter)
|
||||
install(FILES data/com.cutefish.DisplayManager.conf
|
||||
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/dbus-1/system.d)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
@ -0,0 +1,53 @@
|
||||
# cutefish-dm
|
||||
|
||||
The Cutefish display manager. It runs the login screen
|
||||
(`cutefish-compositor --login`) and the Wayland session the user picks, Cutefish
|
||||
or any other `wayland-sessions` entry such as GNOME or Plasma, with no black
|
||||
frame or console in between.
|
||||
|
||||
The login screen runs on VT1 and the session on VT2, so every handover is a switch
|
||||
between two VTs that are already drawing. On a single VT the console would flash
|
||||
whenever a compositor exits.
|
||||
|
||||
- `cutefish-dm` is the root daemon. The login screen talks to it over
|
||||
`GREETD_SOCK` with greetd's protocol; `start_session` may name a
|
||||
`wayland-sessions` id instead of a command.
|
||||
- `cutefish-dm-helper` runs PAM and the session for one login, like greetd's
|
||||
session worker and sddm-helper.
|
||||
- `cutefish-session` calls `com.cutefish.DisplayManager.SwitchToGreeter` before
|
||||
it logs out, so the login screen is already up when the session ends.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Qt 6.8 (Core, DBus, Network) and PAM; `cutefish-compositor` at runtime.
|
||||
|
||||
## Build
|
||||
|
||||
```shell
|
||||
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr
|
||||
cmake --build build
|
||||
sudo cmake --install build
|
||||
```
|
||||
|
||||
The Debian package creates the login screen's user; a source install needs it once:
|
||||
|
||||
```shell
|
||||
sudo adduser --system --group --home /var/lib/cutefish-greeter cutefish-greeter
|
||||
```
|
||||
|
||||
## Enable
|
||||
|
||||
```shell
|
||||
sudo systemctl disable sddm
|
||||
sudo systemctl enable cutefish-dm
|
||||
```
|
||||
|
||||
Optional settings go in `/etc/cutefish/dm.conf`; see `daemon/config.cpp`.
|
||||
|
||||
## Tests
|
||||
|
||||
```shell
|
||||
cmake -S . -B build -DBUILD_TESTING=ON
|
||||
cmake --build build
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
@ -0,0 +1,68 @@
|
||||
#include "wire.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <cstring>
|
||||
|
||||
namespace dm {
|
||||
|
||||
QByteArray encodeFrame(const QJsonObject& message)
|
||||
{
|
||||
const QByteArray payload = QJsonDocument(message).toJson(QJsonDocument::Compact);
|
||||
const quint32 length = quint32(payload.size());
|
||||
QByteArray frame(reinterpret_cast<const char*>(&length), sizeof(length));
|
||||
frame.append(payload);
|
||||
return frame;
|
||||
}
|
||||
|
||||
std::optional<QJsonObject> FrameReader::next()
|
||||
{
|
||||
if (m_failed || m_buffer.size() < qsizetype(sizeof(quint32)))
|
||||
return std::nullopt;
|
||||
quint32 length = 0;
|
||||
std::memcpy(&length, m_buffer.constData(), sizeof(length));
|
||||
if (length > kMaxFrameSize) {
|
||||
m_failed = true;
|
||||
return std::nullopt;
|
||||
}
|
||||
if (m_buffer.size() < qsizetype(sizeof(quint32) + length))
|
||||
return std::nullopt;
|
||||
QByteArray payload = m_buffer.mid(sizeof(quint32), length);
|
||||
// Only the frame taken out: the bytes after it are the next one.
|
||||
explicit_bzero(m_buffer.data(), sizeof(quint32) + length);
|
||||
m_buffer.remove(0, sizeof(quint32) + length);
|
||||
const auto document = QJsonDocument::fromJson(payload);
|
||||
wipe(payload);
|
||||
if (!document.isObject()) {
|
||||
m_failed = true;
|
||||
return std::nullopt;
|
||||
}
|
||||
return document.object();
|
||||
}
|
||||
|
||||
QByteArray encodeLine(const QJsonObject& message)
|
||||
{
|
||||
return QJsonDocument(message).toJson(QJsonDocument::Compact) + '\n';
|
||||
}
|
||||
|
||||
std::optional<QJsonObject> decodeLine(const QByteArray& line)
|
||||
{
|
||||
const auto document = QJsonDocument::fromJson(line.trimmed());
|
||||
if (!document.isObject())
|
||||
return std::nullopt;
|
||||
return document.object();
|
||||
}
|
||||
|
||||
void wipe(QByteArray& bytes)
|
||||
{
|
||||
if (!bytes.isEmpty())
|
||||
explicit_bzero(bytes.data(), size_t(bytes.size()));
|
||||
}
|
||||
|
||||
void wipe(QString& text)
|
||||
{
|
||||
if (!text.isEmpty())
|
||||
explicit_bzero(text.data(), size_t(text.size()) * sizeof(QChar));
|
||||
text.clear();
|
||||
}
|
||||
|
||||
} // namespace dm
|
||||
@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonObject>
|
||||
#include <optional>
|
||||
|
||||
namespace dm {
|
||||
|
||||
// greetd's framing (greetd_ipc::codec): native-endian u32 length, JSON object.
|
||||
constexpr quint32 kMaxFrameSize = 64 * 1024;
|
||||
|
||||
QByteArray encodeFrame(const QJsonObject& message);
|
||||
|
||||
// An oversized frame or one that is not a JSON object fails it for good.
|
||||
class FrameReader
|
||||
{
|
||||
public:
|
||||
void append(const QByteArray& bytes) { m_buffer.append(bytes); }
|
||||
std::optional<QJsonObject> next();
|
||||
bool failed() const { return m_failed; }
|
||||
|
||||
private:
|
||||
QByteArray m_buffer;
|
||||
bool m_failed = false;
|
||||
};
|
||||
|
||||
// The daemon and its helpers exchange one compact JSON object per line.
|
||||
QByteArray encodeLine(const QJsonObject& message);
|
||||
std::optional<QJsonObject> decodeLine(const QByteArray& line);
|
||||
|
||||
// Overwrites a buffer that held a password before it is released.
|
||||
void wipe(QByteArray& bytes);
|
||||
void wipe(QString& text);
|
||||
|
||||
} // namespace dm
|
||||
@ -0,0 +1,40 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QProcess>
|
||||
#include <QSettings>
|
||||
|
||||
namespace dm {
|
||||
|
||||
Config Config::load(const QString& path)
|
||||
{
|
||||
Config config;
|
||||
if (!QFileInfo::exists(path))
|
||||
return config;
|
||||
QSettings settings(path, QSettings::IniFormat);
|
||||
settings.beginGroup(QStringLiteral("Greeter"));
|
||||
config.greeterVt = settings.value(QStringLiteral("Vt"), config.greeterVt).toInt();
|
||||
config.greeterUser = settings.value(QStringLiteral("User"), config.greeterUser).toString();
|
||||
if (settings.contains(QStringLiteral("Command")))
|
||||
config.greeterCommand =
|
||||
QProcess::splitCommand(settings.value(QStringLiteral("Command")).toString());
|
||||
config.greeterService = settings.value(QStringLiteral("Service"), config.greeterService).toString();
|
||||
settings.endGroup();
|
||||
settings.beginGroup(QStringLiteral("Session"));
|
||||
config.sessionVt = settings.value(QStringLiteral("Vt"), config.sessionVt).toInt();
|
||||
config.sessionService = settings.value(QStringLiteral("Service"), config.sessionService).toString();
|
||||
if (settings.contains(QStringLiteral("Directories")))
|
||||
config.sessionDirectories =
|
||||
settings.value(QStringLiteral("Directories")).toString().split(QLatin1Char(':'), Qt::SkipEmptyParts);
|
||||
config.defaultSession =
|
||||
settings.value(QStringLiteral("Default"), config.defaultSession).toString();
|
||||
settings.endGroup();
|
||||
settings.beginGroup(QStringLiteral("Daemon"));
|
||||
config.runtimeDirectory =
|
||||
settings.value(QStringLiteral("RuntimeDirectory"), config.runtimeDirectory).toString();
|
||||
config.statePath = settings.value(QStringLiteral("StatePath"), config.statePath).toString();
|
||||
settings.endGroup();
|
||||
return config;
|
||||
}
|
||||
|
||||
} // namespace dm
|
||||
@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace dm {
|
||||
|
||||
struct Config {
|
||||
// Separate VTs keep a session's end off the screen; VT1 has the boot splash.
|
||||
int greeterVt = 1;
|
||||
int sessionVt = 2;
|
||||
QString greeterUser = QStringLiteral("cutefish-greeter");
|
||||
QStringList greeterCommand = {QStringLiteral("/usr/bin/cutefish-compositor"),
|
||||
QStringLiteral("--login")};
|
||||
QString greeterService = QStringLiteral("cutefish-dm-greeter");
|
||||
QString sessionService = QStringLiteral("cutefish-dm");
|
||||
QStringList sessionDirectories = {QStringLiteral("/usr/local/share/wayland-sessions"),
|
||||
QStringLiteral("/usr/share/wayland-sessions")};
|
||||
QString defaultSession = QStringLiteral("cutefish-wayland");
|
||||
QString statePath = QStringLiteral("/var/lib/cutefish-dm/state.conf");
|
||||
QString runtimeDirectory = QStringLiteral("/run/cutefish-dm");
|
||||
|
||||
// Defaults, overridden by whatever `path` sets.
|
||||
static Config load(const QString& path = QStringLiteral("/etc/cutefish/dm.conf"));
|
||||
};
|
||||
|
||||
} // namespace dm
|
||||
@ -0,0 +1,372 @@
|
||||
#include "daemon.h"
|
||||
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusConnectionInterface>
|
||||
#include <QDBusReply>
|
||||
#include <QDir>
|
||||
#include <QPointer>
|
||||
#include <QProcess>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <pwd.h>
|
||||
|
||||
#include "sessions.h"
|
||||
#include "terminal.h"
|
||||
|
||||
namespace dm {
|
||||
|
||||
namespace {
|
||||
|
||||
// How long the login screen or a session may take to open its display.
|
||||
constexpr int kControlTimeoutMs = 10000;
|
||||
constexpr int kControlPollMs = 20;
|
||||
// How long the login screen outlives the switch to a session: the kernel takes
|
||||
// its cursor down with it, and the session's replaces it once its displays settle.
|
||||
constexpr int kGreeterLingerMs = 3000;
|
||||
|
||||
std::optional<uid_t> uidOf(const QString& user)
|
||||
{
|
||||
const passwd* entry = getpwnam(user.toLocal8Bit().constData());
|
||||
if (!entry)
|
||||
return std::nullopt;
|
||||
return entry->pw_uid;
|
||||
}
|
||||
|
||||
// As GDM does: plymouth gives up the display with its splash left on screen.
|
||||
bool bootSplashRunning()
|
||||
{
|
||||
const QString plymouth = QStandardPaths::findExecutable(QStringLiteral("plymouth"));
|
||||
return !plymouth.isEmpty() && QProcess::execute(plymouth, {QStringLiteral("--ping")}) == 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Daemon::Daemon(Config config, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_config(std::move(config))
|
||||
, m_state(m_config.statePath)
|
||||
{
|
||||
m_greeterRestart.setSingleShot(true);
|
||||
connect(&m_greeterRestart, &QTimer::timeout, this, &Daemon::startGreeter);
|
||||
connect(&m_server, &GreeterServer::createSession, this, &Daemon::createSession);
|
||||
connect(&m_server, &GreeterServer::postAuthMessageResponse, this,
|
||||
&Daemon::postAuthMessageResponse);
|
||||
connect(&m_server, &GreeterServer::startSession, this, &Daemon::startSession);
|
||||
connect(&m_server, &GreeterServer::cancelSession, this, &Daemon::cancelSession);
|
||||
connect(&m_server, &GreeterServer::disconnected, this, [this] {
|
||||
if (m_login && !m_login->hasStarted())
|
||||
release(m_login);
|
||||
});
|
||||
}
|
||||
|
||||
Daemon::~Daemon()
|
||||
{
|
||||
m_stopping = true;
|
||||
replySwitchRequests(false);
|
||||
m_login.reset();
|
||||
m_session.reset();
|
||||
m_greeter.reset();
|
||||
restoreTextMode(m_config.sessionVt);
|
||||
restoreTextMode(m_config.greeterVt);
|
||||
}
|
||||
|
||||
bool Daemon::start()
|
||||
{
|
||||
const auto greeter = uidOf(m_config.greeterUser);
|
||||
if (!greeter) {
|
||||
qCritical() << "No login screen user" << m_config.greeterUser;
|
||||
return false;
|
||||
}
|
||||
m_greeterUid = *greeter;
|
||||
QDir().mkpath(m_config.runtimeDirectory);
|
||||
const QString socket = m_config.runtimeDirectory + QStringLiteral("/greeter.sock");
|
||||
if (!m_server.listen(socket, m_greeterUid)) {
|
||||
qCritical() << "Cannot listen on" << socket;
|
||||
return false;
|
||||
}
|
||||
auto bus = QDBusConnection::systemBus();
|
||||
if (!bus.registerService(QStringLiteral("com.cutefish.DisplayManager"))
|
||||
|| !bus.registerObject(QStringLiteral("/com/cutefish/DisplayManager"), this,
|
||||
QDBusConnection::ExportScriptableSlots))
|
||||
qWarning() << "Cannot register com.cutefish.DisplayManager; sessions will end on screen";
|
||||
if (bootSplashRunning()) {
|
||||
m_bootSplash = true;
|
||||
QProcess::execute(QStringLiteral("plymouth"), {QStringLiteral("deactivate")});
|
||||
}
|
||||
// A run that ended mid-login may have left it in KD_GRAPHICS with nobody on it.
|
||||
restoreTextMode(m_config.sessionVt);
|
||||
startGreeter();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Daemon::startGreeter()
|
||||
{
|
||||
if (m_greeter || m_stopping)
|
||||
return;
|
||||
// Until the compositor draws, VT1 keeps the previous frame, not the console.
|
||||
setGraphicsMode(m_config.greeterVt);
|
||||
m_greeterExpected = false;
|
||||
m_greeterSpent = false;
|
||||
m_greeter = std::make_unique<HelperProcess>();
|
||||
connect(m_greeter.get(), &HelperProcess::authenticated, this, [this] {
|
||||
m_greeter->start(m_config.greeterCommand,
|
||||
greeterEnvironment(m_config.greeterVt, m_server.path()));
|
||||
});
|
||||
connect(m_greeter.get(), &HelperProcess::started, this, [this](const QString& id) {
|
||||
m_greeterSessionId = id;
|
||||
if (std::exchange(m_bootSplash, false))
|
||||
QProcess::startDetached(QStringLiteral("plymouth"),
|
||||
{QStringLiteral("quit"), QStringLiteral("--retain-splash")});
|
||||
showGreeter();
|
||||
});
|
||||
connect(m_greeter.get(), &HelperProcess::failed, this, [](bool, const QString& message) {
|
||||
qWarning() << "Login screen:" << message;
|
||||
});
|
||||
connect(m_greeter.get(), &HelperProcess::finished, this, &Daemon::onGreeterFinished);
|
||||
m_greeter->login(m_config.greeterService, m_config.greeterUser, false, m_config.greeterVt);
|
||||
}
|
||||
|
||||
void Daemon::showGreeter()
|
||||
{
|
||||
if (!m_greeter || !m_greeter->hasStarted())
|
||||
return;
|
||||
// Created on the VT in front, as at boot, the login screen is active already.
|
||||
if (activeVt() == m_config.greeterVt) {
|
||||
replySwitchRequests(true);
|
||||
return;
|
||||
}
|
||||
HelperProcess* greeter = m_greeter.get();
|
||||
whenControlled(greeter, m_config.greeterVt, [this, greeter](bool controlled) {
|
||||
if (m_greeter.get() != greeter)
|
||||
return;
|
||||
if (controlled && activateSession(m_greeterSessionId)) {
|
||||
replySwitchRequests(true);
|
||||
return;
|
||||
}
|
||||
// Never switch to a VT nobody took; restart the login screen instead.
|
||||
qWarning() << "Cannot switch to the login screen on VT" << m_config.greeterVt;
|
||||
greeter->terminate();
|
||||
});
|
||||
}
|
||||
|
||||
void Daemon::stopGreeter()
|
||||
{
|
||||
m_greeterExpected = true;
|
||||
if (m_greeter)
|
||||
m_greeter->terminate();
|
||||
}
|
||||
|
||||
void Daemon::onGreeterFinished()
|
||||
{
|
||||
release(m_greeter);
|
||||
m_greeterSessionId.clear();
|
||||
restoreTextMode(m_config.greeterVt);
|
||||
if (m_stopping)
|
||||
return;
|
||||
if (std::exchange(m_restartGreeter, false)) {
|
||||
startGreeter();
|
||||
return;
|
||||
}
|
||||
if (m_greeterExpected) {
|
||||
m_greeterFailures = 0;
|
||||
return;
|
||||
}
|
||||
// Restart a login screen that died, with backoff, unless a session is up.
|
||||
replySwitchRequests(false);
|
||||
if (m_session)
|
||||
return;
|
||||
++m_greeterFailures;
|
||||
m_greeterRestart.start(qMin(m_greeterFailures, 5) * 1000);
|
||||
}
|
||||
|
||||
void Daemon::replySwitchRequests(bool switched)
|
||||
{
|
||||
auto bus = QDBusConnection::systemBus();
|
||||
for (const QDBusMessage& request : std::exchange(m_switchRequests, {})) {
|
||||
bus.send(switched ? request.createReply()
|
||||
: request.createErrorReply(QDBusError::Failed,
|
||||
QStringLiteral("The login screen is not up")));
|
||||
}
|
||||
}
|
||||
|
||||
void Daemon::SwitchToGreeter(const QDBusMessage& message)
|
||||
{
|
||||
auto bus = QDBusConnection::systemBus();
|
||||
// A caller that is gone before it is looked up is refused, not taken for root.
|
||||
const QDBusReply<uint> caller = bus.interface()->serviceUid(message.service());
|
||||
const auto owner = m_session ? uidOf(m_session->user()) : std::nullopt;
|
||||
if (!caller.isValid() || (caller.value() != 0 && (!owner || caller.value() != *owner))) {
|
||||
bus.send(message.createErrorReply(QDBusError::AccessDenied,
|
||||
QStringLiteral("Only the session's own user can end it")));
|
||||
return;
|
||||
}
|
||||
message.setDelayedReply(true);
|
||||
m_switchRequests.append(message);
|
||||
// A login screen that already handed a login over is replaced, not shown.
|
||||
if (m_greeter && m_greeterSpent) {
|
||||
m_restartGreeter = true;
|
||||
stopGreeter();
|
||||
} else if (m_greeter) {
|
||||
showGreeter();
|
||||
} else {
|
||||
startGreeter();
|
||||
}
|
||||
}
|
||||
|
||||
void Daemon::createSession(const QString& username)
|
||||
{
|
||||
if (m_login)
|
||||
release(m_login);
|
||||
m_login = std::make_unique<HelperProcess>();
|
||||
auto* login = m_login.get();
|
||||
connect(login, &HelperProcess::question, this, [this](const QString& style, const QString& text) {
|
||||
m_server.respond(GreeterServer::authMessage(style, text));
|
||||
});
|
||||
connect(login, &HelperProcess::authenticated, this, [this] {
|
||||
m_server.respond(GreeterServer::success());
|
||||
});
|
||||
connect(login, &HelperProcess::failed, this, [this, login](bool authentication,
|
||||
const QString& message) {
|
||||
m_server.respond(GreeterServer::error(authentication, message));
|
||||
if (m_login.get() == login)
|
||||
release(m_login);
|
||||
});
|
||||
connect(login, &HelperProcess::finished, this, [this, login] {
|
||||
if (m_login.get() == login)
|
||||
release(m_login);
|
||||
});
|
||||
login->login(m_config.sessionService, username, true, m_config.sessionVt);
|
||||
}
|
||||
|
||||
void Daemon::postAuthMessageResponse(const QJsonValue& response)
|
||||
{
|
||||
if (!m_login)
|
||||
m_server.respond(GreeterServer::error(false, QStringLiteral("No session is being created")));
|
||||
else
|
||||
m_login->answer(response);
|
||||
}
|
||||
|
||||
void Daemon::startSession(const QStringList& command, const QStringList& environment,
|
||||
const QString& session)
|
||||
{
|
||||
if (!m_login || !m_login->isAuthenticated()) {
|
||||
m_server.respond(GreeterServer::error(false, QStringLiteral("Not authenticated")));
|
||||
return;
|
||||
}
|
||||
PendingStart start;
|
||||
// The client's variables first: the session's own, set after, win.
|
||||
start.environment = environment;
|
||||
if (!session.isEmpty()) {
|
||||
const auto entry = findSession(m_config.sessionDirectories, session);
|
||||
if (!entry) {
|
||||
m_server.respond(GreeterServer::error(false, QStringLiteral("No such session")));
|
||||
return;
|
||||
}
|
||||
start.command = QStringList {QStringLiteral(DM_SESSION_WRAPPER)} + entry->exec;
|
||||
start.environment += sessionEnvironment(*entry, m_config.sessionVt);
|
||||
m_state.remember(m_login->user(), session);
|
||||
} else if (!command.isEmpty()) {
|
||||
start.command = QStringList {QStringLiteral(DM_SESSION_WRAPPER)} + command;
|
||||
start.environment += sessionEnvironment(SessionEntry {}, m_config.sessionVt);
|
||||
} else {
|
||||
m_server.respond(GreeterServer::error(false, QStringLiteral("Nothing to start")));
|
||||
return;
|
||||
}
|
||||
m_server.respond(GreeterServer::success());
|
||||
|
||||
// The previous session is still ending; the next one follows on its VT.
|
||||
if (m_session) {
|
||||
m_pendingStart = std::move(start);
|
||||
m_session->terminate();
|
||||
return;
|
||||
}
|
||||
launchSession(std::move(start));
|
||||
}
|
||||
|
||||
void Daemon::cancelSession()
|
||||
{
|
||||
if (m_login && !m_login->hasStarted())
|
||||
release(m_login);
|
||||
m_server.respond(GreeterServer::success());
|
||||
}
|
||||
|
||||
void Daemon::launchSession(PendingStart start)
|
||||
{
|
||||
if (!m_login)
|
||||
return;
|
||||
setGraphicsMode(m_config.sessionVt);
|
||||
m_greeterSpent = true;
|
||||
m_session = std::move(m_login);
|
||||
HelperProcess* session = m_session.get();
|
||||
connect(session, &HelperProcess::started, this, [this, session](const QString& id) {
|
||||
whenControlled(session, m_config.sessionVt, [this, session, id](bool controlled) {
|
||||
if (m_session.get() != session)
|
||||
return;
|
||||
// Never switch to a VT nobody took; end the session instead.
|
||||
if (!controlled || !activateSession(id)) {
|
||||
qWarning() << "Cannot switch to the session on VT" << m_config.sessionVt
|
||||
<< "- ending it";
|
||||
session->terminate();
|
||||
return;
|
||||
}
|
||||
QTimer::singleShot(kGreeterLingerMs, this,
|
||||
[this, greeter = QPointer<HelperProcess>(m_greeter.get())] {
|
||||
if (greeter && greeter == m_greeter.get())
|
||||
stopGreeter();
|
||||
});
|
||||
});
|
||||
});
|
||||
connect(m_session.get(), &HelperProcess::finished, this, &Daemon::onSessionFinished);
|
||||
m_session->start(start.command, start.environment);
|
||||
}
|
||||
|
||||
void Daemon::onSessionFinished()
|
||||
{
|
||||
release(m_session);
|
||||
restoreTextMode(m_config.sessionVt);
|
||||
if (m_stopping)
|
||||
return;
|
||||
// A login screen that went away meanwhile took its login along.
|
||||
if (auto pending = std::exchange(m_pendingStart, std::nullopt); pending && m_login) {
|
||||
launchSession(std::move(*pending));
|
||||
return;
|
||||
}
|
||||
// A session that failed before it was shown leaves a spent login screen.
|
||||
if (m_greeter && m_greeterSpent) {
|
||||
m_restartGreeter = true;
|
||||
stopGreeter();
|
||||
return;
|
||||
}
|
||||
// A crash, or another desktop's own logout: the login screen comes back.
|
||||
if (m_greeter)
|
||||
showGreeter();
|
||||
else
|
||||
startGreeter();
|
||||
}
|
||||
|
||||
void Daemon::whenControlled(HelperProcess* helper, int vt, std::function<void(bool)> then)
|
||||
{
|
||||
// The helper's child, so the wait ends with it.
|
||||
auto* timer = new QTimer(helper);
|
||||
auto waited = std::make_shared<int>(0);
|
||||
connect(timer, &QTimer::timeout, this, [timer, waited, vt, then = std::move(then)] {
|
||||
*waited += kControlPollMs;
|
||||
const bool controlled = isControlled(vt);
|
||||
if (!controlled && *waited < kControlTimeoutMs)
|
||||
return;
|
||||
timer->stop();
|
||||
timer->deleteLater();
|
||||
then(controlled);
|
||||
});
|
||||
timer->start(kControlPollMs);
|
||||
}
|
||||
|
||||
void Daemon::release(std::unique_ptr<HelperProcess>& helper)
|
||||
{
|
||||
if (!helper)
|
||||
return;
|
||||
helper->disconnect();
|
||||
helper.release()->deleteLater();
|
||||
}
|
||||
|
||||
} // namespace dm
|
||||
@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDBusMessage>
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QTimer>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "greeterserver.h"
|
||||
#include "helperprocess.h"
|
||||
#include "state.h"
|
||||
|
||||
namespace dm {
|
||||
|
||||
// Runs the login screen and the session on VTs of their own; see README.md.
|
||||
class Daemon : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_CLASSINFO("D-Bus Interface", "com.cutefish.DisplayManager")
|
||||
public:
|
||||
explicit Daemon(Config config, QObject* parent = nullptr);
|
||||
~Daemon() override;
|
||||
|
||||
bool start();
|
||||
|
||||
public Q_SLOTS:
|
||||
// Called by the session before it ends; replies once the login screen is up.
|
||||
Q_SCRIPTABLE void SwitchToGreeter(const QDBusMessage& message);
|
||||
|
||||
private:
|
||||
struct PendingStart {
|
||||
QStringList command;
|
||||
QStringList environment;
|
||||
};
|
||||
|
||||
void startGreeter();
|
||||
void showGreeter();
|
||||
void stopGreeter();
|
||||
void onGreeterFinished();
|
||||
void replySwitchRequests(bool switched);
|
||||
|
||||
void createSession(const QString& username);
|
||||
void postAuthMessageResponse(const QJsonValue& response);
|
||||
void startSession(const QStringList& command, const QStringList& environment,
|
||||
const QString& session);
|
||||
void cancelSession();
|
||||
void launchSession(PendingStart start);
|
||||
void onSessionFinished();
|
||||
|
||||
// then(true) once logind has taken `vt` for the helper's session, then(false)
|
||||
// after a timeout; nothing runs once the helper is released.
|
||||
void whenControlled(HelperProcess* helper, int vt, std::function<void(bool)> then);
|
||||
// HelperProcess objects end in their own signals; they are released later.
|
||||
static void release(std::unique_ptr<HelperProcess>& helper);
|
||||
|
||||
Config m_config;
|
||||
State m_state;
|
||||
GreeterServer m_server;
|
||||
uid_t m_greeterUid = 0;
|
||||
std::unique_ptr<HelperProcess> m_greeter;
|
||||
std::unique_ptr<HelperProcess> m_login;
|
||||
std::unique_ptr<HelperProcess> m_session;
|
||||
QString m_greeterSessionId;
|
||||
std::optional<PendingStart> m_pendingStart;
|
||||
QList<QDBusMessage> m_switchRequests;
|
||||
QTimer m_greeterRestart;
|
||||
int m_greeterFailures = 0;
|
||||
bool m_greeterExpected = false;
|
||||
// The login screen has handed a login over and cannot take another.
|
||||
bool m_greeterSpent = false;
|
||||
bool m_restartGreeter = false;
|
||||
bool m_bootSplash = false;
|
||||
bool m_stopping = false;
|
||||
};
|
||||
|
||||
} // namespace dm
|
||||
@ -0,0 +1,145 @@
|
||||
#include "greeterserver.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QLocalSocket>
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace dm {
|
||||
|
||||
GreeterServer::GreeterServer(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
connect(&m_server, &QLocalServer::newConnection, this, &GreeterServer::accept);
|
||||
}
|
||||
|
||||
GreeterServer::~GreeterServer()
|
||||
{
|
||||
// m_server owns the socket; deleted with it, it would signal a dead object.
|
||||
if (m_client) {
|
||||
m_client->disconnect(this);
|
||||
delete m_client;
|
||||
}
|
||||
m_server.close();
|
||||
}
|
||||
|
||||
bool GreeterServer::listen(const QString& path, uid_t owner)
|
||||
{
|
||||
m_owner = owner;
|
||||
m_path = path;
|
||||
QLocalServer::removeServer(path);
|
||||
m_server.setSocketOptions(QLocalServer::UserAccessOption);
|
||||
if (!m_server.listen(path))
|
||||
return false;
|
||||
const QByteArray native = QFile::encodeName(path);
|
||||
return ::chown(native.constData(), owner, gid_t(-1)) == 0 && ::chmod(native.constData(), 0600) == 0;
|
||||
}
|
||||
|
||||
void GreeterServer::accept()
|
||||
{
|
||||
while (QLocalSocket* socket = m_server.nextPendingConnection()) {
|
||||
ucred peer {};
|
||||
socklen_t length = sizeof(peer);
|
||||
const bool trusted =
|
||||
getsockopt(int(socket->socketDescriptor()), SOL_SOCKET, SO_PEERCRED, &peer, &length) == 0
|
||||
&& (peer.uid == m_owner || peer.uid == 0);
|
||||
if (!trusted) {
|
||||
socket->abort();
|
||||
socket->deleteLater();
|
||||
continue;
|
||||
}
|
||||
// A new login screen replaces whatever was connected before.
|
||||
if (m_client) {
|
||||
m_client->disconnect(this);
|
||||
m_client->abort();
|
||||
m_client->deleteLater();
|
||||
}
|
||||
m_client = socket;
|
||||
m_reader = FrameReader();
|
||||
m_queued.clear();
|
||||
m_waiting = false;
|
||||
connect(socket, &QLocalSocket::readyRead, this, &GreeterServer::readRequests);
|
||||
connect(socket, &QLocalSocket::disconnected, this, [this, socket] {
|
||||
if (socket != m_client)
|
||||
return;
|
||||
m_client = nullptr;
|
||||
socket->deleteLater();
|
||||
emit disconnected();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void GreeterServer::readRequests()
|
||||
{
|
||||
if (!m_client)
|
||||
return;
|
||||
QByteArray bytes = m_client->readAll();
|
||||
m_reader.append(bytes);
|
||||
wipe(bytes);
|
||||
while (auto request = m_reader.next())
|
||||
m_queued.append(*request);
|
||||
if (m_reader.failed()) {
|
||||
m_client->abort();
|
||||
return;
|
||||
}
|
||||
if (!m_waiting && !m_queued.isEmpty())
|
||||
dispatch(m_queued.takeFirst());
|
||||
}
|
||||
|
||||
void GreeterServer::dispatch(const QJsonObject& request)
|
||||
{
|
||||
m_waiting = true;
|
||||
const QString type = request.value(QStringLiteral("type")).toString();
|
||||
if (type == QLatin1String("create_session")) {
|
||||
emit createSession(request.value(QStringLiteral("username")).toString());
|
||||
} else if (type == QLatin1String("post_auth_message_response")) {
|
||||
emit postAuthMessageResponse(request.value(QStringLiteral("response")));
|
||||
} else if (type == QLatin1String("start_session")) {
|
||||
QStringList command, environment;
|
||||
for (const auto& value : request.value(QStringLiteral("cmd")).toArray())
|
||||
command.append(value.toString());
|
||||
for (const auto& value : request.value(QStringLiteral("env")).toArray())
|
||||
environment.append(value.toString());
|
||||
emit startSession(command, environment, request.value(QStringLiteral("session")).toString());
|
||||
} else if (type == QLatin1String("cancel_session")) {
|
||||
emit cancelSession();
|
||||
} else {
|
||||
respond(error(false, QStringLiteral("Unknown request")));
|
||||
}
|
||||
}
|
||||
|
||||
void GreeterServer::respond(const QJsonObject& response)
|
||||
{
|
||||
if (!m_waiting)
|
||||
return;
|
||||
m_waiting = false;
|
||||
if (m_client)
|
||||
m_client->write(encodeFrame(response));
|
||||
if (!m_queued.isEmpty())
|
||||
dispatch(m_queued.takeFirst());
|
||||
}
|
||||
|
||||
QJsonObject GreeterServer::success()
|
||||
{
|
||||
return {{QStringLiteral("type"), QStringLiteral("success")}};
|
||||
}
|
||||
|
||||
QJsonObject GreeterServer::error(bool authentication, const QString& description)
|
||||
{
|
||||
return {{QStringLiteral("type"), QStringLiteral("error")},
|
||||
{QStringLiteral("error_type"),
|
||||
authentication ? QStringLiteral("auth_error") : QStringLiteral("error")},
|
||||
{QStringLiteral("description"), description}};
|
||||
}
|
||||
|
||||
QJsonObject GreeterServer::authMessage(const QString& style, const QString& text)
|
||||
{
|
||||
return {{QStringLiteral("type"), QStringLiteral("auth_message")},
|
||||
{QStringLiteral("auth_message_type"), style},
|
||||
{QStringLiteral("auth_message"), text}};
|
||||
}
|
||||
|
||||
} // namespace dm
|
||||
@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include <QLocalServer>
|
||||
#include <QPointer>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "../common/wire.h"
|
||||
|
||||
class QLocalSocket;
|
||||
|
||||
namespace dm {
|
||||
|
||||
// GREETD_SOCK with greetd's protocol; one request is answered at a time.
|
||||
class GreeterServer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit GreeterServer(QObject* parent = nullptr);
|
||||
~GreeterServer() override;
|
||||
|
||||
// Only `owner` may connect; the socket is chowned to it with mode 0600.
|
||||
bool listen(const QString& path, uid_t owner);
|
||||
QString path() const { return m_path; }
|
||||
|
||||
// Answers the request in flight.
|
||||
void respond(const QJsonObject& response);
|
||||
static QJsonObject success();
|
||||
static QJsonObject error(bool authentication, const QString& description);
|
||||
static QJsonObject authMessage(const QString& style, const QString& text);
|
||||
|
||||
signals:
|
||||
void createSession(const QString& username);
|
||||
void postAuthMessageResponse(const QJsonValue& response);
|
||||
// `session`, a wayland-sessions id, is our extension; greetd greeters send cmd.
|
||||
void startSession(const QStringList& command, const QStringList& environment,
|
||||
const QString& session);
|
||||
void cancelSession();
|
||||
// The client went away; a new greeter starts from scratch.
|
||||
void disconnected();
|
||||
|
||||
private:
|
||||
void accept();
|
||||
void readRequests();
|
||||
void dispatch(const QJsonObject& request);
|
||||
|
||||
QLocalServer m_server;
|
||||
QPointer<QLocalSocket> m_client;
|
||||
FrameReader m_reader;
|
||||
QList<QJsonObject> m_queued;
|
||||
QString m_path;
|
||||
uid_t m_owner = 0;
|
||||
bool m_waiting = false;
|
||||
};
|
||||
|
||||
} // namespace dm
|
||||
@ -0,0 +1,123 @@
|
||||
#include "helperprocess.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "../common/wire.h"
|
||||
|
||||
namespace dm {
|
||||
|
||||
namespace {
|
||||
|
||||
QString helperPath()
|
||||
{
|
||||
const QString overridden = qEnvironmentVariable("CUTEFISH_DM_HELPER");
|
||||
return overridden.isEmpty() ? QStringLiteral(DM_HELPER_PATH) : overridden;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
HelperProcess::HelperProcess(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
m_process.setProgram(helperPath());
|
||||
// The session's output shares the helper's stderr: the daemon's journal.
|
||||
m_process.setProcessChannelMode(QProcess::ForwardedErrorChannel);
|
||||
connect(&m_process, &QProcess::readyReadStandardOutput, this, &HelperProcess::readMessages);
|
||||
connect(&m_process, &QProcess::finished, this, [this] {
|
||||
readMessages();
|
||||
if (!m_authenticated && !m_failed) {
|
||||
m_failed = true;
|
||||
emit failed(false, QStringLiteral("The login helper exited"));
|
||||
}
|
||||
emit finished();
|
||||
});
|
||||
connect(&m_process, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) {
|
||||
if (error == QProcess::FailedToStart) {
|
||||
m_failed = true;
|
||||
emit failed(false, QStringLiteral("The login helper did not start"));
|
||||
emit finished();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
HelperProcess::~HelperProcess()
|
||||
{
|
||||
disconnect(&m_process, nullptr, this, nullptr);
|
||||
if (m_process.state() != QProcess::NotRunning) {
|
||||
terminate();
|
||||
if (!m_process.waitForFinished(3000))
|
||||
m_process.kill();
|
||||
m_process.waitForFinished(1000);
|
||||
}
|
||||
}
|
||||
|
||||
void HelperProcess::login(const QString& service, const QString& user, bool authenticate, int vt)
|
||||
{
|
||||
m_user = user;
|
||||
m_process.start();
|
||||
send({{QStringLiteral("type"), QStringLiteral("login")},
|
||||
{QStringLiteral("service"), service},
|
||||
{QStringLiteral("user"), user},
|
||||
{QStringLiteral("authenticate"), authenticate},
|
||||
{QStringLiteral("vt"), vt}});
|
||||
}
|
||||
|
||||
void HelperProcess::answer(const QJsonValue& response)
|
||||
{
|
||||
send({{QStringLiteral("type"), QStringLiteral("answer")},
|
||||
{QStringLiteral("response"), response}});
|
||||
}
|
||||
|
||||
void HelperProcess::start(const QStringList& command, const QStringList& environment)
|
||||
{
|
||||
send({{QStringLiteral("type"), QStringLiteral("start")},
|
||||
{QStringLiteral("cmd"), QJsonArray::fromStringList(command)},
|
||||
{QStringLiteral("env"), QJsonArray::fromStringList(environment)}});
|
||||
}
|
||||
|
||||
void HelperProcess::terminate()
|
||||
{
|
||||
// Never the session's pid itself: once reaped, another process may have it.
|
||||
if (m_process.state() != QProcess::NotRunning)
|
||||
m_process.terminate();
|
||||
}
|
||||
|
||||
void HelperProcess::send(const QJsonObject& message)
|
||||
{
|
||||
QByteArray line = encodeLine(message);
|
||||
m_process.write(line);
|
||||
wipe(line);
|
||||
}
|
||||
|
||||
void HelperProcess::readMessages()
|
||||
{
|
||||
m_buffer.append(m_process.readAllStandardOutput());
|
||||
qsizetype end = 0;
|
||||
while ((end = m_buffer.indexOf('\n')) >= 0) {
|
||||
const auto message = decodeLine(m_buffer.left(end));
|
||||
m_buffer.remove(0, end + 1);
|
||||
if (!message)
|
||||
continue;
|
||||
const QString type = message->value(QStringLiteral("type")).toString();
|
||||
if (type == QLatin1String("question")) {
|
||||
emit question(message->value(QStringLiteral("style")).toString(),
|
||||
message->value(QStringLiteral("text")).toString());
|
||||
} else if (type == QLatin1String("authenticated")) {
|
||||
m_authenticated = true;
|
||||
emit authenticated();
|
||||
} else if (type == QLatin1String("started")) {
|
||||
m_sessionPid = message->value(QStringLiteral("pid")).toInteger();
|
||||
m_sessionId = message->value(QStringLiteral("session")).toString();
|
||||
emit started(m_sessionId);
|
||||
} else if (type == QLatin1String("error")) {
|
||||
m_failed = true;
|
||||
emit failed(message->value(QStringLiteral("auth")).toBool(),
|
||||
message->value(QStringLiteral("message")).toString());
|
||||
} else if (type == QLatin1String("finished")) {
|
||||
m_sessionPid = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dm
|
||||
@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <QJsonValue>
|
||||
#include <QObject>
|
||||
#include <QProcess>
|
||||
#include <QStringList>
|
||||
|
||||
namespace dm {
|
||||
|
||||
// The daemon's side of one cutefish-dm-helper: a login, then its session.
|
||||
class HelperProcess : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit HelperProcess(QObject* parent = nullptr);
|
||||
~HelperProcess() override;
|
||||
|
||||
void login(const QString& service, const QString& user, bool authenticate, int vt);
|
||||
// A string for a prompt, null for an informational message.
|
||||
void answer(const QJsonValue& response);
|
||||
void start(const QStringList& command, const QStringList& environment);
|
||||
// SIGTERM to the helper, which passes it on to its session, or stops the
|
||||
// login while none runs yet.
|
||||
void terminate();
|
||||
|
||||
QString user() const { return m_user; }
|
||||
bool isAuthenticated() const { return m_authenticated; }
|
||||
bool hasStarted() const { return m_sessionPid > 0; }
|
||||
QString sessionId() const { return m_sessionId; }
|
||||
|
||||
signals:
|
||||
void question(const QString& style, const QString& text);
|
||||
void authenticated();
|
||||
void started(const QString& sessionId);
|
||||
// `authentication`: the credentials were wrong, rather than something broke.
|
||||
void failed(bool authentication, const QString& message);
|
||||
// The helper exited; after `started`, the session ended and PAM closed it.
|
||||
void finished();
|
||||
|
||||
private:
|
||||
void send(const QJsonObject& message);
|
||||
void readMessages();
|
||||
|
||||
QProcess m_process;
|
||||
QByteArray m_buffer;
|
||||
QString m_user;
|
||||
QString m_sessionId;
|
||||
qint64 m_sessionPid = 0;
|
||||
bool m_authenticated = false;
|
||||
bool m_failed = false;
|
||||
};
|
||||
|
||||
} // namespace dm
|
||||
@ -0,0 +1,52 @@
|
||||
#include <QCommandLineParser>
|
||||
#include <QCoreApplication>
|
||||
#include <QSocketNotifier>
|
||||
|
||||
#include <csignal>
|
||||
#include <sys/signalfd.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "daemon.h"
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
// Through signalfd, so SIGTERM runs the daemon's teardown.
|
||||
sigset_t mask;
|
||||
sigemptyset(&mask);
|
||||
sigaddset(&mask, SIGTERM);
|
||||
sigaddset(&mask, SIGINT);
|
||||
sigprocmask(SIG_BLOCK, &mask, nullptr);
|
||||
const int signalFd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
|
||||
|
||||
QCoreApplication app(argc, argv);
|
||||
QCoreApplication::setApplicationName(QStringLiteral("cutefish-dm"));
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.addHelpOption();
|
||||
const QCommandLineOption configOption(QStringLiteral("config"),
|
||||
QStringLiteral("Configuration file to read."),
|
||||
QStringLiteral("path"),
|
||||
QStringLiteral("/etc/cutefish/dm.conf"));
|
||||
parser.addOption(configOption);
|
||||
parser.process(app);
|
||||
|
||||
if (geteuid() != 0) {
|
||||
qCritical("cutefish-dm has to run as root");
|
||||
return 1;
|
||||
}
|
||||
|
||||
dm::Daemon daemon(dm::Config::load(parser.value(configOption)));
|
||||
if (signalFd >= 0) {
|
||||
auto* notifier = new QSocketNotifier(signalFd, QSocketNotifier::Read, &app);
|
||||
QObject::connect(notifier, &QSocketNotifier::activated, &app, [signalFd] {
|
||||
signalfd_siginfo info;
|
||||
while (read(signalFd, &info, sizeof(info)) == sizeof(info)) {
|
||||
}
|
||||
QCoreApplication::quit();
|
||||
});
|
||||
}
|
||||
if (!daemon.start())
|
||||
return 1;
|
||||
return app.exec();
|
||||
}
|
||||
@ -0,0 +1,164 @@
|
||||
#include "sessions.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QProcess>
|
||||
#include <QSet>
|
||||
#include <QStandardPaths>
|
||||
|
||||
namespace dm {
|
||||
|
||||
namespace {
|
||||
|
||||
// The escapes the Desktop Entry specification defines for string values.
|
||||
QString unescape(const QString& value)
|
||||
{
|
||||
QString result;
|
||||
result.reserve(value.size());
|
||||
for (qsizetype i = 0; i < value.size(); ++i) {
|
||||
const QChar c = value.at(i);
|
||||
if (c != QLatin1Char('\\') || i + 1 == value.size()) {
|
||||
result.append(c);
|
||||
continue;
|
||||
}
|
||||
const QChar next = value.at(++i);
|
||||
switch (next.unicode()) {
|
||||
case 's': result.append(QLatin1Char(' ')); break;
|
||||
case 'n': result.append(QLatin1Char('\n')); break;
|
||||
case 't': result.append(QLatin1Char('\t')); break;
|
||||
case 'r': result.append(QLatin1Char('\r')); break;
|
||||
default: result.append(next); break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isTrue(const QString& value)
|
||||
{
|
||||
return value.trimmed() == QLatin1String("true");
|
||||
}
|
||||
|
||||
bool executableExists(const QString& program)
|
||||
{
|
||||
if (QFileInfo(program).isAbsolute())
|
||||
return QFileInfo(program).isExecutable();
|
||||
return !QStandardPaths::findExecutable(program).isEmpty();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<SessionEntry> readSessionFile(const QString& path)
|
||||
{
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
return std::nullopt;
|
||||
|
||||
QHash<QString, QString> keys;
|
||||
bool inEntry = false;
|
||||
while (!file.atEnd()) {
|
||||
const QString line = QString::fromUtf8(file.readLine()).trimmed();
|
||||
if (line.isEmpty() || line.startsWith(QLatin1Char('#')))
|
||||
continue;
|
||||
if (line.startsWith(QLatin1Char('['))) {
|
||||
inEntry = line == QLatin1String("[Desktop Entry]");
|
||||
continue;
|
||||
}
|
||||
const qsizetype equals = line.indexOf(QLatin1Char('='));
|
||||
if (!inEntry || equals <= 0)
|
||||
continue;
|
||||
const QString key = line.left(equals).trimmed();
|
||||
if (!keys.contains(key))
|
||||
keys.insert(key, line.mid(equals + 1).trimmed());
|
||||
}
|
||||
|
||||
if (isTrue(keys.value(QStringLiteral("Hidden")))
|
||||
|| isTrue(keys.value(QStringLiteral("NoDisplay"))))
|
||||
return std::nullopt;
|
||||
const QString tryExec = unescape(keys.value(QStringLiteral("TryExec")));
|
||||
if (!tryExec.isEmpty() && !executableExists(tryExec))
|
||||
return std::nullopt;
|
||||
|
||||
SessionEntry entry;
|
||||
entry.id = QFileInfo(path).completeBaseName();
|
||||
entry.name = unescape(keys.value(QStringLiteral("Name"), entry.id));
|
||||
entry.comment = unescape(keys.value(QStringLiteral("Comment")));
|
||||
// Field codes have no meaning for a session and are dropped.
|
||||
for (const QString& argument : QProcess::splitCommand(unescape(keys.value(QStringLiteral("Exec"))))) {
|
||||
if (!(argument.size() == 2 && argument.startsWith(QLatin1Char('%'))))
|
||||
entry.exec.append(argument);
|
||||
}
|
||||
for (const QString& name : keys.value(QStringLiteral("DesktopNames")).split(QLatin1Char(';'))) {
|
||||
if (!name.trimmed().isEmpty())
|
||||
entry.desktopNames.append(unescape(name.trimmed()));
|
||||
}
|
||||
if (entry.exec.isEmpty())
|
||||
return std::nullopt;
|
||||
return entry;
|
||||
}
|
||||
|
||||
QList<SessionEntry> findSessions(const QStringList& directories)
|
||||
{
|
||||
QList<SessionEntry> sessions;
|
||||
QSet<QString> seen;
|
||||
for (const QString& directory : directories) {
|
||||
const auto files = QDir(directory).entryInfoList({QStringLiteral("*.desktop")}, QDir::Files,
|
||||
QDir::Name);
|
||||
for (const QFileInfo& file : files) {
|
||||
const QString id = file.completeBaseName();
|
||||
if (seen.contains(id))
|
||||
continue;
|
||||
// A hidden entry masks the same id further down, as with autostart.
|
||||
seen.insert(id);
|
||||
if (auto entry = readSessionFile(file.absoluteFilePath()))
|
||||
sessions.append(*entry);
|
||||
}
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
std::optional<SessionEntry> findSession(const QStringList& directories, const QString& id)
|
||||
{
|
||||
// An id comes from the greeter; it must name a file, not a path.
|
||||
if (id.isEmpty() || id.contains(QLatin1Char('/')) || id.startsWith(QLatin1Char('.')))
|
||||
return std::nullopt;
|
||||
for (const SessionEntry& session : findSessions(directories)) {
|
||||
if (session.id == id)
|
||||
return session;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QStringList sessionEnvironment(const SessionEntry& session, int vt)
|
||||
{
|
||||
QStringList environment {
|
||||
QStringLiteral("XDG_SESSION_TYPE=wayland"),
|
||||
QStringLiteral("XDG_SESSION_CLASS=user"),
|
||||
QStringLiteral("XDG_SEAT=seat0"),
|
||||
QStringLiteral("XDG_VTNR=%1").arg(vt),
|
||||
};
|
||||
// A bare command, as a greetd greeter sends, names no session.
|
||||
if (!session.id.isEmpty()) {
|
||||
environment.append(QStringLiteral("DESKTOP_SESSION=%1").arg(session.id));
|
||||
environment.append(QStringLiteral("XDG_SESSION_DESKTOP=%1")
|
||||
.arg(session.desktopNames.isEmpty() ? session.id
|
||||
: session.desktopNames.first()));
|
||||
}
|
||||
if (!session.desktopNames.isEmpty())
|
||||
environment.append(
|
||||
QStringLiteral("XDG_CURRENT_DESKTOP=%1").arg(session.desktopNames.join(QLatin1Char(':'))));
|
||||
return environment;
|
||||
}
|
||||
|
||||
QStringList greeterEnvironment(int vt, const QString& socket)
|
||||
{
|
||||
return {
|
||||
QStringLiteral("XDG_SESSION_TYPE=wayland"),
|
||||
QStringLiteral("XDG_SESSION_CLASS=greeter"),
|
||||
QStringLiteral("XDG_SEAT=seat0"),
|
||||
QStringLiteral("XDG_VTNR=%1").arg(vt),
|
||||
QStringLiteral("GREETD_SOCK=%1").arg(socket),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace dm
|
||||
@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <optional>
|
||||
|
||||
namespace dm {
|
||||
|
||||
// One entry of wayland-sessions, as sddm and GDM offer it.
|
||||
struct SessionEntry {
|
||||
// The file name without ".desktop", e.g. "cutefish-wayland".
|
||||
QString id;
|
||||
QString name;
|
||||
QString comment;
|
||||
QStringList exec;
|
||||
QStringList desktopNames;
|
||||
};
|
||||
|
||||
// Hidden, NoDisplay, a missing TryExec or no Exec: not a session.
|
||||
std::optional<SessionEntry> readSessionFile(const QString& path);
|
||||
|
||||
// An id in an earlier directory hides the same id in later ones.
|
||||
QList<SessionEntry> findSessions(const QStringList& directories);
|
||||
|
||||
std::optional<SessionEntry> findSession(const QStringList& directories, const QString& id);
|
||||
|
||||
// For pam_systemd, set before pam_open_session (sddm's Display::startAuth).
|
||||
QStringList sessionEnvironment(const SessionEntry& session, int vt);
|
||||
QStringList greeterEnvironment(int vt, const QString& socket);
|
||||
|
||||
} // namespace dm
|
||||
@ -0,0 +1,37 @@
|
||||
#include "state.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QSettings>
|
||||
|
||||
namespace dm {
|
||||
|
||||
State::State(QString path)
|
||||
: m_path(std::move(path))
|
||||
{
|
||||
}
|
||||
|
||||
QString State::lastUser() const
|
||||
{
|
||||
return QSettings(m_path, QSettings::IniFormat).value(QStringLiteral("Last/User")).toString();
|
||||
}
|
||||
|
||||
QString State::lastSession(const QString& user) const
|
||||
{
|
||||
QSettings settings(m_path, QSettings::IniFormat);
|
||||
settings.beginGroup(QStringLiteral("Sessions"));
|
||||
return settings.value(user).toString();
|
||||
}
|
||||
|
||||
void State::remember(const QString& user, const QString& session)
|
||||
{
|
||||
QDir().mkpath(QFileInfo(m_path).absolutePath());
|
||||
QSettings settings(m_path, QSettings::IniFormat);
|
||||
settings.setValue(QStringLiteral("Last/User"), user);
|
||||
settings.beginGroup(QStringLiteral("Sessions"));
|
||||
settings.setValue(user, session);
|
||||
settings.endGroup();
|
||||
settings.sync();
|
||||
}
|
||||
|
||||
} // namespace dm
|
||||
@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace dm {
|
||||
|
||||
// Who logged in last and with which session, kept for the login screen to
|
||||
// preselect. The file is world-readable: the greeter reads it as its own user.
|
||||
class State
|
||||
{
|
||||
public:
|
||||
explicit State(QString path);
|
||||
|
||||
QString lastUser() const;
|
||||
QString lastSession(const QString& user) const;
|
||||
void remember(const QString& user, const QString& session);
|
||||
|
||||
private:
|
||||
QString m_path;
|
||||
};
|
||||
|
||||
} // namespace dm
|
||||
@ -0,0 +1,80 @@
|
||||
#include "terminal.h"
|
||||
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusReply>
|
||||
#include <QFile>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <linux/kd.h>
|
||||
#include <linux/vt.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace dm {
|
||||
|
||||
namespace {
|
||||
|
||||
int openVt(int vt)
|
||||
{
|
||||
return ::open(QStringLiteral("/dev/tty%1").arg(vt).toLocal8Bit().constData(),
|
||||
O_RDWR | O_NOCTTY | O_CLOEXEC);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool setGraphicsMode(int vt)
|
||||
{
|
||||
const int fd = openVt(vt);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
const bool set = ioctl(fd, KDSETMODE, KD_GRAPHICS) == 0;
|
||||
::close(fd);
|
||||
return set;
|
||||
}
|
||||
|
||||
void restoreTextMode(int vt)
|
||||
{
|
||||
const int fd = openVt(vt);
|
||||
if (fd < 0)
|
||||
return;
|
||||
vt_mode mode {};
|
||||
int kdMode = KD_TEXT;
|
||||
if (ioctl(fd, VT_GETMODE, &mode) == 0 && mode.mode == VT_AUTO
|
||||
&& ioctl(fd, KDGETMODE, &kdMode) == 0 && kdMode == KD_GRAPHICS)
|
||||
ioctl(fd, KDSETMODE, KD_TEXT);
|
||||
::close(fd);
|
||||
}
|
||||
|
||||
bool isControlled(int vt)
|
||||
{
|
||||
const int fd = openVt(vt);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
vt_mode mode {};
|
||||
const bool controlled = ioctl(fd, VT_GETMODE, &mode) == 0 && mode.mode == VT_PROCESS;
|
||||
::close(fd);
|
||||
return controlled;
|
||||
}
|
||||
|
||||
int activeVt()
|
||||
{
|
||||
QFile file(QStringLiteral("/sys/class/tty/tty0/active"));
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return 0;
|
||||
return QString::fromLatin1(file.readAll()).trimmed().mid(3).toInt();
|
||||
}
|
||||
|
||||
bool activateSession(const QString& sessionId)
|
||||
{
|
||||
QDBusInterface manager(QStringLiteral("org.freedesktop.login1"),
|
||||
QStringLiteral("/org/freedesktop/login1"),
|
||||
QStringLiteral("org.freedesktop.login1.Manager"),
|
||||
QDBusConnection::systemBus());
|
||||
const QDBusReply<void> reply = manager.call(QStringLiteral("ActivateSession"), sessionId);
|
||||
if (!reply.isValid())
|
||||
qWarning() << "Cannot activate session" << sessionId << reply.error().message();
|
||||
return reply.isValid();
|
||||
}
|
||||
|
||||
} // namespace dm
|
||||
@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace dm {
|
||||
|
||||
// A VT in KD_GRAPHICS that comes to the front keeps the previous master's
|
||||
// frame instead of showing the console.
|
||||
bool setGraphicsMode(int vt);
|
||||
|
||||
// Undoes setGraphicsMode when nobody took the VT: in front, KD_GRAPHICS with
|
||||
// VT_AUTO makes the kernel ignore every VT switch (change_console).
|
||||
void restoreTextMode(int vt);
|
||||
|
||||
// logind has taken the VT for a session's controller (VT_PROCESS).
|
||||
bool isControlled(int vt);
|
||||
|
||||
int activeVt();
|
||||
|
||||
// logind's ActivateSession: the VT switch plus the DRM and input handover.
|
||||
bool activateSession(const QString& sessionId);
|
||||
|
||||
} // namespace dm
|
||||
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
||||
<busconfig>
|
||||
<policy user="root">
|
||||
<allow own="com.cutefish.DisplayManager"/>
|
||||
</policy>
|
||||
<!-- The daemon checks that the caller owns the session it is asked to end. -->
|
||||
<policy context="default">
|
||||
<allow send_destination="com.cutefish.DisplayManager"
|
||||
send_interface="com.cutefish.DisplayManager"/>
|
||||
<allow send_destination="com.cutefish.DisplayManager"
|
||||
send_interface="org.freedesktop.DBus.Introspectable"/>
|
||||
</policy>
|
||||
</busconfig>
|
||||
@ -0,0 +1,18 @@
|
||||
#%PAM-1.0
|
||||
|
||||
# The login screen runs without authentication, as its own system user.
|
||||
auth required pam_permit.so
|
||||
|
||||
@include common-account
|
||||
|
||||
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so close
|
||||
session optional pam_keyinit.so force revoke
|
||||
session required pam_limits.so
|
||||
session required pam_loginuid.so
|
||||
@include common-session
|
||||
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so open
|
||||
|
||||
password required pam_deny.so
|
||||
|
||||
session required pam_env.so
|
||||
session required pam_env.so envfile=/etc/default/locale
|
||||
@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
# Runs the session through the login shell so profiles apply (sddm's wayland-session).
|
||||
|
||||
case $SHELL in
|
||||
*/bash|*/zsh)
|
||||
exec "$SHELL" --login -c 'exec "$@"' - "$@"
|
||||
;;
|
||||
*/fish)
|
||||
[ -f /etc/profile ] && . /etc/profile
|
||||
[ -f "$HOME/.profile" ] && . "$HOME/.profile"
|
||||
exec "$SHELL" --login -c 'exec $argv' "$@"
|
||||
;;
|
||||
*)
|
||||
[ -f /etc/profile ] && . /etc/profile
|
||||
[ -f "$HOME/.profile" ] && . "$HOME/.profile"
|
||||
exec "$@"
|
||||
;;
|
||||
esac
|
||||
@ -0,0 +1,26 @@
|
||||
#%PAM-1.0
|
||||
|
||||
# Block login if they are globally disabled
|
||||
auth requisite pam_nologin.so
|
||||
auth required pam_succeed_if.so user != root quiet_success
|
||||
@include common-auth
|
||||
-auth optional pam_gnome_keyring.so
|
||||
-auth optional pam_kwallet5.so
|
||||
|
||||
@include common-account
|
||||
|
||||
# SELinux needs to be the first session rule, and to intervene last.
|
||||
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so close
|
||||
session optional pam_keyinit.so force revoke
|
||||
session required pam_limits.so
|
||||
session required pam_loginuid.so
|
||||
@include common-session
|
||||
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so open
|
||||
-session optional pam_gnome_keyring.so auto_start
|
||||
-session optional pam_kwallet5.so auto_start
|
||||
|
||||
@include common-password
|
||||
|
||||
# pam_env goes last: what it sets can affect the modules above.
|
||||
session required pam_env.so
|
||||
session required pam_env.so envfile=/etc/default/locale user_readenv=1
|
||||
@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=Cutefish Display Manager
|
||||
Conflicts=getty@tty1.service
|
||||
After=getty@tty1.service systemd-user-sessions.service systemd-logind.service
|
||||
# The login screen takes over plymouth's splash, as GDM does.
|
||||
Conflicts=plymouth-quit.service
|
||||
After=plymouth-quit.service plymouth-start.service
|
||||
OnFailure=plymouth-quit.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/cutefish-dm
|
||||
# The daemon ends the login screen and the session itself.
|
||||
KillMode=mixed
|
||||
Restart=always
|
||||
RestartSec=1s
|
||||
EnvironmentFile=-/etc/default/locale
|
||||
|
||||
[Install]
|
||||
Alias=display-manager.service
|
||||
@ -0,0 +1,5 @@
|
||||
cutefish-dm (0.1.0) UNRELEASED; urgency=medium
|
||||
|
||||
* Initial release.
|
||||
|
||||
-- Reion Wong <reionwong@gmail.com> Fri, 25 Sep 2026 22:34:44 +0800
|
||||
@ -0,0 +1 @@
|
||||
9
|
||||
@ -0,0 +1,21 @@
|
||||
Source: cutefish-dm
|
||||
Section: x11
|
||||
Priority: optional
|
||||
Maintainer: Reion Wong <reionwong@gmail.com>
|
||||
Build-Depends: cmake (>= 3.22),
|
||||
debhelper (>= 9),
|
||||
qt6-base-dev (>= 6.8),
|
||||
libpam0g-dev
|
||||
Standards-Version: 4.5.0
|
||||
Homepage: https://github.com/cutefishos
|
||||
|
||||
Package: cutefish-dm
|
||||
Architecture: any
|
||||
Depends: adduser,
|
||||
cutefish-compositor,
|
||||
${misc:Depends},
|
||||
${shlibs:Depends}
|
||||
Description: CutefishOS display manager
|
||||
Runs the Cutefish login screen and the Wayland session a user logs in to,
|
||||
Cutefish or any other desktop in wayland-sessions, and hands the display
|
||||
between them without a black frame or the console in between.
|
||||
@ -0,0 +1,3 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: cutefish-dm
|
||||
Source: cutefishos.com
|
||||
@ -0,0 +1,30 @@
|
||||
#! /bin/sh
|
||||
# see: dh_installdeb(1)
|
||||
|
||||
set -e
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
# The login screen's own user; it owns nothing but its home.
|
||||
if ! getent passwd cutefish-greeter >/dev/null; then
|
||||
adduser --system --group --home /var/lib/cutefish-greeter \
|
||||
--gecos "Cutefish login screen" --quiet cutefish-greeter
|
||||
fi
|
||||
mkdir -p /var/lib/cutefish-dm
|
||||
;;
|
||||
|
||||
abort-upgrade|abort-remove|abort-deconfigure)
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "postinst called with unknown argument \`$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# dh_installdeb will replace this with shell code automatically
|
||||
# generated by other debhelper scripts.
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
||||
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
%:
|
||||
dh $@ --parallel
|
||||
@ -0,0 +1 @@
|
||||
3.0 (quilt)
|
||||
@ -0,0 +1,336 @@
|
||||
// One login, PAM to session end, outside the threaded daemon (greetd's session
|
||||
// worker, sddm-helper). stdout carries only replies; the session writes stderr.
|
||||
|
||||
#include <security/pam_appl.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <grp.h>
|
||||
#include <optional>
|
||||
#include <pwd.h>
|
||||
#include <string>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "../common/wire.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t kMaxLine = 64 * 1024;
|
||||
|
||||
// After authentication nobody answers; a session module's motd would hang.
|
||||
bool g_conversing = true;
|
||||
|
||||
// The session's pid until it is reaped: unreaped, it cannot belong to anyone else.
|
||||
volatile sig_atomic_t g_session = 0;
|
||||
|
||||
// SIGTERM ends the session, which this process then closes, or the login before one.
|
||||
void onTerminate(int)
|
||||
{
|
||||
if (g_session > 0)
|
||||
kill(pid_t(g_session), SIGTERM);
|
||||
else
|
||||
_exit(128 + SIGTERM);
|
||||
}
|
||||
|
||||
std::optional<QJsonObject> receive()
|
||||
{
|
||||
std::string line;
|
||||
char c = 0;
|
||||
ssize_t count = 0;
|
||||
while ((count = read(STDIN_FILENO, &c, 1)) == 1 || (count < 0 && errno == EINTR)) {
|
||||
if (count < 0)
|
||||
continue;
|
||||
if (c == '\n')
|
||||
break;
|
||||
line.push_back(c);
|
||||
if (line.size() > kMaxLine)
|
||||
return std::nullopt;
|
||||
}
|
||||
if (line.empty())
|
||||
return std::nullopt;
|
||||
const auto message = dm::decodeLine(QByteArray::fromStdString(line));
|
||||
explicit_bzero(line.data(), line.size());
|
||||
return message;
|
||||
}
|
||||
|
||||
void send(const QJsonObject& message)
|
||||
{
|
||||
const QByteArray bytes = dm::encodeLine(message);
|
||||
qsizetype written = 0;
|
||||
while (written < bytes.size()) {
|
||||
const ssize_t count = write(STDOUT_FILENO, bytes.constData() + written,
|
||||
size_t(bytes.size() - written));
|
||||
if (count < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (count <= 0)
|
||||
return;
|
||||
written += count;
|
||||
}
|
||||
}
|
||||
|
||||
void fail(bool authentication, const QString& message)
|
||||
{
|
||||
send({{QStringLiteral("type"), QStringLiteral("error")},
|
||||
{QStringLiteral("auth"), authentication},
|
||||
{QStringLiteral("message"), message}});
|
||||
}
|
||||
|
||||
void freeResponses(pam_response* responses, int count)
|
||||
{
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (responses[i].resp) {
|
||||
explicit_bzero(responses[i].resp, strlen(responses[i].resp));
|
||||
free(responses[i].resp);
|
||||
}
|
||||
}
|
||||
free(responses);
|
||||
}
|
||||
|
||||
// Every PAM message goes to the greeter and waits for its answer, as in greetd.
|
||||
int converse(int count, const pam_message** messages, pam_response** result, void*)
|
||||
{
|
||||
if (count <= 0 || count > PAM_MAX_NUM_MSG)
|
||||
return PAM_CONV_ERR;
|
||||
auto* responses = static_cast<pam_response*>(calloc(size_t(count), sizeof(pam_response)));
|
||||
if (!responses)
|
||||
return PAM_BUF_ERR;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const int kind = messages[i]->msg_style;
|
||||
if (!g_conversing) {
|
||||
if (kind == PAM_PROMPT_ECHO_OFF || kind == PAM_PROMPT_ECHO_ON) {
|
||||
freeResponses(responses, count);
|
||||
return PAM_CONV_ERR;
|
||||
}
|
||||
fprintf(stderr, "PAM: %s\n", messages[i]->msg);
|
||||
continue;
|
||||
}
|
||||
QString style;
|
||||
switch (messages[i]->msg_style) {
|
||||
case PAM_PROMPT_ECHO_OFF: style = QStringLiteral("secret"); break;
|
||||
case PAM_PROMPT_ECHO_ON: style = QStringLiteral("visible"); break;
|
||||
case PAM_ERROR_MSG: style = QStringLiteral("error"); break;
|
||||
case PAM_TEXT_INFO: style = QStringLiteral("info"); break;
|
||||
default:
|
||||
freeResponses(responses, count);
|
||||
return PAM_CONV_ERR;
|
||||
}
|
||||
send({{QStringLiteral("type"), QStringLiteral("question")},
|
||||
{QStringLiteral("style"), style},
|
||||
{QStringLiteral("text"), QString::fromUtf8(messages[i]->msg)}});
|
||||
auto answer = receive();
|
||||
if (!answer || answer->value(QStringLiteral("type")).toString() != QLatin1String("answer")) {
|
||||
freeResponses(responses, count);
|
||||
return PAM_CONV_ERR;
|
||||
}
|
||||
const int prompt = messages[i]->msg_style;
|
||||
if (prompt == PAM_PROMPT_ECHO_OFF || prompt == PAM_PROMPT_ECHO_ON) {
|
||||
QString text = answer->value(QStringLiteral("response")).toString();
|
||||
QByteArray bytes = text.toUtf8();
|
||||
responses[i].resp = strdup(bytes.constData());
|
||||
dm::wipe(bytes);
|
||||
dm::wipe(text);
|
||||
if (!responses[i].resp) {
|
||||
freeResponses(responses, count);
|
||||
return PAM_BUF_ERR;
|
||||
}
|
||||
}
|
||||
(*answer)[QStringLiteral("response")] = QJsonValue();
|
||||
}
|
||||
*result = responses;
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
struct Account {
|
||||
std::string name;
|
||||
std::string home;
|
||||
std::string shell;
|
||||
uid_t uid = 0;
|
||||
gid_t gid = 0;
|
||||
};
|
||||
|
||||
std::optional<Account> account(pam_handle_t* pam)
|
||||
{
|
||||
// PAM_USER, since a module may have changed it.
|
||||
const void* item = nullptr;
|
||||
if (pam_get_item(pam, PAM_USER, &item) != PAM_SUCCESS || !item)
|
||||
return std::nullopt;
|
||||
const passwd* entry = getpwnam(static_cast<const char*>(item));
|
||||
if (!entry)
|
||||
return std::nullopt;
|
||||
return Account {entry->pw_name, entry->pw_dir, entry->pw_shell, entry->pw_uid, entry->pw_gid};
|
||||
}
|
||||
|
||||
std::vector<std::string> strings(const QJsonArray& array)
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
for (const auto& value : array)
|
||||
result.push_back(value.toString().toStdString());
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
// The daemon's signalfd mask is inherited; without this SIGTERM never lands.
|
||||
sigset_t none;
|
||||
sigemptyset(&none);
|
||||
sigprocmask(SIG_SETMASK, &none, nullptr);
|
||||
struct sigaction action = {};
|
||||
action.sa_handler = onTerminate;
|
||||
sigemptyset(&action.sa_mask);
|
||||
sigaction(SIGTERM, &action, nullptr);
|
||||
// The daemon's death ends the login like its SIGTERM; no password goes to a core file.
|
||||
prctl(PR_SET_PDEATHSIG, SIGTERM);
|
||||
prctl(PR_SET_DUMPABLE, 0);
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
const auto login = receive();
|
||||
if (!login || login->value(QStringLiteral("type")).toString() != QLatin1String("login")) {
|
||||
fail(false, QStringLiteral("Expected a login request"));
|
||||
return 1;
|
||||
}
|
||||
const std::string service = login->value(QStringLiteral("service")).toString().toStdString();
|
||||
const std::string user = login->value(QStringLiteral("user")).toString().toStdString();
|
||||
const bool authenticate = login->value(QStringLiteral("authenticate")).toBool();
|
||||
const int vt = login->value(QStringLiteral("vt")).toInt();
|
||||
if (service.empty() || user.empty() || vt <= 0) {
|
||||
fail(false, QStringLiteral("Incomplete login request"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const pam_conv conversation {converse, nullptr};
|
||||
pam_handle_t* pam = nullptr;
|
||||
int status = pam_start(service.c_str(), user.c_str(), &conversation, &pam);
|
||||
if (status != PAM_SUCCESS) {
|
||||
fail(false, QString::fromUtf8(pam_strerror(pam, status)));
|
||||
return 1;
|
||||
}
|
||||
const auto finish = [&](int result) {
|
||||
pam_end(pam, result);
|
||||
return 1;
|
||||
};
|
||||
|
||||
if (authenticate) {
|
||||
status = pam_authenticate(pam, 0);
|
||||
if (status != PAM_SUCCESS) {
|
||||
fail(true, QString::fromUtf8(pam_strerror(pam, status)));
|
||||
return finish(status);
|
||||
}
|
||||
}
|
||||
status = pam_acct_mgmt(pam, 0);
|
||||
if (status == PAM_NEW_AUTHTOK_REQD)
|
||||
status = pam_chauthtok(pam, PAM_CHANGE_EXPIRED_AUTHTOK);
|
||||
if (status != PAM_SUCCESS) {
|
||||
fail(true, QString::fromUtf8(pam_strerror(pam, status)));
|
||||
return finish(status);
|
||||
}
|
||||
status = pam_setcred(pam, PAM_ESTABLISH_CRED);
|
||||
if (status != PAM_SUCCESS) {
|
||||
fail(false, QString::fromUtf8(pam_strerror(pam, status)));
|
||||
return finish(status);
|
||||
}
|
||||
g_conversing = false;
|
||||
send({{QStringLiteral("type"), QStringLiteral("authenticated")}});
|
||||
|
||||
const auto request = receive();
|
||||
if (!request || request->value(QStringLiteral("type")).toString() != QLatin1String("start")) {
|
||||
pam_setcred(pam, PAM_DELETE_CRED);
|
||||
pam_end(pam, PAM_SUCCESS);
|
||||
return 0;
|
||||
}
|
||||
const std::vector<std::string> command = strings(request->value(QStringLiteral("cmd")).toArray());
|
||||
const auto owner = account(pam);
|
||||
if (command.empty() || !owner) {
|
||||
fail(false, QStringLiteral("No command or no account to start"));
|
||||
pam_setcred(pam, PAM_DELETE_CRED);
|
||||
return finish(PAM_SESSION_ERR);
|
||||
}
|
||||
|
||||
// pam_systemd takes the seat, VT, type and class from here.
|
||||
const std::string tty = "tty" + std::to_string(vt);
|
||||
pam_set_item(pam, PAM_TTY, tty.c_str());
|
||||
for (const std::string& variable : strings(request->value(QStringLiteral("env")).toArray()))
|
||||
pam_putenv(pam, variable.c_str());
|
||||
const std::string home = "HOME=" + owner->home;
|
||||
const std::string name = "USER=" + owner->name;
|
||||
const std::string logname = "LOGNAME=" + owner->name;
|
||||
// The session wrapper picks the login shell from it.
|
||||
const std::string shell = "SHELL=" + owner->shell;
|
||||
pam_putenv(pam, home.c_str());
|
||||
pam_putenv(pam, name.c_str());
|
||||
pam_putenv(pam, logname.c_str());
|
||||
pam_putenv(pam, shell.c_str());
|
||||
if (!pam_getenv(pam, "PATH"))
|
||||
pam_putenv(pam, "PATH=/usr/local/bin:/usr/bin:/bin");
|
||||
|
||||
status = pam_open_session(pam, 0);
|
||||
if (status != PAM_SUCCESS) {
|
||||
fail(false, QString::fromUtf8(pam_strerror(pam, status)));
|
||||
pam_setcred(pam, PAM_DELETE_CRED);
|
||||
return finish(status);
|
||||
}
|
||||
const char* sessionId = pam_getenv(pam, "XDG_SESSION_ID");
|
||||
char** environment = pam_getenvlist(pam);
|
||||
|
||||
std::vector<char*> arguments;
|
||||
for (const std::string& argument : command)
|
||||
arguments.push_back(const_cast<char*>(argument.c_str()));
|
||||
arguments.push_back(nullptr);
|
||||
|
||||
// The session runs in a child; this process stays to close the PAM session.
|
||||
const pid_t child = fork();
|
||||
if (child == 0) {
|
||||
setsid();
|
||||
const int null = open("/dev/null", O_RDONLY | O_CLOEXEC);
|
||||
if (null >= 0)
|
||||
dup2(null, STDIN_FILENO);
|
||||
dup2(STDERR_FILENO, STDOUT_FILENO);
|
||||
// Not the session's: fds PAM modules left open, the ignored SIGPIPE.
|
||||
close_range(3, ~0U, 0);
|
||||
signal(SIGPIPE, SIG_DFL);
|
||||
if (initgroups(owner->name.c_str(), owner->gid) != 0 || setgid(owner->gid) != 0
|
||||
|| setuid(owner->uid) != 0)
|
||||
_exit(127);
|
||||
// setuid resets it.
|
||||
prctl(PR_SET_PDEATHSIG, SIGTERM);
|
||||
if (chdir(owner->home.c_str()) != 0 && chdir("/") != 0)
|
||||
_exit(127);
|
||||
execve(arguments.front(), arguments.data(), environment);
|
||||
_exit(127);
|
||||
}
|
||||
if (child < 0) {
|
||||
fail(false, QString::fromUtf8(strerror(errno)));
|
||||
pam_close_session(pam, 0);
|
||||
pam_setcred(pam, PAM_DELETE_CRED);
|
||||
return finish(PAM_SESSION_ERR);
|
||||
}
|
||||
g_session = child;
|
||||
send({{QStringLiteral("type"), QStringLiteral("started")},
|
||||
{QStringLiteral("pid"), qint64(child)},
|
||||
{QStringLiteral("session"), QString::fromUtf8(sessionId ? sessionId : "")}});
|
||||
|
||||
int wait = 0;
|
||||
while (waitpid(child, &wait, 0) < 0 && errno == EINTR) {
|
||||
}
|
||||
// The session is over; closing it is not to be cut short.
|
||||
signal(SIGTERM, SIG_IGN);
|
||||
// Closing the PAM session needs root.
|
||||
pam_close_session(pam, 0);
|
||||
pam_setcred(pam, PAM_DELETE_CRED);
|
||||
pam_end(pam, PAM_SUCCESS);
|
||||
send({{QStringLiteral("type"), QStringLiteral("finished")},
|
||||
{QStringLiteral("status"),
|
||||
WIFEXITED(wait) ? WEXITSTATUS(wait) : 128 + (WIFSIGNALED(wait) ? WTERMSIG(wait) : 0)}});
|
||||
return 0;
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
find_package(Qt6 6.8 REQUIRED COMPONENTS Test)
|
||||
|
||||
add_executable(dm-test
|
||||
dm_test.cpp
|
||||
../daemon/config.cpp
|
||||
../daemon/greeterserver.cpp
|
||||
../daemon/helperprocess.cpp
|
||||
../daemon/sessions.cpp
|
||||
../daemon/state.cpp
|
||||
)
|
||||
target_compile_definitions(dm-test PRIVATE
|
||||
DM_HELPER_PATH="/nonexistent/cutefish-dm-helper"
|
||||
DM_SESSION_WRAPPER="/nonexistent/cutefish-dm-session"
|
||||
)
|
||||
target_include_directories(dm-test PRIVATE ${PROJECT_SOURCE_DIR})
|
||||
target_link_libraries(dm-test PRIVATE cutefish-dm-common Qt6::Core Qt6::Network Qt6::Test)
|
||||
add_test(NAME dm COMMAND dm-test)
|
||||
@ -0,0 +1,346 @@
|
||||
#include <QCoreApplication>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QLocalSocket>
|
||||
#include <QSignalSpy>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTest>
|
||||
|
||||
#include <csignal>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "common/wire.h"
|
||||
#include "daemon/config.h"
|
||||
#include "daemon/greeterserver.h"
|
||||
#include "daemon/helperprocess.h"
|
||||
#include "daemon/sessions.h"
|
||||
#include "daemon/state.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// When set, the test binary is the helper: it accepts "right" and its session
|
||||
// runs until terminated.
|
||||
constexpr char kFakeHelper[] = "DM_TEST_FAKE_HELPER";
|
||||
|
||||
QByteArray readLine()
|
||||
{
|
||||
QByteArray line;
|
||||
char c = 0;
|
||||
while (read(STDIN_FILENO, &c, 1) == 1 && c != '\n')
|
||||
line.append(c);
|
||||
return line;
|
||||
}
|
||||
|
||||
void writeLine(const QJsonObject& message)
|
||||
{
|
||||
const QByteArray bytes = dm::encodeLine(message);
|
||||
if (write(STDOUT_FILENO, bytes.constData(), size_t(bytes.size())) != bytes.size())
|
||||
_exit(3);
|
||||
}
|
||||
|
||||
int fakeHelper()
|
||||
{
|
||||
const auto login = dm::decodeLine(readLine());
|
||||
if (!login || login->value(QStringLiteral("user")).toString() != QLatin1String("alice"))
|
||||
return 2;
|
||||
writeLine({{QStringLiteral("type"), QStringLiteral("question")},
|
||||
{QStringLiteral("style"), QStringLiteral("secret")},
|
||||
{QStringLiteral("text"), QStringLiteral("Password:")}});
|
||||
const auto answer = dm::decodeLine(readLine());
|
||||
if (!answer || answer->value(QStringLiteral("response")).toString() != QLatin1String("right")) {
|
||||
writeLine({{QStringLiteral("type"), QStringLiteral("error")},
|
||||
{QStringLiteral("auth"), true},
|
||||
{QStringLiteral("message"), QStringLiteral("Authentication failure")}});
|
||||
return 1;
|
||||
}
|
||||
writeLine({{QStringLiteral("type"), QStringLiteral("authenticated")}});
|
||||
const auto start = dm::decodeLine(readLine());
|
||||
if (!start || start->value(QStringLiteral("cmd")).toArray().isEmpty())
|
||||
return 0;
|
||||
writeLine({{QStringLiteral("type"), QStringLiteral("started")},
|
||||
{QStringLiteral("pid"), qint64(getpid())},
|
||||
{QStringLiteral("session"), QStringLiteral("42")}});
|
||||
for (;;)
|
||||
pause();
|
||||
}
|
||||
|
||||
QByteArray frame(const QJsonObject& message)
|
||||
{
|
||||
return dm::encodeFrame(message);
|
||||
}
|
||||
|
||||
void writeFile(const QString& path, const QByteArray& contents)
|
||||
{
|
||||
QFile file(path);
|
||||
QVERIFY(file.open(QIODevice::WriteOnly));
|
||||
file.write(contents);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class DisplayManagerTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
|
||||
void framesSurviveSplitsAndBatches()
|
||||
{
|
||||
const QByteArray first = frame({{QStringLiteral("type"), QStringLiteral("success")}});
|
||||
const QByteArray second = frame({{QStringLiteral("type"), QStringLiteral("cancel_session")}});
|
||||
dm::FrameReader reader;
|
||||
reader.append(first.left(3));
|
||||
QVERIFY(!reader.next());
|
||||
reader.append(first.mid(3) + second);
|
||||
const auto a = reader.next();
|
||||
const auto b = reader.next();
|
||||
QVERIFY(a && b);
|
||||
QCOMPARE(a->value(QStringLiteral("type")).toString(), QStringLiteral("success"));
|
||||
QCOMPARE(b->value(QStringLiteral("type")).toString(), QStringLiteral("cancel_session"));
|
||||
QVERIFY(!reader.next());
|
||||
QVERIFY(!reader.failed());
|
||||
}
|
||||
|
||||
void oversizedOrMalformedFramesFailTheReader()
|
||||
{
|
||||
dm::FrameReader oversized;
|
||||
const quint32 length = dm::kMaxFrameSize + 1;
|
||||
oversized.append(QByteArray(reinterpret_cast<const char*>(&length), sizeof(length)));
|
||||
QVERIFY(!oversized.next());
|
||||
QVERIFY(oversized.failed());
|
||||
|
||||
dm::FrameReader malformed;
|
||||
const QByteArray payload("[1,2]");
|
||||
const quint32 size = quint32(payload.size());
|
||||
malformed.append(QByteArray(reinterpret_cast<const char*>(&size), sizeof(size)) + payload);
|
||||
QVERIFY(!malformed.next());
|
||||
QVERIFY(malformed.failed());
|
||||
}
|
||||
|
||||
void linesRoundTrip()
|
||||
{
|
||||
const QJsonObject message {{QStringLiteral("type"), QStringLiteral("answer")},
|
||||
{QStringLiteral("response"), QJsonValue()}};
|
||||
const QByteArray line = dm::encodeLine(message);
|
||||
QVERIFY(line.endsWith('\n'));
|
||||
QCOMPARE(line.count('\n'), 1);
|
||||
QCOMPARE(*dm::decodeLine(line), message);
|
||||
QVERIFY(!dm::decodeLine("not json"));
|
||||
}
|
||||
|
||||
void sessionFilesParse()
|
||||
{
|
||||
QTemporaryDir directory;
|
||||
writeFile(directory.filePath(QStringLiteral("plasma.desktop")),
|
||||
"[Desktop Entry]\n"
|
||||
"# a comment\n"
|
||||
"Name=Plasma (Wayland)\n"
|
||||
"Comment=Plasma by KDE\n"
|
||||
"Exec=/usr/bin/startplasma-wayland --arg \"two words\" %f\n"
|
||||
"DesktopNames=KDE;Plasma;\n"
|
||||
"[Other Group]\n"
|
||||
"Exec=ignored\n");
|
||||
const auto entry = dm::readSessionFile(directory.filePath(QStringLiteral("plasma.desktop")));
|
||||
QVERIFY(entry);
|
||||
QCOMPARE(entry->id, QStringLiteral("plasma"));
|
||||
QCOMPARE(entry->name, QStringLiteral("Plasma (Wayland)"));
|
||||
QCOMPARE(entry->exec, (QStringList {QStringLiteral("/usr/bin/startplasma-wayland"),
|
||||
QStringLiteral("--arg"), QStringLiteral("two words")}));
|
||||
QCOMPARE(entry->desktopNames, (QStringList {QStringLiteral("KDE"), QStringLiteral("Plasma")}));
|
||||
}
|
||||
|
||||
void hiddenMissingOrCommandlessSessionsAreNotOffered()
|
||||
{
|
||||
QTemporaryDir directory;
|
||||
writeFile(directory.filePath(QStringLiteral("hidden.desktop")),
|
||||
"[Desktop Entry]\nName=Hidden\nExec=/bin/true\nHidden=true\n");
|
||||
writeFile(directory.filePath(QStringLiteral("nodisplay.desktop")),
|
||||
"[Desktop Entry]\nName=NoDisplay\nExec=/bin/true\nNoDisplay=true\n");
|
||||
writeFile(directory.filePath(QStringLiteral("missing.desktop")),
|
||||
"[Desktop Entry]\nName=Missing\nExec=/bin/true\nTryExec=/nonexistent/program\n");
|
||||
writeFile(directory.filePath(QStringLiteral("noexec.desktop")),
|
||||
"[Desktop Entry]\nName=NoExec\n");
|
||||
writeFile(directory.filePath(QStringLiteral("good.desktop")),
|
||||
"[Desktop Entry]\nName=Good\nExec=/bin/true\nTryExec=true\n");
|
||||
const auto sessions = dm::findSessions({directory.path()});
|
||||
QCOMPARE(sessions.size(), 1);
|
||||
QCOMPARE(sessions.first().id, QStringLiteral("good"));
|
||||
}
|
||||
|
||||
void earlierDirectoriesWinAndIdsNameFilesOnly()
|
||||
{
|
||||
QTemporaryDir local, system;
|
||||
writeFile(local.filePath(QStringLiteral("cutefish.desktop")),
|
||||
"[Desktop Entry]\nName=Local\nExec=/bin/true\n");
|
||||
writeFile(system.filePath(QStringLiteral("cutefish.desktop")),
|
||||
"[Desktop Entry]\nName=System\nExec=/bin/false\n");
|
||||
writeFile(local.filePath(QStringLiteral("gnome.desktop")),
|
||||
"[Desktop Entry]\nName=GNOME\nExec=/bin/true\nHidden=true\n");
|
||||
writeFile(system.filePath(QStringLiteral("gnome.desktop")),
|
||||
"[Desktop Entry]\nName=GNOME\nExec=/bin/true\n");
|
||||
const QStringList directories {local.path(), system.path()};
|
||||
QCOMPARE(dm::findSession(directories, QStringLiteral("cutefish"))->name,
|
||||
QStringLiteral("Local"));
|
||||
// A hidden local entry masks the system one.
|
||||
QVERIFY(!dm::findSession(directories, QStringLiteral("gnome")));
|
||||
QVERIFY(!dm::findSession(directories, QStringLiteral("../cutefish")));
|
||||
QVERIFY(!dm::findSession(directories, QStringLiteral(".hidden")));
|
||||
QVERIFY(!dm::findSession(directories, QString()));
|
||||
}
|
||||
|
||||
void environmentsCarryWhatPamSystemdReads()
|
||||
{
|
||||
dm::SessionEntry gnome;
|
||||
gnome.id = QStringLiteral("gnome");
|
||||
gnome.desktopNames = {QStringLiteral("GNOME")};
|
||||
const QStringList session = dm::sessionEnvironment(gnome, 2);
|
||||
QVERIFY(session.contains(QStringLiteral("XDG_SESSION_TYPE=wayland")));
|
||||
QVERIFY(session.contains(QStringLiteral("XDG_SESSION_CLASS=user")));
|
||||
QVERIFY(session.contains(QStringLiteral("XDG_SEAT=seat0")));
|
||||
QVERIFY(session.contains(QStringLiteral("XDG_VTNR=2")));
|
||||
QVERIFY(session.contains(QStringLiteral("DESKTOP_SESSION=gnome")));
|
||||
QVERIFY(session.contains(QStringLiteral("XDG_SESSION_DESKTOP=GNOME")));
|
||||
QVERIFY(session.contains(QStringLiteral("XDG_CURRENT_DESKTOP=GNOME")));
|
||||
|
||||
const QStringList bare = dm::sessionEnvironment(dm::SessionEntry {}, 2);
|
||||
QVERIFY(bare.contains(QStringLiteral("XDG_SESSION_TYPE=wayland")));
|
||||
for (const QString& variable : bare)
|
||||
QVERIFY(!variable.startsWith(QLatin1String("DESKTOP_SESSION=")));
|
||||
|
||||
const QStringList greeter = dm::greeterEnvironment(1, QStringLiteral("/run/x.sock"));
|
||||
QVERIFY(greeter.contains(QStringLiteral("XDG_SESSION_CLASS=greeter")));
|
||||
QVERIFY(greeter.contains(QStringLiteral("XDG_VTNR=1")));
|
||||
QVERIFY(greeter.contains(QStringLiteral("GREETD_SOCK=/run/x.sock")));
|
||||
}
|
||||
|
||||
void stateRemembersTheLastLogin()
|
||||
{
|
||||
QTemporaryDir directory;
|
||||
dm::State state(directory.filePath(QStringLiteral("state/state.conf")));
|
||||
QVERIFY(state.lastUser().isEmpty());
|
||||
state.remember(QStringLiteral("alice"), QStringLiteral("gnome"));
|
||||
state.remember(QStringLiteral("bob"), QStringLiteral("cutefish-wayland"));
|
||||
dm::State reread(directory.filePath(QStringLiteral("state/state.conf")));
|
||||
QCOMPARE(reread.lastUser(), QStringLiteral("bob"));
|
||||
QCOMPARE(reread.lastSession(QStringLiteral("alice")), QStringLiteral("gnome"));
|
||||
QCOMPARE(reread.lastSession(QStringLiteral("bob")), QStringLiteral("cutefish-wayland"));
|
||||
}
|
||||
|
||||
void configFallsBackToDefaults()
|
||||
{
|
||||
const dm::Config defaults = dm::Config::load(QStringLiteral("/nonexistent/dm.conf"));
|
||||
QCOMPARE(defaults.greeterVt, 1);
|
||||
QCOMPARE(defaults.sessionVt, 2);
|
||||
QCOMPARE(defaults.greeterUser, QStringLiteral("cutefish-greeter"));
|
||||
|
||||
QTemporaryDir directory;
|
||||
writeFile(directory.filePath(QStringLiteral("dm.conf")),
|
||||
"[Greeter]\nVt=8\nCommand=/usr/bin/cutefish-compositor --login --no-xwayland\n"
|
||||
"[Session]\nVt=9\n[Daemon]\nRuntimeDirectory=/run/test\n");
|
||||
const dm::Config config = dm::Config::load(directory.filePath(QStringLiteral("dm.conf")));
|
||||
QCOMPARE(config.greeterVt, 8);
|
||||
QCOMPARE(config.sessionVt, 9);
|
||||
QCOMPARE(config.greeterCommand.last(), QStringLiteral("--no-xwayland"));
|
||||
QCOMPARE(config.runtimeDirectory, QStringLiteral("/run/test"));
|
||||
QCOMPARE(config.greeterUser, QStringLiteral("cutefish-greeter"));
|
||||
}
|
||||
|
||||
void greeterServerSpeaksGreetd()
|
||||
{
|
||||
QTemporaryDir directory;
|
||||
const QString path = directory.filePath(QStringLiteral("greeter.sock"));
|
||||
dm::GreeterServer server;
|
||||
QVERIFY(server.listen(path, getuid()));
|
||||
QCOMPARE(QFile::permissions(path) & (QFile::ReadGroup | QFile::ReadOther), QFileDevice::Permissions());
|
||||
|
||||
QSignalSpy created(&server, &dm::GreeterServer::createSession);
|
||||
QSignalSpy started(&server, &dm::GreeterServer::startSession);
|
||||
QLocalSocket client;
|
||||
client.connectToServer(path);
|
||||
QVERIFY(client.waitForConnected(1000));
|
||||
|
||||
// Two requests at once: the second waits for the first's answer.
|
||||
client.write(frame({{QStringLiteral("type"), QStringLiteral("create_session")},
|
||||
{QStringLiteral("username"), QStringLiteral("alice")}}));
|
||||
client.write(frame({{QStringLiteral("type"), QStringLiteral("start_session")},
|
||||
{QStringLiteral("cmd"), QJsonArray {}},
|
||||
{QStringLiteral("env"), QJsonArray {QStringLiteral("LANG=C")}},
|
||||
{QStringLiteral("session"), QStringLiteral("gnome")}}));
|
||||
client.flush();
|
||||
QTRY_COMPARE(created.count(), 1);
|
||||
QCOMPARE(created.first().first().toString(), QStringLiteral("alice"));
|
||||
QTest::qWait(50);
|
||||
QCOMPARE(started.count(), 0);
|
||||
|
||||
server.respond(dm::GreeterServer::authMessage(QStringLiteral("secret"), QStringLiteral("Password:")));
|
||||
QTRY_COMPARE(started.count(), 1);
|
||||
QCOMPARE(started.first().at(1).toStringList(), QStringList {QStringLiteral("LANG=C")});
|
||||
QCOMPARE(started.first().at(2).toString(), QStringLiteral("gnome"));
|
||||
server.respond(dm::GreeterServer::success());
|
||||
|
||||
dm::FrameReader reader;
|
||||
QList<QJsonObject> responses;
|
||||
QTRY_VERIFY([&] {
|
||||
reader.append(client.readAll());
|
||||
while (auto response = reader.next())
|
||||
responses.append(*response);
|
||||
return responses.size() == 2;
|
||||
}());
|
||||
QCOMPARE(responses[0].value(QStringLiteral("type")).toString(), QStringLiteral("auth_message"));
|
||||
QCOMPARE(responses[0].value(QStringLiteral("auth_message_type")).toString(),
|
||||
QStringLiteral("secret"));
|
||||
QCOMPARE(responses[1].value(QStringLiteral("type")).toString(), QStringLiteral("success"));
|
||||
}
|
||||
|
||||
void helperProtocolRunsALogin()
|
||||
{
|
||||
qputenv("CUTEFISH_DM_HELPER", QCoreApplication::applicationFilePath().toLocal8Bit());
|
||||
qputenv(kFakeHelper, "1");
|
||||
dm::HelperProcess helper;
|
||||
QSignalSpy questions(&helper, &dm::HelperProcess::question);
|
||||
QSignalSpy authenticated(&helper, &dm::HelperProcess::authenticated);
|
||||
QSignalSpy started(&helper, &dm::HelperProcess::started);
|
||||
QSignalSpy finished(&helper, &dm::HelperProcess::finished);
|
||||
helper.login(QStringLiteral("cutefish-dm"), QStringLiteral("alice"), true, 2);
|
||||
QTRY_COMPARE(questions.count(), 1);
|
||||
QCOMPARE(questions.first().first().toString(), QStringLiteral("secret"));
|
||||
helper.answer(QStringLiteral("right"));
|
||||
QTRY_COMPARE(authenticated.count(), 1);
|
||||
QVERIFY(helper.isAuthenticated());
|
||||
helper.start({QStringLiteral("/bin/true")}, {QStringLiteral("XDG_VTNR=2")});
|
||||
QTRY_COMPARE(started.count(), 1);
|
||||
QCOMPARE(helper.sessionId(), QStringLiteral("42"));
|
||||
QVERIFY(helper.hasStarted());
|
||||
helper.terminate();
|
||||
QTRY_COMPARE(finished.count(), 1);
|
||||
qunsetenv(kFakeHelper);
|
||||
}
|
||||
|
||||
void helperReportsAWrongPassword()
|
||||
{
|
||||
qputenv("CUTEFISH_DM_HELPER", QCoreApplication::applicationFilePath().toLocal8Bit());
|
||||
qputenv(kFakeHelper, "1");
|
||||
dm::HelperProcess helper;
|
||||
QSignalSpy failed(&helper, &dm::HelperProcess::failed);
|
||||
QSignalSpy finished(&helper, &dm::HelperProcess::finished);
|
||||
QSignalSpy questions(&helper, &dm::HelperProcess::question);
|
||||
helper.login(QStringLiteral("cutefish-dm"), QStringLiteral("alice"), true, 2);
|
||||
QTRY_COMPARE(questions.count(), 1);
|
||||
helper.answer(QStringLiteral("wrong"));
|
||||
QTRY_COMPARE(finished.count(), 1);
|
||||
QCOMPARE(failed.count(), 1);
|
||||
QVERIFY(failed.first().first().toBool());
|
||||
QVERIFY(!helper.isAuthenticated());
|
||||
qunsetenv(kFakeHelper);
|
||||
}
|
||||
};
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
if (qEnvironmentVariableIsSet(kFakeHelper))
|
||||
return fakeHelper();
|
||||
QCoreApplication app(argc, argv);
|
||||
DisplayManagerTest test;
|
||||
return QTest::qExec(&test, argc, argv);
|
||||
}
|
||||
|
||||
#include "dm_test.moc"
|
||||
Loading…
Reference in New Issue