diff --git a/.gitignore b/.gitignore index df956b4..7d372be 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,8 @@ /.pytest_cache/ **/__pycache__/ +/shell/build/ +/shell/.cache/ + **/.DS_Store /.idea/ diff --git a/docs/building.rst b/docs/building.rst index 45e7c56..d013f21 100644 --- a/docs/building.rst +++ b/docs/building.rst @@ -156,29 +156,69 @@ exporting artifacts. QEMU startup ---------------------- -``mosaic-shell.service`` starts ``/usr/bin/mosaic-shell`` on VT1 as the locked, -non-login ``mosaic`` account with ``video`` and ``input`` group membership. It waits +QEMU images boot to ``graphical.target``, which starts ``mosaic-shell.service``. +The service runs ``/usr/bin/mosaic-shell`` on VT1 as the locked, +non-login ``mosaic`` account with ``video``, ``input``, and ``render`` group membership. It waits for DRM card0, VT1, and systemd user-session initialization, and conflicts with -``getty@tty1.service``. Use the serial console for administration and diagnostics. +``getty@tty1.service``. If VT1 already has a controlling process, startup fails +instead of waiting for that process to release it. Use the serial console for +administration and diagnostics. systemd creates ``/run/mosaic`` with mode ``0700`` and sets ``XDG_RUNTIME_DIR`` to that directory. +It also creates ``/var/lib/mosaic`` with mode ``0750`` as ``HOME`` and +``/var/cache/mosaic`` with mode ``0700`` as ``XDG_CACHE_HOME``. These directories +are owned by ``mosaic`` and persist across service restarts and boots. +Output goes to journald, and failures restart after two seconds, with at most +five starts per minute. The product layer installs ``/etc/mosaic/session.env`` with: .. code-block:: text - QT_QPA_PLATFORM=linuxfb - QT_QPA_FB_DRM=1 - QT_QUICK_BACKEND=software - -These settings select LinuxFB using DRM dumb buffers and software rendering. -The session includes Qt platform/QML plugins and DejaVu Sans. It uses the pinned -Qt recipe's ``linuxfb``, ``kms``, ``libinput``, and ``udev`` support. Product graphics -policy belongs in ``meta-mosaic-product``, including the service, environment file, -and udev rules under ``recipes-shell/mosaic-session/``. To change the environment -on a running image, edit ``/etc/mosaic/session.env`` and restart the service. + QT_QPA_PLATFORM=eglfs + QT_QPA_EGLFS_INTEGRATION=eglfs_kms + QT_QPA_EGLFS_KMS_CONFIG=/etc/mosaic/eglfs_kms.json + QT_QPA_EGLFS_HIDECURSOR=1 + QSG_RHI_BACKEND=opengl + QT_SHADER_CACHE_PATH=/var/cache/mosaic + +These settings select EGLFS/KMS and Qt Quick's OpenGL renderer. The installed +``/etc/mosaic/eglfs_kms.json`` selects ``/dev/dri/card0`` and disables the hardware +cursor. The session hides the pointer and stores shader cache files in its +persistent cache directory. Udev rules grant the ``video`` group access to card0 +and the ``render`` group access to DRM render nodes. + +The QEMU distro override enables ``opengl``. The pinned Qt recipe then enables +``gbm``, ``gles2``, and ``eglfs`` alongside ``kms``, ``libinput``, and ``udev``; +Mesa provides EGL, GLES, GBM, and runtime drivers. The session includes Mesa's +runtime drivers explicitly because they are loaded dynamically, along with Qt +platform/QML plugins and DejaVu Sans. The standard virtio GPU uses Mesa's +software rendering fallback, so host GPU acceleration is not required. +This uses the OpenGL rendering pipeline rather than Qt Quick's software backend. +STM32 graphics configuration is unchanged. + +Product graphics policy belongs in ``meta-mosaic-product``, including the service, +environment file, KMS configuration, and udev rules under +``recipes-shell/mosaic-session/``. To change the display setup on a running image, +edit ``/etc/mosaic/session.env`` or ``/etc/mosaic/eglfs_kms.json`` and restart the service. A missing environment file or failed display/QML startup fails the service. +Lock-screen authentication +-------------------------- + +The shell package installs ``/etc/pam.d/mosaic-shell``. The authentication backend +selects this PAM service to check the current process user's password and account +status using the system's ``common-auth`` and ``common-account`` stacks, like sudo. +The backend rejects empty passwords and does not open a new login session or +change passwords. ``libpam-runtime`` supplies the common policies, authentication +modules, and password-checking helper. Local edits to the Mosaic policy are +preserved during package upgrades. + +Under ``mosaic-shell.service``, the authenticated user is ``mosaic``. The current +boot configuration still locks that account's password and sets +``NoNewPrivileges=yes``. Password provisioning and permission for PAM's +``unix_chkpwd`` helper must be configured before unlocking can work in the image. + Add another machine ------------------- diff --git a/shell/.clang-format b/shell/.clang-format new file mode 100644 index 0000000..2d0acfc --- /dev/null +++ b/shell/.clang-format @@ -0,0 +1,104 @@ +# Generated by CLion for QT +BasedOnStyle: LLVM + +AccessModifierOffset: -8 +AlignAfterOpenBracket: AlwaysBreak +AlignConsecutiveAssignments: None +AlignConsecutiveDeclarations: None +AlignEscapedNewlines: DontAlign +AlignOperands: Align +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: true + AfterControlStatement: Never + AfterEnum: false + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: true + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakBeforeBinaryOperators: All +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 100 +CommentPragmas: "^ IWYU pragma:" +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 8 +ContinuationIndentWidth: 8 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - forever # avoids { wrapped to next line + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeCategories: + - Regex: "^ 0 + color: "red" + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } +} diff --git a/shell/qml/Main.qml b/shell/qml/Main.qml index b33606a..85d9b21 100644 --- a/shell/qml/Main.qml +++ b/shell/qml/Main.qml @@ -8,10 +8,7 @@ Window { title: qsTr("Mosaic") color: "white" - Text { - text: "Mosaic" - color: "black" - font.pixelSize: 48 - anchors.centerIn: parent + Shell { + anchors.fill: parent } } diff --git a/shell/qml/Shell.qml b/shell/qml/Shell.qml new file mode 100644 index 0000000..6b55270 --- /dev/null +++ b/shell/qml/Shell.qml @@ -0,0 +1,23 @@ +import QtQuick +import Mosaic + +Item { + HomeScreen { + anchors.fill: parent + visible: !SessionController.locked + enabled: !SessionController.locked + } + + LockScreen { + anchors.fill: parent + z: 1 + + visible: SessionController.locked + enabled: SessionController.locked + errorMessage: SessionController.error_msg + + onUnlockRequested: (password) => { + SessionController.requestUnlock(password) + } + } +} diff --git a/shell/src/auth/AuthenticationBackend.h b/shell/src/auth/AuthenticationBackend.h new file mode 100644 index 0000000..779eded --- /dev/null +++ b/shell/src/auth/AuthenticationBackend.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +class AuthenticationBackend : public QObject +{ + Q_OBJECT + +public: + enum class Result { SUCCESS, REJECTED, BACKENDFAILURE }; + Q_ENUM(Result) + + explicit AuthenticationBackend(QObject *parent = nullptr) + : QObject(parent) + {} + + virtual void authenticate(quint64 request_id, QString password) = 0; + +signals: + void authenticationFinished(quint64 request_id, Result result); +}; diff --git a/shell/src/auth/PamAuthenticationBackend.cpp b/shell/src/auth/PamAuthenticationBackend.cpp new file mode 100644 index 0000000..b386393 --- /dev/null +++ b/shell/src/auth/PamAuthenticationBackend.cpp @@ -0,0 +1,164 @@ +#include "PamAuthenticationBackend.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int PamAuthenticationBackend::conversationCallback( + int messageCount, const pam_message **messages, pam_response **responses, void *appdata) +{ + if (responses == nullptr) + return PAM_CONV_ERR; + + *responses = nullptr; + if (messageCount <= 0 || messageCount > PAM_MAX_NUM_MSG || messages == nullptr + || appdata == nullptr) + return PAM_CONV_ERR; + + const auto *context = static_cast(appdata); + auto *replies = static_cast( + std::calloc(static_cast(messageCount), sizeof(pam_response))); + if (replies == nullptr) + return PAM_BUF_ERR; + + const auto fail = [replies, messageCount](int status) { + for (int i = 0; i < messageCount; ++i) + std::free(replies[i].resp); + std::free(replies); + return status; + }; + + for (int i = 0; i < messageCount; ++i) { + if (messages[i] == nullptr) + return fail(PAM_CONV_ERR); + + const QByteArray *answer = nullptr; + switch (messages[i]->msg_style) { + case PAM_PROMPT_ECHO_OFF: + answer = &context->password; + break; + case PAM_PROMPT_ECHO_ON: + answer = &context->username; + break; + case PAM_TEXT_INFO: + case PAM_ERROR_MSG: + continue; + default: + return fail(PAM_CONV_ERR); + } + + const auto length = static_cast(answer->size()); + if (std::memchr(answer->constData(), '\0', length) != nullptr) + return fail(PAM_CONV_ERR); + + replies[i].resp = static_cast(std::malloc(length + 1)); + if (replies[i].resp == nullptr) + return fail(PAM_BUF_ERR); + + std::memcpy(replies[i].resp, answer->constData(), length); + replies[i].resp[length] = '\0'; + } + + *responses = replies; + return PAM_SUCCESS; +} + +void PamAuthenticationBackend::authenticate(quint64 request_id, QString password) +{ + auto *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, [this, watcher, request_id] { + const Result result = watcher->result(); + watcher->deleteLater(); + emit authenticationFinished(request_id, result); + }); + + // The worker owns its input and does not access the backend object. + watcher->setFuture(QtConcurrent::run([password = std::move(password)]() mutable { + try { + return authenticateBlocking(std::move(password)); + } catch (...) { + return Result::BACKENDFAILURE; + } + })); +} + +AuthenticationBackend::Result PamAuthenticationBackend::authenticateBlocking(QString password) +{ + const QByteArray username = getCurrentUsername(); + + struct ConversationContext context + { + username, password.toUtf8() + }; + + pam_conv conversation{conversationCallback, &context}; + + pam_handle_t *handle = nullptr; + + int status = pam_start("mosaic-shell", context.username.constData(), &conversation, &handle); + if (status != PAM_SUCCESS) + return Result::BACKENDFAILURE; + + status = pam_authenticate(handle, PAM_DISALLOW_NULL_AUTHTOK); + if (status != PAM_SUCCESS) { + pam_end(handle, status); + return resolveResultFromPamStatus(status); + } + + status = pam_acct_mgmt(handle, 0); + if (status != PAM_SUCCESS) { + pam_end(handle, status); + return resolveResultFromPamStatus(status); + } + + status = pam_end(handle, status); + return resolveResultFromPamStatus(status); +} + +QByteArray PamAuthenticationBackend::getCurrentUsername() +{ + passwd entry{}; + passwd *result = nullptr; + std::vector buffer(4096); + + int status; + while (true) { + status = getpwuid_r(getuid(), &entry, buffer.data(), buffer.size(), &result); + + if (status != ERANGE) + break; + + buffer.resize(buffer.size() * 2); + } + + if (status != 0 || result == nullptr) + throw std::runtime_error("Unable to resolve current user"); + + return QByteArray(entry.pw_name); +} + +AuthenticationBackend::Result PamAuthenticationBackend::resolveResultFromPamStatus(int status) +{ + switch (status) { + case PAM_AUTH_ERR: + case PAM_USER_UNKNOWN: + case PAM_MAXTRIES: + case PAM_PERM_DENIED: + case PAM_ACCT_EXPIRED: + case PAM_NEW_AUTHTOK_REQD: + case PAM_AUTHTOK_EXPIRED: + return Result::REJECTED; + case PAM_SUCCESS: + return Result::SUCCESS; + default: + return Result::BACKENDFAILURE; + } +} diff --git a/shell/src/auth/PamAuthenticationBackend.h b/shell/src/auth/PamAuthenticationBackend.h new file mode 100644 index 0000000..4a7a47a --- /dev/null +++ b/shell/src/auth/PamAuthenticationBackend.h @@ -0,0 +1,39 @@ +#pragma once + +#include "AuthenticationBackend.h" + +#include +#include +#include + +struct pam_message; +struct pam_response; + +class PamAuthenticationBackend : public AuthenticationBackend +{ +public: + explicit PamAuthenticationBackend(QObject *parent = nullptr) + : AuthenticationBackend(parent) + {} + + void authenticate(quint64 request_id, QString password) override; + +private: + struct ConversationContext + { + QByteArray username; + QByteArray password; + }; + + static Result authenticateBlocking(QString password); + + static QByteArray getCurrentUsername(); + + static Result resolveResultFromPamStatus(int status); + + static int conversationCallback( + int messageCount, + const pam_message **messages, + pam_response **responses, + void *appdata); +}; diff --git a/shell/src/main.cpp b/shell/src/main.cpp index 3c79bd5..8a56b47 100644 --- a/shell/src/main.cpp +++ b/shell/src/main.cpp @@ -1,14 +1,15 @@ #include #include -int main(int argc, char* argv[]) { - QGuiApplication app(argc, argv); - QQmlApplicationEngine engine; +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + QQmlApplicationEngine engine; - QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QGuiApplication::quit); - QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QGuiApplication::quit); + QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QGuiApplication::quit); + QObject::connect(&engine, &QQmlApplicationEngine::quit, &app, &QGuiApplication::quit); - engine.loadFromModule("Mosaic", "Main"); + engine.loadFromModule("Mosaic", "Main"); - return app.exec(); + return app.exec(); } diff --git a/shell/src/session/sessionController.cpp b/shell/src/session/sessionController.cpp new file mode 100644 index 0000000..69e832d --- /dev/null +++ b/shell/src/session/sessionController.cpp @@ -0,0 +1,81 @@ +#include "sessionController.h" + +SessionController::SessionController(QObject *parent) + : QObject(parent) +{ + connect(&authenticationBackend, + &AuthenticationBackend::authenticationFinished, + this, + [this](quint64 requestId, AuthenticationBackend::Result result) { + if (requestId != m_requestId || m_state != State::AUTHENTICATING) { + return; + } + + switch(result) { + case AuthenticationBackend::Result::SUCCESS: + setErrorMsg(""); + setState(State::UNLOCKED); + break; + case AuthenticationBackend::Result::REJECTED: + setErrorMsg("Authentication rejected"); + setState(State::LOCKED); + break; + case AuthenticationBackend::Result::BACKENDFAILURE: + setErrorMsg("Authentication unavailable"); + setState(State::LOCKED); + break; + } + }); +} + +QString SessionController::getErrorMsg() const +{ + return error_msg; +} + +bool SessionController::isLocked() const +{ + return m_state != State::UNLOCKED; +} + +void SessionController::lock() +{ + ++m_requestId; + setState(State::LOCKED); +} + +void SessionController::requestUnlock(const QString &password) +{ + if (m_state != State::LOCKED) { + return; + } + + setErrorMsg(""); + + if (password.isEmpty()) { + setErrorMsg("Password cannot be empty"); + return; + } + + ++m_requestId; + setState(State::AUTHENTICATING); + authenticationBackend.authenticate(m_requestId, password); +} + +void SessionController::setState(const State state) +{ + if (m_state == state) + return; + + m_state = state; + emit stateChanged(); +} + +void SessionController::setErrorMsg(const QString &msg) +{ + if (error_msg == msg) + return; + + error_msg = msg; + emit errorMsgChanged(); +} diff --git a/shell/src/session/sessionController.h b/shell/src/session/sessionController.h new file mode 100644 index 0000000..37771e0 --- /dev/null +++ b/shell/src/session/sessionController.h @@ -0,0 +1,42 @@ +#pragma once + +#include "PamAuthenticationBackend.h" +#include +#include +#include + +class SessionController : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(bool locked READ isLocked NOTIFY stateChanged) + Q_PROPERTY(QString error_msg READ getErrorMsg NOTIFY errorMsgChanged) + +public: + enum class State { LOCKED, AUTHENTICATING, UNLOCKED }; + Q_ENUM(State) + + explicit SessionController(QObject *parent = nullptr); + + QString getErrorMsg() const; + bool isLocked() const; + + Q_INVOKABLE void lock(); + Q_INVOKABLE void requestUnlock(const QString &password); + +signals: + void stateChanged(); + void errorMsgChanged(); + +private: + PamAuthenticationBackend authenticationBackend; + quint64 m_requestId = 0; + + State m_state = State::LOCKED; + QString error_msg; + + void setState(const State state); + void setErrorMsg(const QString &msg); +}; diff --git a/yocto/layers/meta-mosaic-product/README b/yocto/layers/meta-mosaic-product/README index cd21eaa..a34d196 100644 --- a/yocto/layers/meta-mosaic-product/README +++ b/yocto/layers/meta-mosaic-product/README @@ -13,20 +13,31 @@ Both qemu-dev and qemu-prod use 512 MiB RAM and the upstream virtio GPU, USB keyboard, and USB tablet configuration. STM32 profiles include the shell without this graphical startup package. -mosaic-session installs and enables mosaic-shell.service on multi-user.target. +mosaic-session installs and enables mosaic-shell.service on graphical.target. +The QEMU image boots to graphical.target in both dev and prod profiles. The service runs /usr/bin/mosaic-shell as the locked, non-login mosaic account, -with video and input group membership. A udev rule grants that account exclusive -access to tty1 and grants the video group read/write access to DRM card0. +with video, input, and render group membership. A udev rule grants that account +exclusive access to tty1 and grants the video group read/write access to DRM card0. +DRM render nodes are accessible to the render group. Input access uses the standard udev input group permissions. The service waits for card0, tty1, and systemd-user-sessions.service. It owns VT1 and conflicts with getty@tty1.service. The serial console remains available. +If another process already controls VT1, startup fails instead of waiting for it. systemd creates /run/mosaic with mode 0700 and sets XDG_RUNTIME_DIR to that path. - -/etc/mosaic/session.env selects Qt LinuxFB with DRM dumb buffers and software -Qt Quick rendering. The session depends on shell, Qt platform/QML plugins, -and DejaVu Sans. The pinned qtbase recipe enables linuxfb, kms, libinput, and -udev with the current distro configuration. No compositor is selected. +It also creates /var/lib/mosaic with mode 0750 as HOME and /var/cache/mosaic +with mode 0700 as XDG_CACHE_HOME. Both persist across service restarts and boots. + +/etc/mosaic/session.env selects Qt EGLFS with the eglfs_kms integration and +the OpenGL Qt Quick renderer. /etc/mosaic/eglfs_kms.json selects DRM card0 and +disables the hardware cursor; the session hides the pointer entirely. Shader +cache files use /var/cache/mosaic. + +The QEMU distro override enables opengl, which selects EGL, GLES, GBM, and EGLFS +in the pinned Qt/Mesa recipes. The session includes Mesa's runtime drivers, +Qt platform/QML plugins, and DejaVu Sans. The standard virtio GPU uses Mesa's +software rendering fallback; host GPU acceleration is not required. STM32 +graphics policy is unchanged. Mosaic owns the display directly through EGLFS. Output goes to journald. Failed processes restart after two seconds, with at most five starts in a 60-second interval. After correcting a persistent failure, use diff --git a/yocto/layers/meta-mosaic-product/conf/distro/mosaic.conf b/yocto/layers/meta-mosaic-product/conf/distro/mosaic.conf index db61bf5..d0dfa9f 100644 --- a/yocto/layers/meta-mosaic-product/conf/distro/mosaic.conf +++ b/yocto/layers/meta-mosaic-product/conf/distro/mosaic.conf @@ -20,7 +20,10 @@ FULL_OPTIMIZATION = "-Os ${DEBUG_LEVELFLAG}" # explicit feature baseline DISTRO_FEATURES_OPTED_OUT = "*" -DISTRO_FEATURES = "pci ldconfig ptest multiarch ipv4 ipv6 wayland" +DISTRO_FEATURES = "pci ldconfig pam ptest multiarch ipv4 ipv6 wayland" + +# The QEMU session uses Qt EGLFS/KMS with Mesa EGL, GLES, and GBM. +DISTRO_FEATURES:append:qemuarm64 = " opengl" PACKAGECONFIG:remove:pn-systemd = "hibernate localed machined nss-mymachines" diff --git a/yocto/layers/meta-mosaic-product/recipes-core/images/mosaic-image.bb b/yocto/layers/meta-mosaic-product/recipes-core/images/mosaic-image.bb index c37aa4d..74b13ba 100644 --- a/yocto/layers/meta-mosaic-product/recipes-core/images/mosaic-image.bb +++ b/yocto/layers/meta-mosaic-product/recipes-core/images/mosaic-image.bb @@ -7,5 +7,6 @@ DESCRIPTION = "Machine-independent Mosaic image with development and production IMAGE_BASENAME = "mosaic-image-${MOSAIC_BUILD_PROFILE}" IMAGE_INSTALL:append = " packagegroup-mosaic packagegroup-mosaic-apps" +SYSTEMD_DEFAULT_TARGET:qemuarm64 = "graphical.target" IMAGE_FEATURES:append:mosaic-dev = " tools-debug dbg-pkgs" IMAGE_FEATURES:remove:mosaic-prod = " tools-debug dbg-pkgs" diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/70-mosaic-session.rules b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/70-mosaic-session.rules index 8619c07..9fc48be 100644 --- a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/70-mosaic-session.rules +++ b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/70-mosaic-session.rules @@ -1,2 +1,3 @@ SUBSYSTEM=="tty", KERNEL=="tty1", OWNER="mosaic", GROUP="mosaic", MODE="0600", TAG+="systemd" SUBSYSTEM=="drm", KERNEL=="card0", GROUP="video", MODE="0660", TAG+="systemd" +SUBSYSTEM=="drm", KERNEL=="renderD*", GROUP="render", MODE="0660" diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/eglfs_kms.json b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/eglfs_kms.json new file mode 100644 index 0000000..db7a503 --- /dev/null +++ b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/eglfs_kms.json @@ -0,0 +1,4 @@ +{ + "device": "/dev/dri/card0", + "hwcursor": false +} diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/mosaic-shell.service b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/mosaic-shell.service index 8df6180..77e860d 100644 --- a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/mosaic-shell.service +++ b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/mosaic-shell.service @@ -1,10 +1,9 @@ [Unit] Description=Mosaic graphical shell -Requires=dev-dri-card0.device dev-tty1.device -Wants=systemd-user-sessions.service +Requires=dev-dri-card0.device dev-tty1.device systemd-user-sessions.service After=dev-dri-card0.device dev-tty1.device systemd-user-sessions.service Conflicts=getty@tty1.service -Before=getty@tty1.service +Before=getty@tty1.service graphical.target StartLimitIntervalSec=60 StartLimitBurst=5 @@ -12,14 +11,20 @@ StartLimitBurst=5 Type=simple User=mosaic Group=mosaic -SupplementaryGroups=video input +SupplementaryGroups=video input render +StateDirectory=mosaic +StateDirectoryMode=0750 +CacheDirectory=mosaic +CacheDirectoryMode=0700 RuntimeDirectory=mosaic RuntimeDirectoryMode=0700 Environment=XDG_RUNTIME_DIR=/run/mosaic +Environment=HOME=/var/lib/mosaic +Environment=XDG_CACHE_HOME=/var/cache/mosaic EnvironmentFile=/etc/mosaic/session.env ExecStart=/usr/bin/mosaic-shell TTYPath=/dev/tty1 -StandardInput=tty +StandardInput=tty-fail TTYReset=yes TTYVHangup=yes TTYVTDisallocate=yes @@ -32,4 +37,4 @@ CapabilityBoundingSet= UMask=0077 [Install] -WantedBy=multi-user.target +WantedBy=graphical.target diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/session.env b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/session.env index 06ae42e..de5a5cc 100644 --- a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/session.env +++ b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/files/session.env @@ -1,3 +1,6 @@ -QT_QPA_PLATFORM=linuxfb -QT_QPA_FB_DRM=1 -QT_QUICK_BACKEND=software +QT_QPA_PLATFORM=eglfs +QT_QPA_EGLFS_INTEGRATION=eglfs_kms +QT_QPA_EGLFS_KMS_CONFIG=/etc/mosaic/eglfs_kms.json +QT_QPA_EGLFS_HIDECURSOR=1 +QSG_RHI_BACKEND=opengl +QT_SHADER_CACHE_PATH=/var/cache/mosaic diff --git a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/mosaic-session_0.1.bb b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/mosaic-session_0.1.bb index 8eb71f5..4b28ad8 100644 --- a/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/mosaic-session_0.1.bb +++ b/yocto/layers/meta-mosaic-product/recipes-shell/mosaic-session/mosaic-session_0.1.bb @@ -5,6 +5,7 @@ LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/GPL-3.0-only;md5=c79ff39f19dfec SRC_URI = " \ file://mosaic-shell.service \ file://session.env \ + file://eglfs_kms.json \ file://70-mosaic-session.rules \ " S = "${UNPACKDIR}" @@ -13,11 +14,11 @@ COMPATIBLE_MACHINE = "^qemuarm64$" inherit systemd useradd -RDEPENDS:${PN} = "shell qtbase-plugins qtdeclarative-qmlplugins ttf-dejavu-sans" +RDEPENDS:${PN} = "shell qtbase-plugins qtdeclarative-qmlplugins mesa-megadriver ttf-dejavu-sans" USERADD_PACKAGES = "${PN}" -GROUPADD_PARAM:${PN} = "--system mosaic" -USERADD_PARAM:${PN} = "--system --gid mosaic --groups video,input --home-dir / \ +GROUPADD_PARAM:${PN} = "--system mosaic; --system render" +USERADD_PARAM:${PN} = "--system --gid mosaic --groups video,input,render --home-dir /var/lib/mosaic \ --no-create-home --shell ${base_sbindir}/nologin --password '!' mosaic" SYSTEMD_SERVICE:${PN} = "mosaic-shell.service" @@ -27,8 +28,9 @@ do_install() { install -d ${D}${systemd_system_unitdir} ${D}${sysconfdir}/mosaic ${D}${nonarch_libdir}/udev/rules.d install -m 0644 ${UNPACKDIR}/mosaic-shell.service ${D}${systemd_system_unitdir}/mosaic-shell.service install -m 0644 ${UNPACKDIR}/session.env ${D}${sysconfdir}/mosaic/session.env + install -m 0644 ${UNPACKDIR}/eglfs_kms.json ${D}${sysconfdir}/mosaic/eglfs_kms.json install -m 0644 ${UNPACKDIR}/70-mosaic-session.rules ${D}${nonarch_libdir}/udev/rules.d/70-mosaic-session.rules } -CONFFILES:${PN} = "${sysconfdir}/mosaic/session.env" +CONFFILES:${PN} = "${sysconfdir}/mosaic/session.env ${sysconfdir}/mosaic/eglfs_kms.json" FILES:${PN} += "${systemd_system_unitdir}/mosaic-shell.service ${nonarch_libdir}/udev/rules.d/70-mosaic-session.rules" diff --git a/yocto/layers/meta-mosaic/README b/yocto/layers/meta-mosaic/README index 991dcbd..55bfa89 100644 --- a/yocto/layers/meta-mosaic/README +++ b/yocto/layers/meta-mosaic/README @@ -38,8 +38,13 @@ Layer contents ============== shell: Builds the local shell/ sources with Qt 6 and installs the - executable as /usr/bin/mosaic-shell. The recipe declares Qt - platform plugins and Qt Quick QML imports as runtime dependencies. + executable as /usr/bin/mosaic-shell. The recipe depends on libpam + for the lock screen's authentication backend. Runtime dependencies + include Qt platform plugins, Qt Quick QML imports, and libpam-runtime + for PAM's common configuration, authentication modules, and helper. + It installs /etc/pam.d/mosaic-shell and marks it as package configuration. + The backend uses this policy's common-auth and common-account stacks + to authenticate the current process user. Build the package for a machine and profile: diff --git a/yocto/layers/meta-mosaic/recipes-shell/shell/files/mosaic-shell.pam b/yocto/layers/meta-mosaic/recipes-shell/shell/files/mosaic-shell.pam new file mode 100644 index 0000000..db7e24b --- /dev/null +++ b/yocto/layers/meta-mosaic/recipes-shell/shell/files/mosaic-shell.pam @@ -0,0 +1,4 @@ +#%PAM-1.0 +# Unlock the current user's Mosaic session using the system authentication policy. +auth include common-auth +account include common-account diff --git a/yocto/layers/meta-mosaic/recipes-shell/shell/shell_0.1.bb b/yocto/layers/meta-mosaic/recipes-shell/shell/shell_0.1.bb index ca1770f..abc96da 100644 --- a/yocto/layers/meta-mosaic/recipes-shell/shell/shell_0.1.bb +++ b/yocto/layers/meta-mosaic/recipes-shell/shell/shell_0.1.bb @@ -10,14 +10,17 @@ SRC_URI = " \ file://shell/src \ file://shell/qml \ file://LICENSE \ + file://mosaic-shell.pam \ " S = "${UNPACKDIR}/shell" inherit qt6-cmake -DEPENDS += "qtbase qtdeclarative qtdeclarative-native" +DEPENDS += "qtbase qtdeclarative qtdeclarative-native libpam" -RDEPENDS:${PN} += "qtbase-plugins qtdeclarative-qmlplugins" +RDEPENDS:${PN} += "qtbase-plugins qtdeclarative-qmlplugins libpam-runtime" + +CONFFILES:${PN} += "${sysconfdir}/pam.d/mosaic-shell" do_compile() { case "${MOSAIC_APP_LIFECYCLE}" in @@ -38,6 +41,7 @@ do_compile() { } do_install() { - install -d ${D}${bindir} + install -d ${D}${bindir} ${D}${sysconfdir}/pam.d install -m 0755 ${B}/app ${D}${bindir}/mosaic-shell + install -m 0644 ${UNPACKDIR}/mosaic-shell.pam ${D}${sysconfdir}/pam.d/mosaic-shell }