diff --git a/debian/control b/debian/control index 1129f7f..118d855 100644 --- a/debian/control +++ b/debian/control @@ -23,7 +23,7 @@ Provides: x-display-manager Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, - seatd, + dde-seatd, treeland, libpam-systemd, Description: a modern display manager for Wayland sessions aiming to be fast, simple and beautiful. diff --git a/services/CMakeLists.txt b/services/CMakeLists.txt index 1e0b7a3..25a6126 100644 --- a/services/CMakeLists.txt +++ b/services/CMakeLists.txt @@ -1,6 +1,11 @@ configure_file(ddm.service.in ddm.service) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ddm.service" DESTINATION "${SYSTEMD_SYSTEM_UNIT_DIR}") +configure_file(treeland-dde-seatd.conf.in treeland-dde-seatd.conf) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/treeland-dde-seatd.conf" + DESTINATION "${SYSTEMD_SYSTEM_UNIT_DIR}/treeland.service.d" + RENAME 10-dde-seatd.conf) + # systemd-tmpfiles can be used standalone without other systemd parts if(DEFINED SYSTEMD_TMPFILES_DIR) configure_file(ddm-tmpfiles.conf.in ddm-tmpfiles.conf) diff --git a/services/ddm.service.in b/services/ddm.service.in index c74e3dd..36b583d 100644 --- a/services/ddm.service.in +++ b/services/ddm.service.in @@ -2,13 +2,12 @@ Description=Simple Desktop Display Manager Documentation=man:ddm(1) man:ddm.conf(5) Conflicts=getty@tty${DDM_INITIAL_VT}.service -After=systemd-user-sessions.service getty@tty${DDM_INITIAL_VT}.service plymouth-quit.service systemd-logind.service +After=systemd-user-sessions.service getty@tty${DDM_INITIAL_VT}.service plymouth-quit.service systemd-logind.service dde-seatd.service PartOf=graphical.target StartLimitIntervalSec=30 StartLimitBurst=2 -Requires=seatd.service -Before=seatd.service +Requires=dde-seatd.service [Service] ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/ddm diff --git a/services/treeland-dde-seatd.conf.in b/services/treeland-dde-seatd.conf.in new file mode 100644 index 0000000..269e97c --- /dev/null +++ b/services/treeland-dde-seatd.conf.in @@ -0,0 +1,7 @@ +[Unit] +Requires=dde-seatd.service +After=dde-seatd.service + +[Service] +Environment=LIBSEAT_BACKEND=seatd +Environment=SEATD_SOCK=/run/dde-seatd.sock diff --git a/src/common/SignalHandler.cpp b/src/common/SignalHandler.cpp index c4ef5df..b2590f2 100644 --- a/src/common/SignalHandler.cpp +++ b/src/common/SignalHandler.cpp @@ -90,7 +90,7 @@ namespace DDM { sigemptyset(&sigterm.sa_mask); sigterm.sa_flags = SA_RESTART; - if (sigaction(SIGTERM, &sigterm, 0) > 0) { + if (sigaction(SIGTERM, &sigterm, 0) < 0) { qCritical() << "Failed to set up SIGTERM handler."; return; } @@ -106,10 +106,19 @@ namespace DDM { sigemptyset(&sigcustom.sa_mask); sigcustom.sa_flags = SA_RESTART; - if (sigaction(signal, &sigcustom, 0) > 0) { + if (sigaction(signal, &sigcustom, 0) < 0) { qCritical() << "Failed to set up " << strsignal(signal) << " handler."; return; } + + sigset_t set; + sigemptyset(&set); + sigaddset(&set, signal); + if (sigprocmask(SIG_UNBLOCK, &set, nullptr) < 0) { + qCritical() << "Failed to unblock " << strsignal(signal) << " handler."; + return; + } + customSignals.append(signal); } diff --git a/src/daemon/Display.cpp b/src/daemon/Display.cpp index 83f65e9..35de7e7 100644 --- a/src/daemon/Display.cpp +++ b/src/daemon/Display.cpp @@ -97,6 +97,29 @@ namespace DDM { return VirtualTerminal::setUpNewVt(); } + static bool isVtReservedByDdm(int vt) { + for (Display *display : daemonApp->seatManager()->displays) { + if (display->terminalId == vt) + return true; + for (Auth *auth : display->auths) { + if (auth->tty == vt) + return true; + } + } + return false; + } + + static int fetchAvailableUserVt() { + for (int vt = DDM_INITIAL_VT; vt <= MAX_NR_CONSOLES; ++vt) { + if (isVtReservedByDdm(vt)) + continue; + if (!isTtyInUse(QStringLiteral("tty%1").arg(vt))) + return vt; + } + + return VirtualTerminal::setUpNewVt(); + } + Display::Display(SeatManager *parent, QString name) : QObject(parent) , name(name) @@ -112,7 +135,7 @@ namespace DDM { {}, name, "dde", - static_cast(VirtualTerminal::currentVt())); + terminalId); // connect connected signal connect(m_socketServer, &SocketServer::connected, this, &Display::connected); @@ -138,6 +161,9 @@ namespace DDM { } void Display::activateSession(const QString &user, int xdgSessionId) { + qWarning() << "Display activateSession requested for user" << user + << "xdgSessionId" << xdgSessionId + << "display VT" << terminalId; if (xdgSessionId <= 0 && user != QStringLiteral("dde")) { qCritical() << "Invalid xdg session id" << xdgSessionId << "for user" << user; return; @@ -239,35 +265,48 @@ namespace DDM { qWarning() << "Existing authentication ongoing, aborting"; return; } + const bool insertedAuth = !auths.contains(auth); + if (insertedAuth) + auths << auth; // sanity check if (!session.isValid()) { qCritical() << "Invalid session" << session.fileName(); + if (insertedAuth) { + auths.removeAll(auth); + delete auth; + } return; } if (session.xdgSessionType().isEmpty()) { qCritical() << "Failed to find XDG session type for session" << session.fileName(); + if (insertedAuth) { + auths.removeAll(auth); + delete auth; + } return; } if (session.exec().isEmpty()) { qCritical() << "Failed to find command for session" << session.fileName(); + if (insertedAuth) { + auths.removeAll(auth); + delete auth; + } return; } + const QString sessionId = QStringLiteral("Session%1").arg(daemonApp->newSessionId()); + // Run password check - if (session.isSingleMode()) - auth->tty = VirtualTerminal::setUpNewVt(); - else - auth->tty = terminalId; if (!auth->authenticate(password.toLocal8Bit())) { + if (insertedAuth) { + auths.removeAll(auth); + delete auth; + } Q_EMIT loginFailed(socket, user); return; } - // some information - qInfo() << "Authentication succeeded for user" << user << ", opening session" - << session.fileName() << ", command:" << session.exec() << ", VT:" << auth->tty; - // save last user and last session DaemonApp::instance()->displayManager()->setLastActivatedUser(user); if (mainConfig.Users.RememberLastUser.get()) @@ -280,14 +319,39 @@ namespace DDM { stateConfig.Last.Session.setDefault(); stateConfig.save(); + auth->sessionId = sessionId; + + // Special preparation for each display server type + // + // TODO: Let Treeland drop DRM master when inactive, so that X + // server and other Wayland compositor can co-exist with + // greeter (the Treeland) + QByteArray cookie; + if (session.isSingleMode()) { + auth->type = Treeland; + auth->tty = daemonApp->treelandConnector()->createGroupVtForTreeland(user, sessionId); + if (auth->tty <= 0) { + qCritical() << "Failed to allocate grouped VT for Treeland user session"; + auths.removeAll(auth); + delete auth; + return; + } + } else if (session.xdgSessionType() == QLatin1String("x11")) { + auth->type = X11; + auth->tty = fetchAvailableUserVt(); + } else { + auth->type = Wayland; + auth->tty = fetchAvailableUserVt(); + } + + qInfo() << "Authentication succeeded for user" << user << ", opening session" + << session.fileName() << ", command:" << session.exec() << ", VT:" << auth->tty; + // Prepare session environment QProcessEnvironment env; env.insert(session.additionalEnv()); - // session id - const QString sessionId = QStringLiteral("Session%1").arg(daemonApp->newSessionId()); env.insert(QStringLiteral("XDG_SESSION_PATH"), daemonApp->displayManager()->sessionPath(sessionId)); - auth->sessionId = sessionId; env.insert(QStringLiteral("PATH"), mainConfig.Users.DefaultPath.get()); env.insert(QStringLiteral("DESKTOP_SESSION"), session.desktopSession()); @@ -300,18 +364,9 @@ namespace DDM { env.insert(QStringLiteral("XDG_SEAT_PATH"), daemonApp->displayManager()->seatPath(name)); env.insert(QStringLiteral("XDG_SESSION_DESKTOP"), session.desktopNames()); - // Special preparation for each display server type - // - // TODO: Let Treeland drop DRM master when inactive, so that X - // server and other Wayland compositor can co-exist with - // greeter (the Treeland) - QByteArray cookie; if (session.isSingleMode()) { - auth->type = Treeland; env.insert("DDE_CURRENT_COMPOSITOR", "TreeLand"); } else if (session.xdgSessionType() == QLatin1String("x11")) { - auth->type = X11; - qInfo() << "Stopping Treeland"; daemonApp->treelandConnector()->disconnect(); m_treeland->stop(); @@ -323,6 +378,10 @@ namespace DDM { connect(m_x11Server, &XorgDisplayServer::stopped, this, &Display::stop); if (!m_x11Server->start(auth->tty)) { qCritical() << "Failed to start X11 display server"; + delete m_x11Server; + m_x11Server = nullptr; + auths.removeAll(auth); + delete auth; return; } m_x11Server->setupDisplay(); @@ -330,8 +389,6 @@ namespace DDM { env.insert(QStringLiteral("DISPLAY"), m_x11Server->display); cookie = m_x11Server->cookie(); } else { - auth->type = Wayland; - qInfo() << "Stopping Treeland"; daemonApp->treelandConnector()->disconnect(); m_treeland->stop(); @@ -343,6 +400,9 @@ namespace DDM { if (xdgSessionId <= 0) { qCritical() << "Failed to open logind session for user" << user; + if (auth->type == Treeland) + daemonApp->treelandConnector()->destroyGroupVt(auth->tty); + auths.removeAll(auth); delete auth; return; } @@ -351,14 +411,15 @@ namespace DDM { qWarning() << "Session for user" << auth->user << "finished"; auths.removeAll(auth); daemonApp->displayManager()->RemoveSession(auth->sessionId); + if (auth->type == Treeland) + daemonApp->treelandConnector()->destroyGroupVt(auth->tty); delete auth; }); daemonApp->displayManager()->AddSession(sessionId, name, user, auth->tty); daemonApp->displayManager()->setLastSession(sessionId); - // The user process is ongoing, append to active auths - // The auth will be delete later in userProcessFinished() - auths << auth; + if (auth->type == Treeland) + activateSession(user, xdgSessionId); qInfo() << "Successfully logged in user" << user; } @@ -419,7 +480,10 @@ namespace DDM { Logind::managerPath(), QDBusConnection::systemBus()); manager.UnlockSession(QString::number(auth->xdgSessionId)); - VirtualTerminal::jumpToVt(auth->tty, false); + if (auth->type == Treeland) + activateSession(user, auth->xdgSessionId); + else + VirtualTerminal::jumpToVt(auth->tty, false, false); qInfo() << "Successfully identified user" << user; return; } diff --git a/src/daemon/DisplayManager.cpp b/src/daemon/DisplayManager.cpp index fe698db..f36fe29 100644 --- a/src/daemon/DisplayManager.cpp +++ b/src/daemon/DisplayManager.cpp @@ -28,12 +28,45 @@ #include "seatadaptor.h" #include "sessionadaptor.h" +#include +#include +#include + const QString DISPLAYMANAGER_SERVICE = QStringLiteral("org.freedesktop.DisplayManager"); const QString DISPLAYMANAGER_PATH = QStringLiteral("/org/freedesktop/DisplayManager"); const QString DISPLAYMANAGER_SEAT_PATH = QStringLiteral("/org/freedesktop/DisplayManager/Seat"); const QString DISPLAYMANAGER_SESSION_PATH = QStringLiteral("/org/freedesktop/DisplayManager/Session"); namespace DDM { + static QString dbusCallerDescription(const QDBusContext &context) { + if (!context.calledFromDBus()) + return QStringLiteral("non-dbus"); + + const auto service = context.message().service(); + auto description = QStringLiteral("service=%1").arg(service); + + auto *interface = QDBusConnection::systemBus().interface(); + if (!interface) + return description; + + const QDBusReply uidReply = interface->serviceUid(service); + if (uidReply.isValid()) + description += QStringLiteral(" uid=%1").arg(uidReply.value()); + + const QDBusReply pidReply = interface->servicePid(service); + if (!pidReply.isValid()) + return description; + + description += QStringLiteral(" pid=%1").arg(pidReply.value()); + + const QFileInfo exeInfo(QStringLiteral("/proc/%1/exe").arg(pidReply.value())); + const auto exePath = exeInfo.symLinkTarget(); + if (!exePath.isEmpty()) + description += QStringLiteral(" exe=%1").arg(exePath); + + return description; + } + DisplayManager::DisplayManager(QObject *parent) : QObject(parent) { // create adaptor new DisplayManagerAdaptor(this); @@ -207,6 +240,8 @@ namespace DDM { } void DisplayManagerSeat::SwitchToGreeter() { + qWarning() << "DisplayManagerSeat::SwitchToGreeter requested for seat" << m_name + << "from" << dbusCallerDescription(*this); daemonApp->seatManager()->switchToGreeter(m_name); } diff --git a/src/daemon/DisplayManager.h b/src/daemon/DisplayManager.h index f3dd217..7857e97 100644 --- a/src/daemon/DisplayManager.h +++ b/src/daemon/DisplayManager.h @@ -23,6 +23,7 @@ #include +#include #include #include #include @@ -92,7 +93,7 @@ namespace DDM { /*************************************************************************** * org.freedesktop.DisplayManager.Seat **************************************************************************/ - class DisplayManagerSeat: public QObject { + class DisplayManagerSeat: public QObject, protected QDBusContext { Q_OBJECT Q_DISABLE_COPY(DisplayManagerSeat) Q_PROPERTY(bool CanSwitch READ CanSwitch CONSTANT) diff --git a/src/daemon/TreelandConnector.cpp b/src/daemon/TreelandConnector.cpp index 66457f7..6aa4cf0 100644 --- a/src/daemon/TreelandConnector.cpp +++ b/src/daemon/TreelandConnector.cpp @@ -7,29 +7,151 @@ #include "Display.h" #include "DisplayManager.h" #include "SeatManager.h" -#include "VirtualTerminal.h" #include "treeland-ddm-v1.h" #include +#include +#include +#include +#include +#include #include -#include #include -#include +#include #include #include +#include +#include +#include +#include #include #include +#include +#include +#include +#include +#include +#include #include -#include -#include namespace DDM { -// Virtural Terminal helper from VirturalTerminal.h +static constexpr auto controlSocketPath = "/run/dde-seatd-control.sock"; +static constexpr auto systemdService = "org.freedesktop.systemd1"; +static constexpr auto systemdPath = "/org/freedesktop/systemd1"; +static constexpr auto systemdManagerInterface = "org.freedesktop.systemd1.Manager"; +static constexpr auto systemdPropertiesInterface = "org.freedesktop.DBus.Properties"; +static constexpr auto systemdServiceInterface = "org.freedesktop.systemd1.Service"; +static constexpr auto treelandUnit = "treeland.service"; +static constexpr uint16_t maxControlPayloadSize = 4096; + +enum ControlOpcode : uint16_t { + ControlCreateGroupVt = 1, + ControlDestroyGroupVt = 2, + ControlGroupVtCreated = 100, + ControlVtChanged = 101, + ControlError = 255, +}; + +struct ControlHeader { + uint16_t opcode; + uint16_t size; +}; + +struct ControlCreateGroupVtRequest { + int32_t ownerPid; + int32_t vt; + char user[64]; + char session[64]; +}; + +struct ControlDestroyGroupVtRequest { + int32_t vt; +}; -static const char *defaultVtPath = "/dev/tty0"; +struct ControlGroupVtCreatedEvent { + int32_t ownerPid; + int32_t vt; +}; + +struct ControlVtChangeEvent { + int32_t oldVt; + int32_t newVt; +}; + +struct ControlErrorMessage { + int32_t error; +}; + +static int connectUnixSocket(const char *path) { + int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (fd == -1) + return -1; + + sockaddr_un addr {}; + addr.sun_family = AF_UNIX; + const size_t pathLength = strlen(path); + if (pathLength >= sizeof(addr.sun_path)) { + close(fd); + errno = ENAMETOOLONG; + return -1; + } + memcpy(addr.sun_path, path, pathLength + 1); + const auto size = static_cast(offsetof(sockaddr_un, sun_path) + pathLength + 1); + if (::connect(fd, reinterpret_cast(&addr), size) == -1) { + close(fd); + return -1; + } + return fd; +} + +static ssize_t recvAll(int fd, void *buffer, size_t size) { + auto data = static_cast(buffer); + size_t received = 0; + while (received < size) { + const auto ret = recv(fd, data + received, size - received, 0); + if (ret == -1) { + if (errno == EINTR) + continue; + return -1; + } + if (ret == 0) { + errno = ECONNRESET; + return -1; + } + received += static_cast(ret); + } + return static_cast(received); +} + +static bool sendAll(int fd, const void *buffer, size_t size) { + const auto *data = static_cast(buffer); + size_t sent = 0; + while (sent < size) { + const auto ret = send(fd, data + sent, size - sent, MSG_NOSIGNAL); + if (ret == -1) { + if (errno == EINTR) + continue; + return false; + } + if (ret == 0) { + errno = EPIPE; + return false; + } + sent += static_cast(ret); + } + return true; +} + +template +static void copyFixedString(char (&target)[N], const QString &source) { + const auto bytes = source.toUtf8(); + const auto length = std::min(static_cast(bytes.size()), N - 1); + memcpy(target, bytes.constData(), static_cast(length)); + target[length] = '\0'; +} static bool isVtRunningTreeland(int vtnr) { for (Display *display : daemonApp->seatManager()->displays) { @@ -42,100 +164,51 @@ static bool isVtRunningTreeland(int vtnr) { return false; } -/** - * Callback function of disableRender - * - * This will be called after treeland render has been disabled, which is - * happened after a VT release-display signal, to finalize VT switching (see - * onReleaseDisplay()). - */ -static void renderDisabled([[maybe_unused]] void *data, struct wl_callback *callback, [[maybe_unused]] uint32_t serial) { - wl_callback_destroy(callback); - - // Acknowledge kernel to release display - int fd = open(defaultVtPath, O_RDWR | O_NOCTTY); - ioctl(fd, VT_RELDISP, 1); - close(fd); - - // Get active VT by reading /sys/class/tty/tty0/active . - // Note that we cannot use open(defaultVtPath, ...) here, as the open() will - // block VT file access, causing error to systemd-getty-generator, and stop - // getty from spawning if current VT is empty. - int activeVt = -1; - QFile tty("/sys/class/tty/tty0/active"); - if (!tty.open(QIODevice::ReadOnly | QIODevice::Text)) { - qWarning("Failed to open active tty file"); - } else { - auto active = tty.readAll(); - tty.close(); - int scanResult = sscanf(qPrintable(active), "tty%d", &activeVt); - if (scanResult != 1) { - qWarning("Failed to parse active VT from /sys/class/tty/tty0/active with content %s", qPrintable(active)); - activeVt = -1; - } - } - - auto user = daemonApp->displayManager()->findUserByVt(activeVt); - bool isTreeland = isVtRunningTreeland(activeVt); - auto conn = daemonApp->treelandConnector(); - qDebug("Next VT: %d, user: %s", activeVt, user.isEmpty() ? "None" : qPrintable(user)); - - if (isTreeland) { - // If user is not empty, it means the switching can be issued by - // ddm-helper. It uses VT signals from VirtualTerminal.h, - // which is not what we want, so we should acquire VT control here. - int activeVtFd = open(defaultVtPath, O_RDWR | O_NOCTTY); - VirtualTerminal::handleVtSwitches(activeVtFd); - close(activeVtFd); - - conn->enableRender(); - conn->switchToUser(user.isEmpty() ? "dde" : user); - } else { - // Switch to a TTY, deactivate treeland session. - conn->switchToGreeter(); - conn->deactivateSession(); +static bool isTreelandGreeterVt(int vtnr) { + for (Display *display : daemonApp->seatManager()->displays) { + if (display->terminalId == vtnr) + return true; } + return false; } -static const wl_callback_listener renderDisabledListener { - .done = renderDisabled, -}; +static QString findTreelandUserByVt(int vtnr) { + if (vtnr <= 0) + return {}; -static void onAcquireDisplay() { - int fd = open(defaultVtPath, O_RDWR | O_NOCTTY); - - // Activate treeland session before we acknowledge VT switch. - // This will queue our rendering jobs before any keyboard event, to ensure - // all GUI elements are under a prepared state before next possible VT - // switch issued by keyboard. - int vtnr = VirtualTerminal::getVtActive(fd); auto user = daemonApp->displayManager()->findUserByVt(vtnr); - auto conn = daemonApp->treelandConnector(); - if (isVtRunningTreeland(vtnr)) { - qDebug("Activate session at VT %d for user %s", vtnr, qPrintable(user)); - conn->activateSession(); - conn->enableRender(); - conn->switchToUser(user); + if (!user.isEmpty()) + return user; + + for (Display *display : daemonApp->seatManager()->displays) { + for (Auth *auth : display->auths) { + if (auth->tty == vtnr && auth->type == Display::Treeland) + return auth->user; + } } - ioctl(fd, VT_RELDISP, VT_ACKACQ); - close(fd); -} + if (isVtRunningTreeland(vtnr)) + user = daemonApp->displayManager()->LastActivatedUser(); + if (!user.isEmpty()) + return user; -static void onReleaseDisplay() { - auto callback = daemonApp->treelandConnector()->disableRender(); - wl_callback_add_listener(callback, &renderDisabledListener, nullptr); + for (Display *display : daemonApp->seatManager()->displays) { + if (display->terminalId == vtnr) + return QStringLiteral("dde"); + } + return {}; } // TreelandConnector TreelandConnector::TreelandConnector() : QObject(nullptr) { - setSignalHandler(); } TreelandConnector::~TreelandConnector() { + disconnectControlSocket(); delete m_notifier; - wl_display_disconnect(m_display); + if (m_display) + wl_display_disconnect(m_display); } bool TreelandConnector::isConnected() { @@ -147,22 +220,15 @@ void TreelandConnector::setPrivateObject(struct treeland_ddm_v1 *ddm) { } void TreelandConnector::setSignalHandler() { - VirtualTerminal::setVtSignalHandler(onAcquireDisplay, onReleaseDisplay); } // Event implementation static void switchToVt([[maybe_unused]] void *data, [[maybe_unused]] struct treeland_ddm_v1 *ddm, int32_t vtnr) { - int fd = open(qPrintable(VirtualTerminal::path(vtnr)), O_RDWR | O_NOCTTY); - if (ioctl(fd, VT_ACTIVATE, vtnr) < 0) - qWarning("Failed to switch to VT %d: %s", vtnr, strerror(errno)); - close(fd); + qWarning("Ignoring deprecated treeland switch_to_vt request for VT %d; wlroots/libseat handles VT switching directly", vtnr); } -static void acquireVt([[maybe_unused]] void *data, [[maybe_unused]] struct treeland_ddm_v1 *ddm, int32_t vtnr) { - int fd = open(qPrintable(VirtualTerminal::path(vtnr)), O_RDWR | O_NOCTTY); - VirtualTerminal::handleVtSwitches(fd); - close(fd); +static void acquireVt([[maybe_unused]] void *data, [[maybe_unused]] struct treeland_ddm_v1 *ddm, [[maybe_unused]] int32_t vtnr) { } const struct treeland_ddm_v1_listener treelandDDMListener { @@ -197,8 +263,16 @@ const struct wl_registry_listener registryListener { void TreelandConnector::connect(QString socketPath) { disconnect(); + if (!connectControlSocket()) { + qCritical("Cannot connect Treeland without dde-seatd control socket"); + return; + } m_display = wl_display_connect(qPrintable(socketPath)); + if (m_display == nullptr) { + qWarning("Failed to connect to Treeland Wayland socket %s", qPrintable(socketPath)); + return; + } auto registry = wl_display_get_registry(m_display); wl_registry_add_listener(registry, ®istryListener, this); @@ -219,6 +293,7 @@ void TreelandConnector::connect(QString socketPath) { } void TreelandConnector::disconnect() { + disconnectControlSocket(); if (m_display) { if (m_notifier) m_notifier->setEnabled(false); @@ -232,61 +307,275 @@ void TreelandConnector::disconnect() { m_ddm = nullptr; } -// Request wrapper +bool TreelandConnector::connectControlSocket() { + if (m_controlSocket && m_controlSocket->state() == QLocalSocket::ConnectedState) + return true; -void TreelandConnector::switchToGreeter() { - if (isConnected()) { - treeland_ddm_v1_switch_to_greeter(m_ddm); - wl_display_flush(m_display); - } else { - qWarning("Treeland is not connected when trying to call switchToGreeter"); + disconnectControlSocket(); + + m_controlSocket = new QLocalSocket(this); + QObject::connect(m_controlSocket, &QLocalSocket::readyRead, this, &TreelandConnector::handleControlSocket); + QObject::connect(m_controlSocket, &QLocalSocket::disconnected, this, &TreelandConnector::disconnectControlSocket); + + m_controlSocket->connectToServer(QString::fromLatin1(controlSocketPath)); + if (!m_controlSocket->waitForConnected(3000)) { + qWarning() << "Failed to connect dde-seatd control socket" + << controlSocketPath << ":" << m_controlSocket->errorString(); + disconnectControlSocket(); + return false; } + qDebug("Connected dde-seatd control event socket via QLocalSocket"); + return true; } -void TreelandConnector::switchToUser(const QString username) { - if (isConnected()) { - treeland_ddm_v1_switch_to_user(m_ddm, qPrintable(username)); - wl_display_flush(m_display); - } else { - qWarning("Treeland is not connected when trying to call switchToUser"); +void TreelandConnector::disconnectControlSocket() { + if (m_controlSocket) { + if (m_controlSocket->state() == QLocalSocket::ConnectedState + || m_controlSocket->state() == QLocalSocket::ClosingState) { + qDebug("dde-seatd control socket disconnected"); + } + m_controlSocket->blockSignals(true); + if (m_controlSocket->state() != QLocalSocket::UnconnectedState) + m_controlSocket->disconnectFromServer(); + m_controlSocket->deleteLater(); + m_controlSocket = nullptr; } + m_controlBuffer.clear(); } -void TreelandConnector::activateSession() { - if (isConnected()) { - treeland_ddm_v1_activate_session(m_ddm); - wl_display_flush(m_display); - } else { - qWarning("Treeland is not connected when trying to call activateSession"); +int TreelandConnector::treelandMainPid() const { + QDBusInterface systemd(systemdService, + systemdPath, + systemdManagerInterface, + QDBusConnection::systemBus()); + const auto unitReply = systemd.call(QStringLiteral("GetUnit"), QString::fromLatin1(treelandUnit)); + if (unitReply.type() == QDBusMessage::ErrorMessage) { + qWarning() << "Failed to get" << treelandUnit << "unit:" << unitReply.errorMessage(); + return -1; + } + + const auto unitPath = qvariant_cast(unitReply.arguments().value(0)).path(); + QDBusInterface properties(systemdService, + unitPath, + systemdPropertiesInterface, + QDBusConnection::systemBus()); + const auto reply = properties.call(QStringLiteral("Get"), + QString::fromLatin1(systemdServiceInterface), + QStringLiteral("MainPID")); + if (reply.type() == QDBusMessage::ErrorMessage) { + qWarning() << "Failed to get Treeland MainPID:" << reply.errorMessage(); + return -1; + } + + const auto variant = qvariant_cast(reply.arguments().value(0)).variant(); + bool ok = false; + const auto pid = variant.toULongLong(&ok); + if (!ok || pid == 0 || pid > static_cast(std::numeric_limits::max())) { + qWarning() << "Invalid Treeland MainPID from systemd:" << variant; + return -1; } + return static_cast(pid); } -void TreelandConnector::deactivateSession() { - if (isConnected()) { - treeland_ddm_v1_deactivate_session(m_ddm); - wl_display_flush(m_display); +int TreelandConnector::createGroupVtForTreeland(const QString &user, const QString &sessionId) { + const int ownerPid = treelandMainPid(); + if (ownerPid <= 0) + return -1; + + const int fd = connectUnixSocket(controlSocketPath); + if (fd == -1) { + qWarning("Failed to connect dde-seatd control socket %s: %s", + controlSocketPath, strerror(errno)); + return -1; + } + + ControlHeader header { + .opcode = ControlCreateGroupVt, + .size = sizeof(ControlCreateGroupVtRequest), + }; + ControlCreateGroupVtRequest request {}; + request.ownerPid = ownerPid; + request.vt = 0; + copyFixedString(request.user, user); + copyFixedString(request.session, sessionId); + + struct { + ControlHeader header; + ControlCreateGroupVtRequest request; + } message { + .header = header, + .request = request, + }; + + if (!sendAll(fd, &message, sizeof(message))) { + qWarning("Failed to send create-group-vt request: %s", strerror(errno)); + close(fd); + return -1; + } + + pollfd pollFd { + .fd = fd, + .events = POLLIN, + .revents = 0, + }; + int pollResult = -1; + do { + pollResult = poll(&pollFd, 1, 3000); + } while (pollResult == -1 && errno == EINTR); + if (pollResult == -1) { + qWarning("Failed waiting for dde-seatd create-group-vt response: %s", strerror(errno)); + close(fd); + return -1; + } + if (pollResult == 0) { + qWarning("Timed out waiting for dde-seatd create-group-vt response"); + close(fd); + return -1; + } + if ((pollFd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0 || (pollFd.revents & POLLIN) == 0) { + qWarning("Invalid dde-seatd create-group-vt poll events: 0x%x", pollFd.revents); + close(fd); + errno = ECONNRESET; + return -1; + } + + ControlHeader responseHeader {}; + if (recvAll(fd, &responseHeader, sizeof(responseHeader)) != static_cast(sizeof(responseHeader))) { + qWarning("Failed to read create-group-vt response header: %s", strerror(errno)); + close(fd); + return -1; + } + + int vt = -1; + if (responseHeader.opcode == ControlGroupVtCreated && + responseHeader.size == sizeof(ControlGroupVtCreatedEvent)) { + ControlGroupVtCreatedEvent event {}; + if (recvAll(fd, &event, sizeof(event)) == static_cast(sizeof(event))) + vt = event.vt; + } else if (responseHeader.opcode == ControlError && + responseHeader.size == sizeof(ControlErrorMessage)) { + ControlErrorMessage error {}; + if (recvAll(fd, &error, sizeof(error)) == static_cast(sizeof(error))) + qWarning("dde-seatd create-group-vt failed: %s", strerror(error.error)); } else { - qWarning("Treeland is not connected when trying to call deactivateSession"); + qWarning("Unexpected dde-seatd create-group-vt response opcode %u size %u", + responseHeader.opcode, responseHeader.size); + } + + close(fd); + if (vt > 0 && !connectControlSocket()) { + qWarning("Failed to reconnect dde-seatd control event socket, rolling back grouped VT %d", vt); + destroyGroupVt(vt); + return -1; + } + return vt; +} + +bool TreelandConnector::sendDestroyGroupVt(int vt) { + const int fd = connectUnixSocket(controlSocketPath); + if (fd == -1) + return false; + + ControlHeader header { + .opcode = ControlDestroyGroupVt, + .size = sizeof(ControlDestroyGroupVtRequest), + }; + ControlDestroyGroupVtRequest request { + .vt = vt, + }; + struct { + ControlHeader header; + ControlDestroyGroupVtRequest request; + } message { + .header = header, + .request = request, + }; + const bool ok = sendAll(fd, &message, sizeof(message)); + close(fd); + return ok; +} + +void TreelandConnector::destroyGroupVt(int vt) { + if (vt > 0 && !sendDestroyGroupVt(vt)) + qWarning("Failed to destroy grouped VT %d: %s", vt, strerror(errno)); +} + +void TreelandConnector::handleControlSocket() { + if (!m_controlSocket) + return; + + const QByteArray chunk = m_controlSocket->readAll(); + if (chunk.isEmpty()) + return; + + m_controlBuffer.append(chunk); + while (m_controlBuffer.size() >= static_cast(sizeof(ControlHeader))) { + ControlHeader header {}; + memcpy(&header, m_controlBuffer.constData(), sizeof(header)); + if (header.size > maxControlPayloadSize) { + qWarning("Invalid dde-seatd control message size %u", header.size); + disconnectControlSocket(); + return; + } + const int messageSize = static_cast(sizeof(ControlHeader) + header.size); + if (m_controlBuffer.size() < messageSize) + return; + + const auto payload = m_controlBuffer.constData() + sizeof(ControlHeader); + if (header.opcode == ControlVtChanged && header.size == sizeof(ControlVtChangeEvent)) { + ControlVtChangeEvent event {}; + memcpy(&event, payload, sizeof(event)); + const bool oldIsTreelandVt = isVtRunningTreeland(event.oldVt); + const bool oldIsGreeterVt = isTreelandGreeterVt(event.oldVt); + const auto oldUser = findTreelandUserByVt(event.oldVt); + const bool newIsTreelandVt = isVtRunningTreeland(event.newVt); + const bool newIsGreeterVt = isTreelandGreeterVt(event.newVt); + const auto newUser = findTreelandUserByVt(event.newVt); + qInfo("dde-seatd VT change event: oldVt=%d oldIsTreelandVt=%d oldIsGreeterVt=%d oldUser=%s newVt=%d newIsTreelandVt=%d newIsGreeterVt=%d newUser=%s connected=%d", + event.oldVt, + oldIsTreelandVt, + oldIsGreeterVt, + qPrintable(oldUser), + event.newVt, + newIsTreelandVt, + newIsGreeterVt, + qPrintable(newUser), + isConnected()); + if (newIsTreelandVt) { + if (newIsGreeterVt || newUser == QStringLiteral("dde")) { + switchToGreeter(); + } else { + switchToUser(newUser); + } + } else { + qInfo("External VT %d became active; leaving seat transition to dde-seatd/libseat", event.newVt); + } + } else { + qDebug("Ignored dde-seatd control message opcode=%u size=%u", header.opcode, header.size); + } + m_controlBuffer.remove(0, messageSize); } } -void TreelandConnector::enableRender() { +// Request wrapper + +void TreelandConnector::switchToGreeter() { if (isConnected()) { - treeland_ddm_v1_enable_render(m_ddm); + qDebug("Calling treeland switch_to_greeter"); + treeland_ddm_v1_switch_to_greeter(m_ddm); wl_display_flush(m_display); } else { - qWarning("Treeland is not connected when trying to call enableRender"); + qWarning("Treeland is not connected when trying to call switchToGreeter"); } } -struct wl_callback *TreelandConnector::disableRender() { +void TreelandConnector::switchToUser(const QString username) { if (isConnected()) { - auto callback = treeland_ddm_v1_disable_render(m_ddm); + qDebug("Calling treeland switch_to_user: user=%s", qPrintable(username)); + treeland_ddm_v1_switch_to_user(m_ddm, qPrintable(username)); wl_display_flush(m_display); - return callback; } else { - qWarning("Treeland is not connected when trying to call disableRender"); - return nullptr; + qWarning("Treeland is not connected when trying to call switchToUser"); } } diff --git a/src/daemon/TreelandConnector.h b/src/daemon/TreelandConnector.h index 4ed0d48..512ce7c 100644 --- a/src/daemon/TreelandConnector.h +++ b/src/daemon/TreelandConnector.h @@ -2,14 +2,15 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include +#include struct wl_display; -struct wl_callback; struct treeland_ddm_v1; namespace DDM { -class TreelandConnector : QObject { +class TreelandConnector : public QObject { Q_OBJECT public: TreelandConnector(); @@ -19,17 +20,22 @@ class TreelandConnector : QObject { void setSignalHandler(); void connect(const QString socketPath); void disconnect(); + int createGroupVtForTreeland(const QString &user, const QString &sessionId); + void destroyGroupVt(int vt); void switchToGreeter(); void switchToUser(const QString username); - void ackVtSwitch(const int vtnr); - void activateSession(); - void deactivateSession(); - void enableRender(); - struct wl_callback *disableRender(); private: + bool connectControlSocket(); + void disconnectControlSocket(); + void handleControlSocket(); + int treelandMainPid() const; + bool sendDestroyGroupVt(int vt); + struct wl_display *m_display { nullptr }; QSocketNotifier *m_notifier { nullptr }; + QLocalSocket *m_controlSocket { nullptr }; struct treeland_ddm_v1 *m_ddm { nullptr }; + QByteArray m_controlBuffer; }; } diff --git a/src/daemon/TreelandDisplayServer.cpp b/src/daemon/TreelandDisplayServer.cpp index 7629f63..6d1c5bd 100644 --- a/src/daemon/TreelandDisplayServer.cpp +++ b/src/daemon/TreelandDisplayServer.cpp @@ -27,9 +27,13 @@ TreelandDisplayServer::TreelandDisplayServer(SocketServer *socketServer, Display , m_socketServer(socketServer) { connect(m_socketServer, &SocketServer::connected, this, [this, parent](QLocalSocket *socket) { m_greeterSockets << socket; + qWarning("Treeland greeter socket connected: socket=%p total=%lld", + socket, static_cast(m_greeterSockets.size())); }); connect(m_socketServer, &SocketServer::disconnected, this, [this](QLocalSocket *socket) { m_greeterSockets.removeOne(socket); + qWarning("Treeland greeter socket disconnected: socket=%p total=%lld", + socket, static_cast(m_greeterSockets.size())); }); } @@ -44,7 +48,11 @@ bool TreelandDisplayServer::start() { // Start treeland service QDBusInterface systemd("org.freedesktop.systemd1", "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager", QDBusConnection::systemBus()); - systemd.call("StartUnit", "treeland.service", "replace"); + const QDBusMessage reply = systemd.call("StartUnit", "treeland.service", "replace"); + if (reply.type() == QDBusMessage::ErrorMessage) { + qCritical() << "Failed to start treeland.service:" << reply.errorMessage(); + return false; + } // TODO: check treeland service @@ -68,11 +76,16 @@ void TreelandDisplayServer::stop() { } void TreelandDisplayServer::activateUser(const QString &user, int xdgSessionId) { + qDebug("Send greeter activation: user=%s xdgSessionId=%d sockets=%lld", + qPrintable(user), xdgSessionId, static_cast(m_greeterSockets.size())); for (auto greeter : m_greeterSockets) { if (user == "dde") { + qDebug("Sending SwitchToGreeter to socket=%p", greeter); SocketWriter(greeter) << quint32(DaemonMessages::SwitchToGreeter); } + qDebug("Sending UserActivateMessage to socket=%p user=%s xdgSessionId=%d", + greeter, qPrintable(user), xdgSessionId); SocketWriter(greeter) << quint32(DaemonMessages::UserActivateMessage) << user << xdgSessionId; } } diff --git a/src/daemon/VirtualTerminal.cpp b/src/daemon/VirtualTerminal.cpp index b442b43..11e6135 100644 --- a/src/daemon/VirtualTerminal.cpp +++ b/src/daemon/VirtualTerminal.cpp @@ -117,7 +117,12 @@ namespace DDM { daemonApp->signalHandler()->addCustomSignal(RELEASE_DISPLAY_SIGNAL); daemonApp->signalHandler()->addCustomSignal(ACQUIRE_DISPLAY_SIGNAL); - QObject::connect(daemonApp->signalHandler(), &SignalHandler::customSignalReceived, onVtSignal); + static bool vtSignalConnected = false; + if (!vtSignalConnected) { + QObject::connect(daemonApp->signalHandler(), &SignalHandler::customSignalReceived, + daemonApp->signalHandler(), onVtSignal); + vtSignalConnected = true; + } return ok; } @@ -212,6 +217,44 @@ namespace DDM { return vt; } + static bool waitForVtActivation(int fd, int vt, bool wait) { + if (!wait) + return true; + + while (true) { + if (ioctl(fd, VT_WAITACTIVE, vt) == 0) + return true; + if (errno == EINTR) + continue; + + qWarning("Couldn't finalize jump to VT %d: %s", vt, strerror(errno)); + return false; + } + } + + static bool requestVtActivation(int fd, int vt, bool wait) { + while (true) { + if (ioctl(fd, VT_ACTIVATE, vt) == 0) + return waitForVtActivation(fd, vt, wait); + if (errno == EINTR) + continue; + + qWarning("Couldn't initiate jump to VT %d: %s", vt, strerror(errno)); + return false; + } + } + + void activateVt(int vt, bool wait) { + int fd = open(qPrintable(path(vt)), O_RDWR | O_NOCTTY); + if (fd == -1) { + qWarning("Failed to open VT %d: %s", vt, strerror(errno)); + return; + } + + requestVtActivation(fd, vt, wait); + close(fd); + } + void jumpToVt(int vt, bool vt_auto, bool wait) { qDebug() << "Jumping to VT" << vt; @@ -251,21 +294,7 @@ namespace DDM { if (!vt_auto) handleVtSwitches(fd); - do { - errno = 0; - - if (ioctl(fd, VT_ACTIVATE, vt) < 0) { - if (errno == EINTR) - continue; - - qWarning("Couldn't initiate jump to VT %d: %s", vt, strerror(errno)); - break; - } - - if (wait && ioctl(fd, VT_WAITACTIVE, vt) < 0 && errno != EINTR) - qWarning("Couldn't finalize jump to VT %d: %s", vt, strerror(errno)); - - } while (errno == EINTR); + requestVtActivation(fd, vt, wait); close(activeVtFd); if (vtFd != -1) close(vtFd); diff --git a/src/daemon/VirtualTerminal.h b/src/daemon/VirtualTerminal.h index e8e0e9f..c3d3bf0 100644 --- a/src/daemon/VirtualTerminal.h +++ b/src/daemon/VirtualTerminal.h @@ -30,6 +30,7 @@ namespace DDM { bool handleVtSwitches(int fd); int currentVt(); int setUpNewVt(); + void activateVt(int vt, bool wait = true); void jumpToVt(int vt, bool vt_auto, bool wait = true); void setVtSignalHandler(std::function onAcquireDisplay, std::function onReleaseDisplay); }