File indexing completed on 2024-04-28 05:30:31

0001 /*
0002     KWin - the KDE window manager
0003     This file is part of the KDE project.
0004 
0005     SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich <ettrich@kde.org>
0006     SPDX-FileCopyrightText: 2003 Lubos Lunak <l.lunak@kde.org>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "sm.h"
0012 
0013 #include <cstdlib>
0014 #include <kconfig.h>
0015 #include <pwd.h>
0016 #include <unistd.h>
0017 
0018 #include "virtualdesktops.h"
0019 #include "wayland_server.h"
0020 #include "workspace.h"
0021 #include "x11window.h"
0022 #include "xdgshellwindow.h"
0023 #include <QDebug>
0024 
0025 #include <QSessionManager>
0026 #if KWIN_BUILD_NOTIFICATIONS
0027 #include <KLocalizedString>
0028 #include <KNotification>
0029 #include <KService>
0030 #endif
0031 
0032 #include "sessionadaptor.h"
0033 
0034 using namespace Qt::StringLiterals;
0035 
0036 namespace KWin
0037 {
0038 
0039 static KConfig *sessionConfig(QString id, QString key)
0040 {
0041     static KConfig *config = nullptr;
0042     static QString lastId;
0043     static QString lastKey;
0044     static QString pattern = QString(QLatin1String("session/%1_%2_%3")).arg(qApp->applicationName());
0045     if (id != lastId || key != lastKey) {
0046         delete config;
0047         config = nullptr;
0048     }
0049     lastId = id;
0050     lastKey = key;
0051     if (!config) {
0052         config = new KConfig(pattern.arg(id, key), KConfig::SimpleConfig);
0053     }
0054     return config;
0055 }
0056 
0057 static const char *const window_type_names[] = {
0058     "Unknown", "Normal", "Desktop", "Dock", "Toolbar", "Menu", "Dialog",
0059     "Override", "TopMenu", "Utility", "Splash"};
0060 // change also the two functions below when adding new entries
0061 
0062 static const char *windowTypeToTxt(NET::WindowType type)
0063 {
0064     if (type >= NET::Unknown && type <= NET::Splash) {
0065         return window_type_names[type + 1]; // +1 (unknown==-1)
0066     }
0067     if (type == -2) { // undefined (not really part of NET::WindowType)
0068         return "Undefined";
0069     }
0070     qFatal("Unknown Window Type");
0071     return nullptr;
0072 }
0073 
0074 static NET::WindowType txtToWindowType(const char *txt)
0075 {
0076     for (int i = NET::Unknown;
0077          i <= NET::Splash;
0078          ++i) {
0079         if (qstrcmp(txt, window_type_names[i + 1]) == 0) { // +1
0080             return static_cast<NET::WindowType>(i);
0081         }
0082     }
0083     return static_cast<NET::WindowType>(-2); // undefined
0084 }
0085 
0086 /**
0087  * Stores the current session in the config file
0088  *
0089  * @see loadSessionInfo
0090  */
0091 void SessionManager::storeSession(const QString &sessionName, SMSavePhase phase)
0092 {
0093     qCDebug(KWIN_CORE) << "storing session" << sessionName << "in phase" << phase;
0094     KConfig *config = sessionConfig(sessionName, QString());
0095 
0096     KConfigGroup cg(config, QStringLiteral("Session"));
0097     int count = 0;
0098     int active_client = -1;
0099 
0100     const QList<Window *> windows = workspace()->windows();
0101     for (auto it = windows.begin(); it != windows.end(); ++it) {
0102         X11Window *c = qobject_cast<X11Window *>(*it);
0103         if (!c || c->isUnmanaged()) {
0104             continue;
0105         }
0106         if (c->windowType() > NET::Splash) {
0107             // window types outside this are not tooltips/menus/OSDs
0108             // typically these will be unmanaged and not in this list anyway, but that is not enforced
0109             continue;
0110         }
0111         QByteArray sessionId = c->sessionId();
0112         QString wmCommand = c->wmCommand();
0113         if (sessionId.isEmpty()) {
0114             // remember also applications that are not XSMP capable
0115             // and use the obsolete WM_COMMAND / WM_SAVE_YOURSELF
0116             if (wmCommand.isEmpty()) {
0117                 continue;
0118             }
0119         }
0120         count++;
0121         if (c->isActive()) {
0122             active_client = count;
0123         }
0124         if (phase == SMSavePhase2 || phase == SMSavePhase2Full) {
0125             storeClient(cg, count, c);
0126         }
0127     }
0128     if (phase == SMSavePhase0) {
0129         // it would be much simpler to save these values to the config file,
0130         // but both Qt and KDE treat phase1 and phase2 separately,
0131         // which results in different sessionkey and different config file :(
0132         m_sessionActiveClient = active_client;
0133         m_sessionDesktop = VirtualDesktopManager::self()->current();
0134     } else if (phase == SMSavePhase2) {
0135         cg.writeEntry("count", count);
0136         cg.writeEntry("active", m_sessionActiveClient);
0137         cg.writeEntry("desktop", m_sessionDesktop);
0138     } else { // SMSavePhase2Full
0139         cg.writeEntry("count", count);
0140         cg.writeEntry("active", m_sessionActiveClient);
0141         cg.writeEntry("desktop", VirtualDesktopManager::self()->current());
0142     }
0143     config->sync(); // it previously did some "revert to defaults" stuff for phase1 I think
0144 }
0145 
0146 void SessionManager::storeClient(KConfigGroup &cg, int num, X11Window *c)
0147 {
0148     c->setSessionActivityOverride(false); // make sure we get the real values
0149     QString n = QString::number(num);
0150     cg.writeEntry(QLatin1String("sessionId") + n, c->sessionId().constData());
0151     cg.writeEntry(QLatin1String("windowRole") + n, c->windowRole());
0152     cg.writeEntry(QLatin1String("wmCommand") + n, c->wmCommand());
0153     cg.writeEntry(QLatin1String("resourceName") + n, c->resourceName());
0154     cg.writeEntry(QLatin1String("resourceClass") + n, c->resourceClass());
0155     cg.writeEntry(QLatin1String("geometry") + n, QRectF(c->calculateGravitation(true), c->clientSize()).toRect()); // FRAME
0156     cg.writeEntry(QLatin1String("restore") + n, c->geometryRestore());
0157     cg.writeEntry(QLatin1String("fsrestore") + n, c->fullscreenGeometryRestore());
0158     cg.writeEntry(QLatin1String("maximize") + n, (int)c->maximizeMode());
0159     cg.writeEntry(QLatin1String("fullscreen") + n, (int)c->fullScreenMode());
0160     cg.writeEntry(QLatin1String("desktop") + n, c->desktopId());
0161     // the config entry is called "iconified" for back. comp. reasons
0162     // (kconf_update script for updating session files would be too complicated)
0163     cg.writeEntry(QLatin1String("iconified") + n, c->isMinimized());
0164     cg.writeEntry(QLatin1String("opacity") + n, c->opacity());
0165     // the config entry is called "sticky" for back. comp. reasons
0166     cg.writeEntry(QLatin1String("sticky") + n, c->isOnAllDesktops());
0167     cg.writeEntry(QLatin1String("shaded") + n, c->isShade());
0168     // the config entry is called "staysOnTop" for back. comp. reasons
0169     cg.writeEntry(QLatin1String("staysOnTop") + n, c->keepAbove());
0170     cg.writeEntry(QLatin1String("keepBelow") + n, c->keepBelow());
0171     cg.writeEntry(QLatin1String("skipTaskbar") + n, c->originalSkipTaskbar());
0172     cg.writeEntry(QLatin1String("skipPager") + n, c->skipPager());
0173     cg.writeEntry(QLatin1String("skipSwitcher") + n, c->skipSwitcher());
0174     // not really just set by user, but name kept for back. comp. reasons
0175     cg.writeEntry(QLatin1String("userNoBorder") + n, c->userNoBorder());
0176     cg.writeEntry(QLatin1String("windowType") + n, windowTypeToTxt(c->windowType()));
0177     cg.writeEntry(QLatin1String("shortcut") + n, c->shortcut().toString());
0178     cg.writeEntry(QLatin1String("stackingOrder") + n, workspace()->unconstrainedStackingOrder().indexOf(c));
0179     cg.writeEntry(QLatin1String("activities") + n, c->activities());
0180 }
0181 
0182 void SessionManager::storeSubSession(const QString &name, QSet<QByteArray> sessionIds)
0183 {
0184     // TODO clear it first
0185     KConfigGroup cg(KSharedConfig::openConfig(), QLatin1String("SubSession: ") + name);
0186     int count = 0;
0187     int active_client = -1;
0188     const QList<Window *> windows = workspace()->windows();
0189 
0190     for (auto it = windows.begin(); it != windows.end(); ++it) {
0191         X11Window *c = qobject_cast<X11Window *>(*it);
0192         if (!c || c->isUnmanaged()) {
0193             continue;
0194         }
0195         if (c->windowType() > NET::Splash) {
0196             continue;
0197         }
0198         QByteArray sessionId = c->sessionId();
0199         QString wmCommand = c->wmCommand();
0200         if (sessionId.isEmpty()) {
0201             // remember also applications that are not XSMP capable
0202             // and use the obsolete WM_COMMAND / WM_SAVE_YOURSELF
0203             if (wmCommand.isEmpty()) {
0204                 continue;
0205             }
0206         }
0207         if (!sessionIds.contains(sessionId)) {
0208             continue;
0209         }
0210 
0211         qCDebug(KWIN_CORE) << "storing" << sessionId;
0212         count++;
0213         if (c->isActive()) {
0214             active_client = count;
0215         }
0216         storeClient(cg, count, c);
0217     }
0218     cg.writeEntry("count", count);
0219     cg.writeEntry("active", active_client);
0220     // cg.writeEntry( "desktop", currentDesktop());
0221 }
0222 
0223 /**
0224  * Loads the session information from the config file.
0225  *
0226  * @see storeSession
0227  */
0228 void SessionManager::loadSession(const QString &sessionName)
0229 {
0230     session.clear();
0231     KConfigGroup cg(sessionConfig(sessionName, QString()), QStringLiteral("Session"));
0232     Q_EMIT loadSessionRequested(sessionName);
0233     addSessionInfo(cg);
0234 }
0235 
0236 void SessionManager::addSessionInfo(KConfigGroup &cg)
0237 {
0238     workspace()->setInitialDesktop(cg.readEntry("desktop", 1));
0239     int count = cg.readEntry("count", 0);
0240     int active_client = cg.readEntry("active", 0);
0241     for (int i = 1; i <= count; i++) {
0242         QString n = QString::number(i);
0243         SessionInfo *info = new SessionInfo;
0244         session.append(info);
0245         info->sessionId = cg.readEntry(QLatin1String("sessionId") + n, QString()).toLatin1();
0246         info->windowRole = cg.readEntry(QLatin1String("windowRole") + n, QString());
0247         info->wmCommand = cg.readEntry(QLatin1String("wmCommand") + n, QString()).toLatin1();
0248         info->resourceName = cg.readEntry(QLatin1String("resourceName") + n, QString());
0249         info->resourceClass = cg.readEntry(QLatin1String("resourceClass") + n, QString()).toLower();
0250         info->geometry = cg.readEntry(QLatin1String("geometry") + n, QRect());
0251         info->restore = cg.readEntry(QLatin1String("restore") + n, QRect());
0252         info->fsrestore = cg.readEntry(QLatin1String("fsrestore") + n, QRect());
0253         info->maximized = cg.readEntry(QLatin1String("maximize") + n, 0);
0254         info->fullscreen = cg.readEntry(QLatin1String("fullscreen") + n, 0);
0255         info->desktop = cg.readEntry(QLatin1String("desktop") + n, 0);
0256         info->minimized = cg.readEntry(QLatin1String("iconified") + n, false);
0257         info->opacity = cg.readEntry(QLatin1String("opacity") + n, 1.0);
0258         info->onAllDesktops = cg.readEntry(QLatin1String("sticky") + n, false);
0259         info->shaded = cg.readEntry(QLatin1String("shaded") + n, false);
0260         info->keepAbove = cg.readEntry(QLatin1String("staysOnTop") + n, false);
0261         info->keepBelow = cg.readEntry(QLatin1String("keepBelow") + n, false);
0262         info->skipTaskbar = cg.readEntry(QLatin1String("skipTaskbar") + n, false);
0263         info->skipPager = cg.readEntry(QLatin1String("skipPager") + n, false);
0264         info->skipSwitcher = cg.readEntry(QLatin1String("skipSwitcher") + n, false);
0265         info->noBorder = cg.readEntry(QLatin1String("userNoBorder") + n, false);
0266         info->windowType = txtToWindowType(cg.readEntry(QLatin1String("windowType") + n, QString()).toLatin1().constData());
0267         info->shortcut = cg.readEntry(QLatin1String("shortcut") + n, QString());
0268         info->active = (active_client == i);
0269         info->stackingOrder = cg.readEntry(QLatin1String("stackingOrder") + n, -1);
0270         info->activities = cg.readEntry(QLatin1String("activities") + n, QStringList());
0271     }
0272 }
0273 
0274 void SessionManager::loadSubSessionInfo(const QString &name)
0275 {
0276     KConfigGroup cg(KSharedConfig::openConfig(), QLatin1String("SubSession: ") + name);
0277     addSessionInfo(cg);
0278 }
0279 
0280 static bool sessionInfoWindowTypeMatch(X11Window *c, SessionInfo *info)
0281 {
0282     if (info->windowType == -2) {
0283         // undefined (not really part of NET::WindowType)
0284         return !c->isSpecialWindow();
0285     }
0286     return info->windowType == c->windowType();
0287 }
0288 
0289 /**
0290  * Returns a SessionInfo for client \a c. The returned session
0291  * info is removed from the storage. It's up to the caller to delete it.
0292  *
0293  * This function is called when a new window is mapped and must be managed.
0294  * We try to find a matching entry in the session.
0295  *
0296  * May return 0 if there's no session info for the client.
0297  */
0298 SessionInfo *SessionManager::takeSessionInfo(X11Window *c)
0299 {
0300     SessionInfo *realInfo = nullptr;
0301     QByteArray sessionId = c->sessionId();
0302     QString windowRole = c->windowRole();
0303     QString wmCommand = c->wmCommand();
0304     QString resourceName = c->resourceName();
0305     QString resourceClass = c->resourceClass();
0306 
0307     // First search ``session''
0308     if (!sessionId.isEmpty()) {
0309         // look for a real session managed client (algorithm suggested by ICCCM)
0310         for (SessionInfo *info : std::as_const(session)) {
0311             if (realInfo) {
0312                 break;
0313             }
0314             if (info->sessionId == sessionId && sessionInfoWindowTypeMatch(c, info)) {
0315                 if (!windowRole.isEmpty()) {
0316                     if (info->windowRole == windowRole) {
0317                         realInfo = info;
0318                         session.removeAll(info);
0319                     }
0320                 } else {
0321                     if (info->windowRole.isEmpty()
0322                         && info->resourceName == resourceName
0323                         && info->resourceClass == resourceClass) {
0324                         realInfo = info;
0325                         session.removeAll(info);
0326                     }
0327                 }
0328             }
0329         }
0330     } else {
0331         // look for a sessioninfo with matching features.
0332         for (SessionInfo *info : std::as_const(session)) {
0333             if (realInfo) {
0334                 break;
0335             }
0336             if (info->resourceName == resourceName
0337                 && info->resourceClass == resourceClass
0338                 && sessionInfoWindowTypeMatch(c, info)) {
0339                 if (wmCommand.isEmpty() || info->wmCommand == wmCommand) {
0340                     realInfo = info;
0341                     session.removeAll(info);
0342                 }
0343             }
0344         }
0345     }
0346     return realInfo;
0347 }
0348 
0349 SessionManager::SessionManager(QObject *parent)
0350     : QObject(parent)
0351 {
0352     new SessionAdaptor(this);
0353     QDBusConnection::sessionBus().registerObject(QStringLiteral("/Session"), this);
0354 }
0355 
0356 SessionManager::~SessionManager()
0357 {
0358     qDeleteAll(session);
0359 }
0360 
0361 SessionState SessionManager::state() const
0362 {
0363     return m_sessionState;
0364 }
0365 
0366 void SessionManager::setState(uint state)
0367 {
0368     switch (state) {
0369     case 0:
0370         setState(SessionState::Saving);
0371         break;
0372     case 1:
0373         setState(SessionState::Quitting);
0374         break;
0375     default:
0376         setState(SessionState::Normal);
0377     }
0378 }
0379 
0380 // TODO should we rethink this now that we have dedicated start end end save methods?
0381 void SessionManager::setState(SessionState state)
0382 {
0383     if (state == m_sessionState) {
0384         return;
0385     }
0386     // If we're starting to save a session
0387     if (state == SessionState::Saving) {
0388         workspace()->rulebook()->setUpdatesDisabled(true);
0389     }
0390     // If we're ending a save session due to either completion or cancellation
0391     if (m_sessionState == SessionState::Saving) {
0392         workspace()->rulebook()->setUpdatesDisabled(false);
0393         Workspace::self()->forEachClient([](X11Window *client) {
0394             client->setSessionActivityOverride(false);
0395         });
0396     }
0397 
0398     m_sessionState = state;
0399     Q_EMIT stateChanged();
0400 }
0401 
0402 void SessionManager::aboutToSaveSession(const QString &name)
0403 {
0404     Q_EMIT prepareSessionSaveRequested(name);
0405     storeSession(name, SMSavePhase0);
0406 }
0407 
0408 void SessionManager::finishSaveSession(const QString &name)
0409 {
0410     Q_EMIT finishSessionSaveRequested(name);
0411     storeSession(name, SMSavePhase2);
0412 }
0413 
0414 bool SessionManager::closeWaylandWindows()
0415 {
0416     Q_ASSERT(calledFromDBus());
0417     if (!waylandServer()) {
0418         return true;
0419     }
0420 
0421     if (m_closingWindowsGuard) {
0422         sendErrorReply(QDBusError::Failed, u"Operation already in progress"_s);
0423         return false;
0424     }
0425 
0426     m_closingWindowsGuard = std::make_unique<QObject>();
0427     qCDebug(KWIN_CORE) << "Closing windows";
0428 
0429     auto dbusMessage = message();
0430     setDelayedReply(true);
0431 
0432     const auto windows = workspace()->windows();
0433     m_pendingWindows.clear();
0434     m_pendingWindows.reserve(windows.size());
0435     for (const auto window : windows) {
0436         if (auto toplevelWindow = qobject_cast<XdgToplevelWindow *>(window)) {
0437             connect(toplevelWindow, &XdgToplevelWindow::closed, m_closingWindowsGuard.get(), [this, toplevelWindow, dbusMessage] {
0438                 m_pendingWindows.removeOne(toplevelWindow);
0439                 if (m_pendingWindows.empty()) {
0440                     m_closeTimer.stop();
0441                     m_closingWindowsGuard.reset();
0442                     QDBusConnection::sessionBus().send(dbusMessage.createReply(true));
0443                 }
0444             });
0445             m_pendingWindows.push_back(toplevelWindow);
0446             toplevelWindow->closeWindow();
0447         }
0448     }
0449 
0450     if (m_pendingWindows.empty()) {
0451         m_closingWindowsGuard.reset();
0452         QDBusConnection::sessionBus().send(dbusMessage.createReply(true));
0453         return true;
0454     }
0455 
0456     m_closeTimer.start(std::chrono::seconds(10));
0457     m_closeTimer.setSingleShot(true);
0458     connect(&m_closeTimer, &QTimer::timeout, m_closingWindowsGuard.get(), [this, dbusMessage] {
0459 #if KWIN_BUILD_NOTIFICATIONS
0460         QStringList apps;
0461         apps.reserve(m_pendingWindows.size());
0462         std::transform(m_pendingWindows.cbegin(), m_pendingWindows.cend(), std::back_inserter(apps), [](const XdgToplevelWindow *window) -> QString {
0463             const auto service = KService::serviceByDesktopName(window->desktopFileName());
0464             return QChar(u'•') + (service ? service->name() : window->caption());
0465         });
0466         apps.removeDuplicates();
0467         qCDebug(KWIN_CORE) << "Not closed windows" << apps;
0468         auto notification = new KNotification("cancellogout", KNotification::DefaultEvent | KNotification::Persistent);
0469         notification->setText(i18n("The following applications did not close:\n%1", apps.join('\n')));
0470         auto cancel = notification->addAction(i18nc("@action:button", "Cancel Logout"));
0471         auto quit = notification->addAction(i18nc("@action::button", "Log Out Anyway"));
0472         connect(cancel, &KNotificationAction::activated, m_closingWindowsGuard.get(), [dbusMessage, this] {
0473             m_closingWindowsGuard.reset();
0474             QDBusConnection::sessionBus().send(dbusMessage.createReply(false));
0475         });
0476         connect(quit, &KNotificationAction::activated, m_closingWindowsGuard.get(), [dbusMessage, this] {
0477             m_closingWindowsGuard.reset();
0478             QDBusConnection::sessionBus().send(dbusMessage.createReply(true));
0479         });
0480         connect(notification, &KNotification::closed, m_closingWindowsGuard.get(), [dbusMessage, this] {
0481             m_closingWindowsGuard.reset();
0482             QDBusConnection::sessionBus().send(dbusMessage.createReply(false));
0483         });
0484         notification->sendEvent();
0485 #else
0486         m_closingWindowsGuard.reset();
0487         QDBusConnection::sessionBus().send(dbusMessage.createReply(false));
0488 #endif
0489     });
0490     return true;
0491 }
0492 
0493 void SessionManager::quit()
0494 {
0495     qApp->quit();
0496 }
0497 
0498 } // namespace
0499 
0500 #include "moc_sm.cpp"