/* ksmserver - the KDE session management server SPDX-FileCopyrightText: 2000 Matthias Ettrich SPDX-FileContributor: Oswald Buddenhagen some code taken from the dcopserver (part of the KDE libraries), which is SPDX-FileCopyrightText: 1999 Matthias Ettrich SPDX-FileCopyrightText: 1999 Preston Brown SPDX-License-Identifier: MIT */ #include #include // HAVE_LIMITS_H #include #include #include #include #include #include #ifdef HAVE_SYS_TIME_H #include #endif #include #include #include #include #include #include #include #include #include #ifdef HAVE_LIMITS_H #include #endif #include #include #include #include #include #include #include "client.h" #include "global.h" #include "server.h" #include #include #include #include #include #include #include #include "kwinsession_interface.h" #include "logoutprompt_interface.h" #include "shutdown_interface.h" enum KWinSessionState { Normal = 0, Saving = 1, Quitting = 2, }; void KSMServer::logout(int confirm, int sdtype, int sdmode) { // KDE5: remove me if (sdtype == KWorkSpace::ShutdownTypeLogout) sdtype = KWorkSpace::ShutdownTypeNone; shutdown((KWorkSpace::ShutdownConfirm)confirm, (KWorkSpace::ShutdownType)sdtype, (KWorkSpace::ShutdownMode)sdmode); } bool KSMServer::closeSession() { qCDebug(KSMSERVER) << "Close session called. Current state is:" << state; Q_ASSERT(calledFromDBus()); setDelayedReply(true); const QDBusMessage callerContext = message(); auto conn = QSharedPointer::create(QMetaObject::Connection()); *conn = connect(this, &KSMServer::logoutFinished, this, [callerContext, conn](bool sessionClosed) { auto reply = callerContext.createReply(sessionClosed); QDBusConnection::sessionBus().send(reply); QObject::disconnect(*conn); }); performLogout(); return false; } bool KSMServer::canShutdown() { KSharedConfig::Ptr config = KSharedConfig::openConfig(); config->reparseConfiguration(); // config may have changed in the KControl module KConfigGroup cg(config, "General"); return cg.readEntry("offerShutdown", true) && KDisplayManager().canShutdown(); } bool KSMServer::isShuttingDown() const { return state >= Shutdown; } // this method exists purely for compatibility void KSMServer::shutdown(KWorkSpace::ShutdownConfirm confirm, KWorkSpace::ShutdownType sdtype, KWorkSpace::ShutdownMode sdmode) { qCDebug(KSMSERVER) << "Shutdown called with confirm " << confirm << " type " << sdtype << " and mode " << sdmode; if (state >= Shutdown) // already performing shutdown return; if (state != Idle) // performing startup { return; } KSharedConfig::Ptr config = KSharedConfig::openConfig(); config->reparseConfiguration(); // config may have changed in the KControl module KConfigGroup cg(config, "General"); bool logoutConfirmed = (confirm == KWorkSpace::ShutdownConfirmYes ? false : confirm == KWorkSpace::ShutdownConfirmNo ? true : !cg.readEntry("confirmLogout", true)); int shutdownType = (sdtype != KWorkSpace::ShutdownTypeDefault ? sdtype : cg.readEntry("shutdownType", (int)KWorkSpace::ShutdownType::ShutdownTypeLogout)); if (!logoutConfirmed) { OrgKdeLogoutPromptInterface logoutPrompt(QStringLiteral("org.kde.LogoutPrompt"), QStringLiteral("/LogoutPrompt"), QDBusConnection::sessionBus()); switch (shutdownType) { case KWorkSpace::ShutdownTypeHalt: logoutPrompt.promptShutDown(); break; case KWorkSpace::ShutdownTypeReboot: logoutPrompt.promptReboot(); break; case KWorkSpace::ShutdownTypeNone: Q_FALLTHROUGH(); default: logoutPrompt.promptLogout(); break; } } else { OrgKdeShutdownInterface shutdownIface(QStringLiteral("org.kde.Shutdown"), QStringLiteral("/Shutdown"), QDBusConnection::sessionBus()); switch (shutdownType) { case KWorkSpace::ShutdownTypeHalt: shutdownIface.logoutAndShutdown(); break; case KWorkSpace::ShutdownTypeReboot: shutdownIface.logoutAndReboot(); break; case KWorkSpace::ShutdownTypeNone: Q_FALLTHROUGH(); default: shutdownIface.logout(); break; } } } void KSMServer::performLogout() { if (state >= Shutdown) { // already performing shutdown return; } if (state != Idle) { QTimer::singleShot(1000, this, &KSMServer::performLogout); } auto setStateReply = m_kwinInterface->setState(KWinSessionState::Saving); state = Shutdown; // shall we save the session on logout? KConfigGroup cg(KSharedConfig::openConfig(), "General"); saveSession = (cg.readEntry("loginMode", QStringLiteral("restorePreviousLogout")) == QLatin1String("restorePreviousLogout")); qCDebug(KSMSERVER) << "saveSession is " << saveSession; if (saveSession) sessionGroup = QStringLiteral("Session: ") + QString::fromLocal8Bit(SESSION_PREVIOUS_LOGOUT); // Set the real desktop background to black so that exit looks // clean regardless of what was on "our" desktop. QPalette palette; palette.setColor(QApplication::desktop()->backgroundRole(), Qt::black); QApplication::setPalette(palette); saveType = saveSession ? SmSaveBoth : SmSaveGlobal; #ifndef NO_LEGACY_SESSION_MANAGEMENT performLegacySessionSave(); #endif startProtection(); // Tell KWin to start saving before we start tearing down clients // as any "Save changes?" prompt might meddle with the state if (saveSession) { setStateReply.waitForFinished(); // do we have to wait for this to finish? qCDebug(KSMSERVER) << "Telling KWin we're about to save session" << currentSession(); auto saveSessionCall = m_kwinInterface->aboutToSaveSession(currentSession()); // We need to wait for KWin to save the initial state, e.g. active client and // current desktop before we signal any clients to quit. They might bring up // "Save changes?" prompts altering the state. // KWin doesn't talk to KSMServer directly anymore, so this won't deadlock. saveSessionCall.waitForFinished(); } const auto pendingClients = clients; for (KSMClient *c : pendingClients) { c->resetState(); SmsSaveYourself(c->connection(), saveType, true, SmInteractStyleAny, false); } qCDebug(KSMSERVER) << "clients should be empty, " << clients.count(); if (clients.isEmpty()) completeShutdownOrCheckpoint(); } void KSMServer::saveCurrentSession() { if (state != Idle) return; if (currentSession().isEmpty() || currentSession() == QString::fromLocal8Bit(SESSION_PREVIOUS_LOGOUT)) sessionGroup = QLatin1String("Session: ") + QString::fromLocal8Bit(SESSION_BY_USER); state = Checkpoint; saveType = SmSaveLocal; saveSession = true; #ifndef NO_LEGACY_SESSION_MANAGEMENT performLegacySessionSave(); #endif auto aboutToSaveCall = m_kwinInterface->aboutToSaveSession(currentSession()); aboutToSaveCall.waitForFinished(); const auto pendingClients = clients; for (KSMClient *c : pendingClients) { SmsSaveYourself(c->connection(), saveType, false, SmInteractStyleNone, false); } if (clients.isEmpty()) completeShutdownOrCheckpoint(); } void KSMServer::saveCurrentSessionAs(const QString &session) { if (state != Idle) return; sessionGroup = QStringLiteral("Session: ") + session; saveCurrentSession(); } // callbacks void KSMServer::saveYourselfDone(KSMClient *client, bool success) { if (state == Idle) { // State saving when it's not shutdown or checkpoint. Probably // a shutdown was canceled and the client is finished saving // only now. Discard the saved state in order to avoid // the saved data building up. QStringList discard = client->discardCommand(); if (!discard.isEmpty()) executeCommand(discard); return; } if (success) { client->saveYourselfDone = true; completeShutdownOrCheckpoint(); } else { // fake success to make KDE's logout not block with broken // apps. A perfect ksmserver would display a warning box at // the very end. client->saveYourselfDone = true; completeShutdownOrCheckpoint(); } startProtection(); } void KSMServer::interactRequest(KSMClient *client, int /*dialogType*/) { if (state == Shutdown || state == ClosingSubSession) client->pendingInteraction = true; else SmsInteract(client->connection()); handlePendingInteractions(); } void KSMServer::interactDone(KSMClient *client, bool cancelShutdown_) { if (client != clientInteracting) return; // should not happen clientInteracting = nullptr; if (cancelShutdown_) cancelShutdown(client); else handlePendingInteractions(); } void KSMServer::phase2Request(KSMClient *client) { client->waitForPhase2 = true; client->wasPhase2 = true; completeShutdownOrCheckpoint(); } void KSMServer::handlePendingInteractions() { if (clientInteracting) return; foreach (KSMClient *c, clients) { if (c->pendingInteraction) { clientInteracting = c; c->pendingInteraction = false; break; } } if (clientInteracting) { endProtection(); SmsInteract(clientInteracting->connection()); } else { startProtection(); } } void KSMServer::cancelShutdown(KSMClient *c) { clientInteracting = nullptr; qCDebug(KSMSERVER) << state; if (state == ClosingSubSession) { clientsToKill.clear(); clientsToSave.clear(); Q_EMIT subSessionCloseCanceled(); } else { qCDebug(KSMSERVER) << "Client " << c->program() << " (" << c->clientId() << ") canceled shutdown."; KNotification::event(QStringLiteral("cancellogout"), i18n("Logout canceled by '%1'", c->program()), QPixmap(), nullptr, KNotification::DefaultEvent); foreach (KSMClient *c, clients) { SmsShutdownCancelled(c->connection()); if (c->saveYourselfDone) { // Discard also saved state. QStringList discard = c->discardCommand(); if (!discard.isEmpty()) executeCommand(discard); } } } state = Idle; m_kwinInterface->setState(KWinSessionState::Normal); Q_EMIT logoutFinished(false); } void KSMServer::startProtection() { KSharedConfig::Ptr config = KSharedConfig::openConfig(); config->reparseConfiguration(); // config may have changed in the KControl module KConfigGroup cg(config, "General"); int timeout = cg.readEntry("clientShutdownTimeoutSecs", 15) * 1000; protectionTimer.setSingleShot(true); protectionTimer.start(timeout); } void KSMServer::endProtection() { protectionTimer.stop(); } /* Internal protection slot, invoked when clients do not react during shutdown. */ void KSMServer::protectionTimeout() { if ((state != Shutdown && state != Checkpoint && state != ClosingSubSession) || clientInteracting) return; foreach (KSMClient *c, clients) { if (!c->saveYourselfDone && !c->waitForPhase2) { qCDebug(KSMSERVER) << "protectionTimeout: client " << c->program() << "(" << c->clientId() << ")"; c->saveYourselfDone = true; } } completeShutdownOrCheckpoint(); startProtection(); } void KSMServer::completeShutdownOrCheckpoint() { qCDebug(KSMSERVER) << "completeShutdownOrCheckpoint called"; if (state != Shutdown && state != Checkpoint && state != ClosingSubSession) return; QList pendingClients; if (state == ClosingSubSession) pendingClients = clientsToSave; else pendingClients = clients; foreach (KSMClient *c, pendingClients) { if (!c->saveYourselfDone && !c->waitForPhase2) return; // not done yet } // do phase 2 bool waitForPhase2 = false; foreach (KSMClient *c, pendingClients) { if (!c->saveYourselfDone && c->waitForPhase2) { c->waitForPhase2 = false; SmsSaveYourselfPhase2(c->connection()); waitForPhase2 = true; } } if (waitForPhase2) return; if (saveSession) storeSession(); else discardSession(); qCDebug(KSMSERVER) << "state is " << state; if (state == Shutdown) { KNotification *n = KNotification::event(QStringLiteral("exitkde"), QString(), QPixmap(), nullptr, KNotification::DefaultEvent); // Plasma says good bye connect(n, &KNotification::closed, this, &KSMServer::startKilling); state = WaitingForKNotify; // https://bugs.kde.org/show_bug.cgi?id=228005 // if sound is not working for some reason (e.g. no phonon // backends are installed) the closed() signal never happens // and logoutSoundFinished() never gets called. Add this timer to make // sure the shutdown procedure continues even if sound system is broken. QTimer::singleShot(5000, this, [=] { if (state == WaitingForKNotify) { n->deleteLater(); startKilling(); } }); } else if (state == Checkpoint) { foreach (KSMClient *c, clients) { SmsSaveComplete(c->connection()); } state = Idle; } else { // ClosingSubSession startKillingSubSession(); } } void KSMServer::startKilling() { qCDebug(KSMSERVER) << "Starting killing clients"; if (state == Killing) { // we are already killing return; } // kill all clients state = Killing; m_kwinInterface->setState(KWinSessionState::Quitting); foreach (KSMClient *c, clients) { qCDebug(KSMSERVER) << "startKilling: client " << c->program() << "(" << c->clientId() << ")"; SmsDie(c->connection()); } qCDebug(KSMSERVER) << " We killed all clients. We have now clients.count()=" << clients.count() << Qt::endl; completeKilling(); QTimer::singleShot(10000, this, &KSMServer::timeoutQuit); } void KSMServer::completeKilling() { qCDebug(KSMSERVER) << "KSMServer::completeKilling clients.count()=" << clients.count() << Qt::endl; if (state == Killing) { if (!clients.isEmpty()) // still waiting for clients to go away return; killingCompleted(); } } // shutdown is fully complete void KSMServer::killingCompleted() { Q_EMIT logoutFinished(true); qApp->quit(); } void KSMServer::timeoutQuit() { foreach (KSMClient *c, clients) { qCWarning(KSMSERVER) << "SmsDie timeout, client " << c->program() << "(" << c->clientId() << ")"; } killingCompleted(); } void KSMServer::saveSubSession(const QString &name, QStringList saveAndClose, QStringList saveOnly) { if (state != Idle) { // performing startup qCDebug(KSMSERVER) << "not idle!" << state; return; } qCDebug(KSMSERVER) << name << saveAndClose << saveOnly; state = ClosingSubSession; saveType = SmSaveBoth; // both or local? what oes it mean? saveSession = true; sessionGroup = QStringLiteral("SubSession: ") + name; #ifndef NO_LEGACY_SESSION_MANAGEMENT // performLegacySessionSave(); FIXME #endif startProtection(); foreach (KSMClient *c, clients) { if (saveAndClose.contains(QString::fromLocal8Bit(c->clientId()))) { c->resetState(); SmsSaveYourself(c->connection(), saveType, true, SmInteractStyleAny, false); clientsToSave << c; clientsToKill << c; } else if (saveOnly.contains(QString::fromLocal8Bit(c->clientId()))) { c->resetState(); SmsSaveYourself(c->connection(), saveType, true, SmInteractStyleAny, false); clientsToSave << c; } } completeShutdownOrCheckpoint(); } void KSMServer::startKillingSubSession() { qCDebug(KSMSERVER) << "Starting killing clients"; // kill all clients state = KillingSubSession; foreach (KSMClient *c, clientsToKill) { qCDebug(KSMSERVER) << "completeShutdown: client " << c->program() << "(" << c->clientId() << ")"; SmsDie(c->connection()); } qCDebug(KSMSERVER) << " We killed some clients. We have now clients.count()=" << clients.count() << Qt::endl; completeKillingSubSession(); QTimer::singleShot(10000, this, &KSMServer::signalSubSessionClosed); } void KSMServer::completeKillingSubSession() { qCDebug(KSMSERVER) << "KSMServer::completeKillingSubSession clients.count()=" << clients.count() << Qt::endl; if (state == KillingSubSession) { if (!clientsToKill.isEmpty()) { return; // still waiting for clients to go away } signalSubSessionClosed(); } } void KSMServer::signalSubSessionClosed() { if (state != KillingSubSession) return; clientsToKill.clear(); clientsToSave.clear(); // TODO tell the subSession manager the close request was carried out // so that plasma can close its stuff state = Idle; qCDebug(KSMSERVER) << state; Q_EMIT subSessionClosed(); }