/* ksmserver - the KDE session management server SPDX-FileCopyrightText: 2000 Matthias Ettrich SPDX-FileCopyrightText: 2005 Lubos Lunak SPDX-FileCopyrightText: 2023 Harald Sitter 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 "server.h" #include "client.h" #include "global.h" #include "klocalizedstring.h" #include "ksmserver_debug.h" #include "ksmserverinterfaceadaptor.h" #include #include #include #include #include #include #include #ifdef HAVE_SYS_TIME_H #include #endif #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 #include #include #include #include #include #include #include #include #include #include #include #include "kscreenlocker_interface.h" #include "kwinsession_interface.h" #include extern "C" { #include #include #include } // ICEmsg.h has a static_assert macro, which conflicts with C++'s static_assert. #ifdef static_assert #undef static_assert #endif namespace { size_t fileNumberLimit() { static auto limit = []() -> size_t { struct rlimit limit = {}; if (getrlimit(RLIMIT_NOFILE, &limit) != 0) { const auto err = errno; qCWarning(KSMSERVER) << "Failed to getrlimit:" << strerror(err); return std::numeric_limits::max(); } return limit.rlim_cur; }(); return limit; } size_t perAppStartLimit() { static auto limit = fileNumberLimit() / 4; return limit; } } // namespace KSMServer *the_server = nullptr; KSMServer *KSMServer::self() { return the_server; } /*! Utility function to execute a command on the local machine. Used * to restart applications. */ void KSMServer::startApplication(const QStringList &cmd, const QString &clientMachine, const QString &userId) { QStringList command = cmd; if (command.isEmpty()) { return; } if (!userId.isEmpty()) { struct passwd *pw = getpwuid(getuid()); if (pw != nullptr && userId != QString::fromLocal8Bit(pw->pw_name)) { command.prepend(QStringLiteral("--")); command.prepend(userId); command.prepend(QStringLiteral("-u")); command.prepend(QStandardPaths::findExecutable(QStringLiteral("kdesu"))); } } if (!clientMachine.isEmpty() && clientMachine != QLatin1String("localhost")) { command.prepend(clientMachine); command.prepend(xonCommand); // "xon" by default } const QString app = command.takeFirst(); const QStringList argList = command; auto *job = new KIO::CommandLauncherJob(app, argList); auto apps = KApplicationTrader::query([&app](const KService::Ptr &service) { const QString binary = KIO::DesktopExecParser::executablePath(service->exec()); return !service->noDisplay() && !binary.isEmpty() && app.endsWith(binary); }); if (!apps.empty()) { job->setDesktopName(apps[0]->desktopEntryName()); } job->start(); } /*! Utility function to execute a command on the local machine. Used * to discard session data */ void KSMServer::executeCommand(const QStringList &command) { if (command.isEmpty()) { return; } KProcess::execute(command); } IceAuthDataEntry *authDataEntries = nullptr; static QTemporaryFile *remTempFile = nullptr; static IceListenObj *listenObjs = nullptr; int numTransports = 0; Status KSMRegisterClientProc(SmsConn /* smsConn */, SmPointer managerData, char *previousId) { KSMClient *client = (KSMClient *)managerData; client->registerClient(previousId); return 1; } void KSMInteractRequestProc(SmsConn /* smsConn */, SmPointer managerData, int dialogType) { the_server->interactRequest((KSMClient *)managerData, dialogType); } void KSMInteractDoneProc(SmsConn /* smsConn */, SmPointer managerData, Bool cancelShutdown) { the_server->interactDone((KSMClient *)managerData, cancelShutdown); } void KSMSaveYourselfRequestProc(SmsConn smsConn, SmPointer /* managerData */, int saveType, Bool shutdown, int interactStyle, Bool fast, Bool global) { if (shutdown) { qCWarning(KSMSERVER) << "Shutdown via ICE was called but not supported"; } if (!global) { SmsSaveYourself(smsConn, saveType, false, interactStyle, fast); SmsSaveComplete(smsConn); } // else checkpoint only, ksmserver does not yet support this // mode. Will come for KDE 3.1 } void KSMSaveYourselfPhase2RequestProc(SmsConn /* smsConn */, SmPointer managerData) { the_server->phase2Request((KSMClient *)managerData); } void KSMSaveYourselfDoneProc(SmsConn /* smsConn */, SmPointer managerData, Bool success) { the_server->saveYourselfDone((KSMClient *)managerData, success); } void KSMCloseConnectionProc(SmsConn smsConn, SmPointer managerData, int count, char **reasonMsgs) { the_server->deleteClient((KSMClient *)managerData); if (count) { SmFreeReasons(count, reasonMsgs); } IceConn iceConn = SmsGetIceConnection(smsConn); SmsCleanUp(smsConn); IceSetShutdownNegotiation(iceConn, false); IceCloseConnection(iceConn); } void KSMSetPropertiesProc(SmsConn /* smsConn */, SmPointer managerData, int numProps, SmProp **props) { auto client = (KSMClient *)managerData; for (int i = 0; i < numProps; i++) { SmProp *p = client->property(props[i]->name); if (p) { client->properties.removeAll(p); SmFreeProperty(p); } client->properties.append(props[i]); } if (numProps) { free(props); } } void KSMDeletePropertiesProc(SmsConn /* smsConn */, SmPointer managerData, int numProps, char **propNames) { auto client = (KSMClient *)managerData; for (int i = 0; i < numProps; i++) { SmProp *p = client->property(propNames[i]); if (p) { client->properties.removeAll(p); SmFreeProperty(p); } } } void KSMGetPropertiesProc(SmsConn smsConn, SmPointer managerData) { auto client = (KSMClient *)managerData; auto props = new SmProp *[client->properties.count()]; int i = 0; for (SmProp *prop : std::as_const(client->properties)) props[i++] = prop; SmsReturnProperties(smsConn, i, props); delete[] props; } class KSMListener : public QSocketNotifier { public: explicit KSMListener(IceListenObj obj) : QSocketNotifier(IceGetListenConnectionNumber(obj), QSocketNotifier::Read) , listenObj(obj) { } IceListenObj listenObj; }; class KSMConnection : public QSocketNotifier { public: explicit KSMConnection(IceConn conn) : QSocketNotifier(IceConnectionNumber(conn), QSocketNotifier::Read) , iceConn(conn) { count++; } ~KSMConnection() override { count--; } Q_DISABLE_COPY_MOVE(KSMConnection) IceConn iceConn; inline static size_t count = 0; }; /* for printing hex digits */ static void fprintfhex(FILE *fp, unsigned int len, char *cp) { static const char hexchars[] = "0123456789abcdef"; for (; len > 0; len--, cp++) { unsigned char s = *cp; putc(hexchars[s >> 4], fp); putc(hexchars[s & 0x0f], fp); } } #define MAGIC_COOKIE_LEN 16 Status SetAuthentication(int count, IceListenObj *listenObjs, IceAuthDataEntry **authDataEntries) { char *filename = IceAuthFileName(); FILE *fp; if (IceLockAuthFile(filename, 10, 2, 600) != IceAuthLockSuccess) { qWarning() << "Could not lock ICEAuthority file"; return 0; } auto cleanup = qScopeGuard([filename]() { IceUnlockAuthFile(filename); }); if ((fp = fopen(filename, "ab")) == NULL) { qWarning() << "Could not open ICEAuthority file"; return 0; } if ((*authDataEntries = (IceAuthDataEntry *)malloc(count * 2 * sizeof(IceAuthDataEntry))) == nullptr) { (void)fclose(fp); return 0; } for (int i = 0; i < numTransports * 2; i += 2) { // ICE Auth Data (*authDataEntries)[i].network_id = IceGetListenConnectionString(listenObjs[i / 2]); (*authDataEntries)[i].protocol_name = (char *)"ICE"; (*authDataEntries)[i].auth_name = (char *)"MIT-MAGIC-COOKIE-1"; (*authDataEntries)[i].auth_data = IceGenerateMagicCookie(MAGIC_COOKIE_LEN); (*authDataEntries)[i].auth_data_length = MAGIC_COOKIE_LEN; // ICE Auth File { IceAuthFileEntry *file_entry = (IceAuthFileEntry *)malloc(sizeof(IceAuthFileEntry)); file_entry->protocol_name = strdup("ICE"); file_entry->protocol_data = NULL; file_entry->protocol_data_length = 0; file_entry->network_id = IceGetListenConnectionString(listenObjs[i / 2]); file_entry->auth_name = strdup("MIT-MAGIC-COOKIE-1"); if (file_entry->auth_data = (char *)malloc(MAGIC_COOKIE_LEN)) { memcpy(file_entry->auth_data, (*authDataEntries)[i].auth_data, MAGIC_COOKIE_LEN); } file_entry->auth_data_length = MAGIC_COOKIE_LEN; if (IceWriteAuthFileEntry(fp, file_entry) == 0) { qWarning("Failed to write ice auth file entry"); } IceFreeAuthFileEntry(file_entry); } // XSMP Auth Data (*authDataEntries)[i + 1].network_id = IceGetListenConnectionString(listenObjs[i / 2]); (*authDataEntries)[i + 1].protocol_name = (char *)"XSMP"; (*authDataEntries)[i + 1].auth_name = (char *)"MIT-MAGIC-COOKIE-1"; (*authDataEntries)[i + 1].auth_data = IceGenerateMagicCookie(MAGIC_COOKIE_LEN); (*authDataEntries)[i + 1].auth_data_length = MAGIC_COOKIE_LEN; // XSMP Auth file { IceAuthFileEntry *file_entry = (IceAuthFileEntry *)malloc(sizeof(IceAuthFileEntry)); file_entry->protocol_name = strdup("XSMP"); file_entry->protocol_data = NULL; file_entry->protocol_data_length = 0; file_entry->network_id = IceGetListenConnectionString(listenObjs[i / 2]); file_entry->auth_name = strdup("MIT-MAGIC-COOKIE-1"); if (file_entry->auth_data = (char *)malloc(MAGIC_COOKIE_LEN)) { memcpy(file_entry->auth_data, (*authDataEntries)[i + 1].auth_data, MAGIC_COOKIE_LEN); } file_entry->auth_data_length = MAGIC_COOKIE_LEN; if (IceWriteAuthFileEntry(fp, file_entry) == 0) { qWarning("Failed to write xsmp ice auth file entry"); } IceFreeAuthFileEntry(file_entry); } IceSetPaAuthData(2, &(*authDataEntries)[i]); } if (fclose(fp) != 0) { qWarning() << "Could not close ICEAuthority file"; return 0; } return (1); } /* * Free up authentication data. */ void FreeAuthenticationData(int count, IceAuthDataEntry *authDataEntries) { /* Each transport has entries for ICE and XSMP */ for (int i = 0; i < count * 2; i++) { free(authDataEntries[i].network_id); free(authDataEntries[i].auth_data); } free(authDataEntries); } static int Xio_ErrorHandler(Display *) { qCWarning(KSMSERVER, "ksmserver: Fatal IO error: client killed"); // Don't do anything that might require the X connection if (the_server) { KSMServer *server = the_server; the_server = nullptr; server->cleanUp(); // Don't delete server!! } exit(0); // Don't report error, it's not our fault. return 0; // Bogus return value, notreached } void KSMServer::setupXIOErrorHandler() { XSetIOErrorHandler(Xio_ErrorHandler); } static int wake_up_socket = -1; static void sighandler(int sig) { if (sig == SIGHUP) { signal(SIGHUP, sighandler); return; } char ch = 0; std::ignore = ::write(wake_up_socket, &ch, 1); } void KSMWatchProc(IceConn iceConn, IcePointer client_data, Bool opening, IcePointer *watch_data) { auto ds = (KSMServer *)client_data; if (opening) { *watch_data = (IcePointer)ds->watchConnection(iceConn); } else { ds->removeConnection((KSMConnection *)*watch_data); } } static Status KSMNewClientProc(SmsConn conn, SmPointer manager_data, unsigned long *mask_ret, SmsCallbacks *cb, char **failure_reason_ret) { *failure_reason_ret = nullptr; void *client = ((KSMServer *)manager_data)->newClient(conn); if (!client) { const char *errstr = "Connection rejected: ksmserver is shutting down"; qCWarning(KSMSERVER, "%s", errstr); if ((*failure_reason_ret = (char *)malloc(strlen(errstr) + 1)) != nullptr) { strcpy(*failure_reason_ret, errstr); } return 0; } cb->register_client.callback = KSMRegisterClientProc; cb->register_client.manager_data = client; cb->interact_request.callback = KSMInteractRequestProc; cb->interact_request.manager_data = client; cb->interact_done.callback = KSMInteractDoneProc; cb->interact_done.manager_data = client; cb->save_yourself_request.callback = KSMSaveYourselfRequestProc; cb->save_yourself_request.manager_data = client; cb->save_yourself_phase2_request.callback = KSMSaveYourselfPhase2RequestProc; cb->save_yourself_phase2_request.manager_data = client; cb->save_yourself_done.callback = KSMSaveYourselfDoneProc; cb->save_yourself_done.manager_data = client; cb->close_connection.callback = KSMCloseConnectionProc; cb->close_connection.manager_data = client; cb->set_properties.callback = KSMSetPropertiesProc; cb->set_properties.manager_data = client; cb->delete_properties.callback = KSMDeletePropertiesProc; cb->delete_properties.manager_data = client; cb->get_properties.callback = KSMGetPropertiesProc; cb->get_properties.manager_data = client; *mask_ret = SmsRegisterClientProcMask | SmsInteractRequestProcMask | SmsInteractDoneProcMask | SmsSaveYourselfRequestProcMask | SmsSaveYourselfP2RequestProcMask | SmsSaveYourselfDoneProcMask | SmsCloseConnectionProcMask | SmsSetPropertiesProcMask | SmsDeletePropertiesProcMask | SmsGetPropertiesProcMask; return 1; } #ifdef HAVE__ICETRANSNOLISTEN extern "C" int _IceTransNoListen(const char *protocol); #endif KSMServer::KSMServer(InitFlags flags) : sessionGroup(QLatin1String("")) , m_kwinInterface(new OrgKdeKWinSessionInterface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Session"), QDBusConnection::sessionBus(), this)) , sockets{-1, -1} { if (!flags.testFlag(InitFlag::NoLockScreen)) { ScreenLocker::KSldApp::self()->initialize(); if (flags.testFlag(InitFlag::ImmediateLockScreen)) { ScreenLocker::KSldApp::self()->lock(ScreenLocker::EstablishLock::Immediate); } } if (::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sockets) != 0) { qFatal("Could not create socket pair, error %d (%s)", errno, strerror(errno)); } wake_up_socket = sockets[0]; auto notifier = new QSocketNotifier(sockets[1], QSocketNotifier::Read, this); qApp->connect(notifier, &QSocketNotifier::activated, &QApplication::quit); const QString runtimeDirectory = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation); iceAuthFile.setFileTemplate(runtimeDirectory + QStringLiteral("/iceauth_XXXXXX")); if (!iceAuthFile.open()) qFatal("Failed to create ICEauthority file"); setenv("ICEAUTHORITY", qPrintable(iceAuthFile.fileName()), true); new KSMServerInterfaceAdaptor(this); QDBusConnection::sessionBus().registerObject(QStringLiteral("/KSMServer"), this); the_server = this; clean = false; state = Idle; saveSession = false; KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("General")); clientInteracting = nullptr; xonCommand = config.readEntry("xonCommand", "xon"); #ifdef HAVE__ICETRANSNOLISTEN if (flags.testFlag(InitFlag::OnlyLocal)) { _IceTransNoListen("tcp"); } #endif char errormsg[256]; if (!SmsInitialize((char *)KSMVendorString, (char *)KSMReleaseString, KSMNewClientProc, (SmPointer)this, nullptr, 256, errormsg)) { qCWarning(KSMSERVER, "KSMServer: could not register XSM protocol"); } if (!IceListenForConnections(&numTransports, &listenObjs, 256, errormsg)) { qCWarning(KSMSERVER, "KSMServer: Error listening for connections: %s", errormsg); qCWarning(KSMSERVER, "KSMServer: Aborting."); exit(1); } { // publish available transports. QByteArray fName = QFile::encodeName(QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + QDir::separator() + QStringLiteral("KSMserver")); qCDebug(KSMSERVER) << fName; QString display = QString::fromLocal8Bit(::getenv("DISPLAY")); // strip the screen number from the display display.remove(QRegularExpression(QStringLiteral("\\.[0-9]+$"))); int i = 0; while ((i = display.indexOf(QLatin1Char(':'))) >= 0) { display[i] = QLatin1Char('_'); } while ((i = display.indexOf(QLatin1Char('/'))) >= 0) { display[i] = QLatin1Char('_'); } fName += '_' + display.toLocal8Bit(); FILE *f = ::fopen(fName.data(), "w+"); if (!f) { qCWarning(KSMSERVER, "KSMServer: cannot open %s: %s", fName.data(), strerror(errno)); qCWarning(KSMSERVER, "KSMServer: Aborting."); exit(1); } char *session_manager = IceComposeNetworkIdList(numTransports, listenObjs); fprintf(f, "%s\n%i\n", session_manager, getpid()); fclose(f); setenv("SESSION_MANAGER", session_manager, true); QProcessEnvironment newEnv; newEnv.insert(QStringLiteral("SESSION_MANAGER"), QString::fromLatin1(session_manager)); newEnv.insert(QStringLiteral("ICEAUTHORITY"), iceAuthFile.fileName()); auto updateEnvJob = new KUpdateLaunchEnvironmentJob(newEnv); QEventLoop loop; QObject::connect(updateEnvJob, &KUpdateLaunchEnvironmentJob::finished, &loop, &QEventLoop::quit); loop.exec(); free(session_manager); } if (!SetAuthentication(numTransports, listenObjs, &authDataEntries)) { qFatal("KSMSERVER: authentication setup failed."); } IceAddConnectionWatch(KSMWatchProc, (IcePointer)this); KSMListener *con = nullptr; for (int i = 0; i < numTransports; i++) { fcntl(IceGetListenConnectionNumber(listenObjs[i]), F_SETFD, FD_CLOEXEC); con = new KSMListener(listenObjs[i]); listener.append(con); connect(con, &KSMListener::activated, this, &KSMServer::newConnection); } signal(SIGHUP, sighandler); signal(SIGTERM, sighandler); signal(SIGINT, sighandler); signal(SIGPIPE, SIG_IGN); connect(&protectionTimer, &QTimer::timeout, this, &KSMServer::protectionTimeout); connect(this, &KSMServer::sessionRestored, this, [this]() { auto reply = m_restoreSessionCall.createReply(); QDBusConnection::sessionBus().send(reply); m_restoreSessionCall = QDBusMessage(); }); connect(qApp, &QApplication::aboutToQuit, this, &KSMServer::cleanUp); setupXIOErrorHandler(); QDBusMessage ksplashProgressMessage = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KSplash"), QStringLiteral("/KSplash"), QStringLiteral("org.kde.KSplash"), QStringLiteral("setStage")); ksplashProgressMessage.setArguments({QStringLiteral("ksmserver")}); QDBusConnection::sessionBus().call(ksplashProgressMessage, QDBus::NoBlock); } KSMServer::~KSMServer() { qDeleteAll(listener); the_server = nullptr; cleanUp(); } void KSMServer::cleanUp() { if (clean) { return; } clean = true; IceFreeListenObjs(numTransports, listenObjs); wake_up_socket = -1; ::close(sockets[1]); ::close(sockets[0]); sockets[0] = -1; sockets[1] = -1; QByteArray fName = QFile::encodeName(QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + QLatin1Char('/') + QStringLiteral("KSMserver")); QString display = QString::fromLocal8Bit(::getenv("DISPLAY")); // strip the screen number from the display display.remove(QRegularExpression(QStringLiteral("\\.[0-9]+$"))); int i = 0; while ((i = display.indexOf(QLatin1Char(':'))) >= 0) { display[i] = QLatin1Char('_'); } while ((i = display.indexOf(QLatin1Char('/'))) >= 0) { display[i] = QLatin1Char('_'); } fName += '_' + display.toLocal8Bit(); ::unlink(fName.data()); FreeAuthenticationData(numTransports, authDataEntries); signal(SIGTERM, SIG_DFL); signal(SIGINT, SIG_DFL); } void *KSMServer::watchConnection(IceConn iceConn) { auto conn = new KSMConnection(iceConn); connect(conn, &KSMConnection::activated, this, &KSMServer::processData); return (void *)conn; } void KSMServer::removeConnection(KSMConnection *conn) { delete conn; } /*! Called from our IceIoErrorHandler */ void KSMServer::ioError(IceConn /*iceConn*/) { } void KSMServer::processData(int /*socket*/) { auto iceConn = ((KSMConnection *)sender())->iceConn; IceProcessMessagesStatus status = IceProcessMessages(iceConn, nullptr, nullptr); if (status == IceProcessMessagesIOError) { IceSetShutdownNegotiation(iceConn, false); QList::iterator it = clients.begin(); QList::iterator const itEnd = clients.end(); while ((it != itEnd) && *it && (SmsGetIceConnection((*it)->connection()) != iceConn)) { ++it; } if ((it != itEnd) && *it) { SmsConn smsConn = (*it)->connection(); deleteClient(*it); SmsCleanUp(smsConn); } (void)IceCloseConnection(iceConn); } } KSMClient *KSMServer::newClient(SmsConn conn) { KSMClient *client = nullptr; if (state != Killing) { client = new KSMClient(conn); clients.append(client); } return client; } void KSMServer::deleteClient(KSMClient *client) { if (!clients.contains(client)) { // paranoia return; } clients.removeAll(client); clientsToKill.removeAll(client); clientsToSave.removeAll(client); if (client == clientInteracting) { clientInteracting = nullptr; handlePendingInteractions(); } delete client; if (state == Shutdown || state == Checkpoint || state == ClosingSubSession) { completeShutdownOrCheckpoint(); } if (state == Killing) { completeKilling(); } else if (state == KillingSubSession) { completeKillingSubSession(); } } void KSMServer::newConnection(int /*socket*/) { IceAcceptStatus status = IceAcceptFailure; auto iceConn = IceAcceptConnection(((KSMListener *)sender())->listenObj, &status); if (!iceConn) { return; } // This should be sufficient to hold all open file descriptors that are not connection watches. The rest we will // freely use to watch ICE connections. static const size_t backupFileCount = 128; qCDebug(KSMSERVER) << "KSMConnection::count" << KSMConnection::count; if (KSMConnection::count > (fileNumberLimit() - backupFileCount)) { // https://bugs.kde.org/show_bug.cgi?id=475506 qCWarning(KSMSERVER) << "Too many open connections. Refusing to track any more to prevent exhaustion of open file limits."; QHash consumers; for (const auto &client : clients) { QString program = client->program(); if (program.isEmpty() && !client->restartCommand().isEmpty()) { program = client->restartCommand().first(); } if (program.isEmpty()) { program = i18nc("@label an unknown executable is using resources", "[unknown]"); } if (!consumers.contains(program)) { consumers[program] = 0; } consumers[program]++; } std::multimap ranking; // using STL container because QMultiMap has no reverse iterators for (auto it = consumers.cbegin(); it != consumers.cend(); it++) { ranking.emplace(it.value(), it.key()); } QString consumerInfo; int i = 0; for (auto it = ranking.rbegin(); i != 3 && it != ranking.rend(); it++, i++) { if (!consumerInfo.isEmpty()) { consumerInfo += QLatin1String(", "); } consumerInfo += QStringLiteral("%1:%2").arg(it->second, QString::number(it->first)); } KNotification::event(KNotification::Error, xi18nc("@label notification; %1 is a list of executables", "Unable to manage some apps because the system's session management resources are exhausted. Here are the top three " "consumers of session resources:\n%1", consumerInfo), QPixmap(), KNotification::Persistent); std::ignore = IceCloseConnection(iceConn); return; } IceSetShutdownNegotiation(iceConn, false); IceConnectStatus cstatus = IceConnectPending; while ((cstatus = IceConnectionStatus(iceConn)) == IceConnectPending) { (void)IceProcessMessages(iceConn, nullptr, nullptr); } if (cstatus != IceConnectAccepted) { if (cstatus == IceConnectIOError) { qCDebug(KSMSERVER) << "IO error opening ICE Connection!"; } else { qCDebug(KSMSERVER) << "ICE Connection rejected!"; } std::ignore = IceCloseConnection(iceConn); return; } // don't leak the fd fcntl(IceConnectionNumber(iceConn), F_SETFD, FD_CLOEXEC); } QString KSMServer::currentSession() { if (sessionGroup.startsWith(SESSION_PREFIX)) { return sessionGroup.mid(SESSION_PREFIX.size()); } return QLatin1String(""); // empty, not null, since used for KConfig::setGroup // TODO does this comment make any sense? } void KSMServer::discardSession() { KConfigGroup config(KSharedConfig::openConfig(), sessionGroup); int count = config.readEntry("count", 0); for (KSMClient *c : std::as_const(clients)) { QStringList discardCommand = c->discardCommand(); if (discardCommand.isEmpty()) { continue; } // check that non of the old clients used the exactly same // discardCommand before we execute it. This used to be the // case up to KDE and Qt < 3.1 int i = 1; while (i <= count && config.readPathEntry(QString(u"discardCommand" + QString::number(i)), QStringList()) != discardCommand) { i++; } if (i <= count) { executeCommand(discardCommand); } } } void KSMServer::storeSession() { KSharedConfig::Ptr config = KSharedConfig::openConfig(); config->reparseConfiguration(); // config may have changed in the KControl module KConfigGroup generalGroup(config, QStringLiteral("General")); excludeApps = generalGroup.readEntry("excludeApps").toLower().split(QRegularExpression(QStringLiteral("[,:]")), Qt::SkipEmptyParts); KConfigGroup configSessionGroup(config, sessionGroup); int count = configSessionGroup.readEntry("count", 0); for (int i = 1; i <= count; i++) { const auto discardCommand = configSessionGroup.readPathEntry(QLatin1String("discardCommand") + QString::number(i), QStringList()); if (discardCommand.isEmpty()) { continue; } // check that non of the new clients uses the exactly same // discardCommand before we execute it. This used to be the // case up to KDE and Qt < 3.1 QList::iterator it = clients.begin(); QList::iterator const itEnd = clients.end(); while ((it != itEnd) && *it && (discardCommand != (*it)->discardCommand())) { ++it; } if ((it != itEnd) && *it) { continue; } executeCommand(discardCommand); } config->deleteGroup(sessionGroup); // ### does not work with global config object... KConfigGroup cg(config, sessionGroup); count = 0; // Tell kwin to save its state auto reply = m_kwinInterface->finishSaveSession(currentSession()); reply.waitForFinished(); // boo! for (KSMClient *c : std::as_const(clients)) { const int restartHint = c->restartStyleHint(); if (restartHint == SmRestartNever) { continue; } const QString program = c->program(); const QStringList restartCommand = c->restartCommand(); if (program.isEmpty() && restartCommand.isEmpty()) { continue; } if (state == ClosingSubSession && !clientsToSave.contains(c)) { continue; } // 'program' might be (mostly) fullpath, or (sometimes) just the name. // 'name' is just the name. const QFileInfo info(program); const QString &name = info.fileName(); if (excludeApps.contains(program.toLower()) || excludeApps.contains(name.toLower())) { continue; } count++; const QString n = QString::number(count); cg.writeEntry(QString(u"program" + n), program); cg.writeEntry(QString(u"clientId" + n), c->clientId()); cg.writeEntry(QString(u"restartCommand" + n), restartCommand); cg.writePathEntry(QString(u"discardCommand" + n), c->discardCommand()); cg.writeEntry(QString(u"restartStyleHint" + n), restartHint); cg.writeEntry(QString(u"userId" + n), c->userId()); } cg.writeEntry("count", count); KConfigGroup cg2(config, QStringLiteral("General")); storeLegacySession(config.data()); config->sync(); } QStringList KSMServer::sessionList() { QStringList sessions(QStringLiteral("default")); KSharedConfig::Ptr config = KSharedConfig::openConfig(); const QStringList groups = config->groupList(); for (auto it = groups.constBegin(); it != groups.constEnd(); ++it) { if ((*it).startsWith(SESSION_PREFIX)) { sessions << (*it).mid(SESSION_PREFIX.size()); } } return sessions; } bool KSMServer::defaultSession() const { return sessionGroup.isEmpty(); } void KSMServer::setRestoreSession(const QString &sessionName) { if (state != Idle) { return; } qCDebug(KSMSERVER) << "KSMServer::restoreSession " << sessionName; KSharedConfig::Ptr config = KSharedConfig::openConfig(); sessionGroup = SESSION_PREFIX + sessionName; KConfigGroup configSessionGroup(config, sessionGroup); int count = configSessionGroup.readEntry("count", 0); appsToStart = count; } /*! Starts the default session. */ void KSMServer::startDefaultSession() { if (state != Idle) { return; } sessionGroup = QString(); } void KSMServer::restoreSession() { Q_ASSERT(calledFromDBus()); if (defaultSession()) { state = KSMServer::Idle; return; } setDelayedReply(true); m_restoreSessionCall = message(); state = KSMServer::Restoring; auto reply = m_kwinInterface->loadSession(currentSession()); auto watcher = new QDBusPendingCallWatcher(reply, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, reply](QDBusPendingCallWatcher *watcher) { watcher->deleteLater(); if (reply.isError()) { qWarning() << "Failed to notify kwin of current session " << reply.error().message(); } restoreLegacySession(KSharedConfig::openConfig().data()); tryRestore(); }); } void KSMServer::restoreSubSession(const QString &name) { sessionGroup = SUBSESSION_PREFIX + name; KConfigGroup configSessionGroup(KSharedConfig::openConfig(), sessionGroup); int count = configSessionGroup.readEntry("count", 0); appsToStart = count; state = RestoringSubSession; tryRestore(); } void KSMServer::clientRegistered(const char * /* previousId */) { } void KSMServer::tryRestore() { if (state != Restoring && state != RestoringSubSession) { return; } KConfigGroup config(KSharedConfig::openConfig(), sessionGroup); struct DontStartEntry { QString clientId; QString appName; }; QList dontStartEntries; QHash startCounter; struct RestartEntry { QString clientId; QStringList restartCommand; int restartStyleHint; QString clientMachine; QString userId; }; QList entries; entries.reserve(appsToStart); for (int i = 1; i <= appsToStart; i++) { const auto n = QString::number(i); const auto entry = entries.emplace_back(RestartEntry{ .clientId = config.readEntry(QLatin1String("clientId") + n, QString()), .restartCommand = config.readEntry(QLatin1String("restartCommand") + n, QStringList()), .restartStyleHint = config.readEntry(QLatin1String("restartStyleHint") + n, 0), .clientMachine = config.readEntry(QLatin1String("clientMachine") + n, QString()), .userId = config.readEntry(QLatin1String("userId") + n, QString()), }); // Count how many times this command is going to be started. If it is too many we will create a DontStartEntry // and consequently turn *all* restorations no-op in the actual start loop. if (entry.restartCommand.isEmpty()) { continue; } const auto appName = entry.restartCommand.first(); if (!startCounter.contains(appName)) { startCounter[appName] = 0; } startCounter[appName]++; if (startCounter[appName] == perAppStartLimit()) { // only insert this entry once! when the limit is hit dontStartEntries.push_back(DontStartEntry{.clientId = entry.clientId, .appName = appName}); } } for (const auto &entry : dontStartEntries) { qCWarning(KSMSERVER) << "Too many application starts issued for" << entry.appName << ". Not starting any more." << "Something may be broken with the application's session management."; KNotification::event(KNotification::Error, xi18nc("@label notification; %1 is an executable name", "Unable to restore %1 because it is broken and has exhausted the system's session restoration " "resources. Please report this to the app's developers.", entry.appName)); } for (const auto &entry : entries) { // We only discard the entries here because a violating app will get all entries disabled, not just the ones in // excess. So we need to loop all entries twice: once to establish the in-excess apps, and again to actually start // (or not). const bool dontStart = std::any_of(dontStartEntries.cbegin(), dontStartEntries.cend(), [&entry](const auto &dontStartEntry) { return dontStartEntry.clientId == entry.clientId; }); if (dontStart) { continue; } const bool alreadyStarted = std::any_of(clients.cbegin(), clients.cend(), [&entry](const auto &client) { return QString::fromLocal8Bit(client->clientId()) == entry.clientId; }); if (alreadyStarted) { continue; } if (entry.restartCommand.isEmpty() || entry.restartStyleHint == SmRestartNever) { continue; } startApplication(entry.restartCommand, entry.clientMachine, entry.userId); } // all done if (state == Restoring) { Q_EMIT sessionRestored(); } else { // subsession Q_EMIT subSessionOpened(); } state = Idle; } void KSMServer::startupDone() { state = Idle; } void KSMServer::openSwitchUserDialog() { // this method exists only for compatibility. Users should ideally call this directly OrgKdeScreensaverInterface iface(QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("/ScreenSaver"), QDBusConnection::sessionBus()); iface.SwitchUser(); }