File indexing completed on 2024-04-28 04:37:25

0001 /*
0002     SPDX-FileCopyrightText: 2013 Milian Wolff <mail@milianw.de>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 */
0006 
0007 #include "sessionlock.h"
0008 
0009 #include "debug.h"
0010 #include "sessioncontroller.h"
0011 
0012 #include <KLocalizedString>
0013 #include <KMessageBox>
0014 #include <KMessageBox_KDevCompat>
0015 
0016 #include <QDBusConnectionInterface>
0017 #include <QFile>
0018 #include <QDir>
0019 
0020 namespace KDevelop {
0021 QString lockFileForSession( const QString& id )
0022 {
0023     return SessionController::sessionDirectory( id ) + QLatin1String("/lock");
0024 }
0025 
0026 QString dBusServiceNameForSession( const QString& id )
0027 {
0028     // We remove starting "{" and ending "}" from the string UUID representation
0029     // as D-Bus apparently doesn't allow them in service names
0030     return QLatin1String("org.kdevelop.kdevplatform-lock-") + id.midRef(1, id.size() - 2);
0031 }
0032 
0033 /// Force-removes the lock-file.
0034 void forceRemoveLockfile(const QString& lockFilename)
0035 {
0036     if( QFile::exists( lockFilename ) ) {
0037         QFile::remove( lockFilename );
0038     }
0039 }
0040 
0041 TryLockSessionResult SessionLock::tryLockSession(const QString& sessionId, bool doLocking)
0042 {
0043     ///FIXME: if this is hit, someone tried to lock a non-existing session
0044     ///       this should be fixed by using a proper data type distinct from
0045     ///       QString for id's, i.e. QUuid or similar.
0046     Q_ASSERT(QFile::exists(SessionController::sessionDirectory( sessionId )));
0047 
0048     /*
0049      * We've got two locking mechanisms here: D-Bus unique service name (based on the session id)
0050      * and a plain lockfile (QLockFile).
0051      * The latter is required to get the appname/pid of the locking instance
0052      * in case if it's stale/hanging/crashed (to make user know which PID he needs to kill).
0053      * D-Bus mechanism is the primary one.
0054      *
0055      * Since there is a kind of "logic tree", the code is a bit hard.
0056      */
0057     const QString service = dBusServiceNameForSession( sessionId );
0058     QDBusConnection connection = QDBusConnection::sessionBus();
0059     QDBusConnectionInterface* connectionInterface = connection.interface();
0060 
0061     const QString lockFilename = lockFileForSession( sessionId );
0062     QSharedPointer<QLockFile> lockFile(new QLockFile( lockFilename ));
0063 
0064     const bool haveDBus = connection.isConnected();
0065     const bool canLockDBus = haveDBus && connectionInterface && !connectionInterface->isServiceRegistered( service );
0066     bool lockedDBus = false;
0067 
0068     // Lock D-Bus if we can and we need to
0069     if( doLocking && canLockDBus ) {
0070         lockedDBus = connection.registerService( service );
0071     }
0072 
0073     // Attempt to lock file, despite the possibility to do so and presence of the request (doLocking)
0074     // This is required as QLockFile::getLockInfo() works only after QLockFile::lock() is called
0075     bool lockResult = lockFile->tryLock();
0076     SessionRunInfo runInfo;
0077     if (lockResult) {
0078         // Unlock immediately if we shouldn't have locked it
0079         if( haveDBus && !lockedDBus ) {
0080             lockFile->unlock();
0081         }
0082     } else {
0083         // If locking failed, retrieve the lock's metadata
0084         lockFile->getLockInfo(&runInfo.holderPid, &runInfo.holderHostname, &runInfo.holderApp );
0085         runInfo.isRunning = !haveDBus || !canLockDBus;
0086 
0087         if( haveDBus && lockedDBus ) {
0088             // Since the lock-file is secondary, try to force-lock it if D-Bus locking succeeded
0089             forceRemoveLockfile(lockFilename);
0090             lockResult = lockFile->tryLock();
0091             Q_ASSERT(lockResult);
0092         }
0093     }
0094 
0095     // Set the result by D-Bus status
0096     if (doLocking && (haveDBus ? lockedDBus : lockResult)) {
0097         return TryLockSessionResult(QSharedPointer<ISessionLock>(new SessionLock(sessionId, lockFile)));
0098     } else {
0099         return TryLockSessionResult(runInfo);
0100     }
0101 }
0102 
0103 QString SessionLock::id()
0104 {
0105     return m_sessionId;
0106 }
0107 
0108 SessionLock::SessionLock(const QString& sessionId, const QSharedPointer<QLockFile>& lockFile)
0109 : m_sessionId(sessionId)
0110 , m_lockFile(lockFile)
0111 {
0112     Q_ASSERT(lockFile->isLocked());
0113 }
0114 
0115 SessionLock::~SessionLock()
0116 {
0117     m_lockFile->unlock();
0118     bool unregistered = QDBusConnection::sessionBus().unregisterService( dBusServiceNameForSession(m_sessionId) );
0119     Q_UNUSED(unregistered);
0120 }
0121 
0122 void SessionLock::removeFromDisk()
0123 {
0124     Q_ASSERT(m_lockFile->isLocked());
0125     // unlock first to prevent warnings: "Could not remove our own lock file ..."
0126     m_lockFile->unlock();
0127     QDir(SessionController::sessionDirectory(m_sessionId)).removeRecursively();
0128 }
0129 
0130 QString SessionLock::handleLockedSession(const QString& sessionName, const QString& sessionId,
0131                                          const SessionRunInfo& runInfo)
0132 {
0133     if( !runInfo.isRunning ) {
0134         return sessionId;
0135     }
0136 
0137     // try to make the locked session active
0138     {
0139         // The timeout for "ensureVisible" call
0140         // Leave it sufficiently low to avoid waiting for hung instances.
0141         static const int timeout_ms = 1000;
0142 
0143         QDBusMessage message = QDBusMessage::createMethodCall( dBusServiceNameForSession(sessionId),
0144                                                                 QStringLiteral("/kdevelop/MainWindow"),
0145                                                                 QStringLiteral("org.kdevelop.MainWindow"),
0146                                                                 QStringLiteral("ensureVisible") );
0147         QDBusMessage reply = QDBusConnection::sessionBus().call( message,
0148                                                                     QDBus::Block,
0149                                                                     timeout_ms );
0150         if( reply.type() == QDBusMessage::ReplyMessage ) {
0151             QTextStream out(stdout);
0152             out << i18nc(
0153                 "@info:shell",
0154                 "Running %1 instance (PID: %2) detected, making this one visible instead of starting a new one",
0155                 runInfo.holderApp, runInfo.holderPid)
0156                 << Qt::endl;
0157             return QString();
0158         } else {
0159             qCWarning(SHELL) << i18nc("@info:shell", "Running %1 instance (PID: %2) is apparently hung", runInfo.holderApp, runInfo.holderPid);
0160         }
0161     }
0162 
0163     // otherwise ask the user whether we should retry
0164     QString problemDescription = i18nc("@info",
0165                                 "The given application did not respond to a DBUS call, "
0166                                 "it may have crashed or is hanging.");
0167 
0168     QString problemHeader;
0169     if( runInfo.holderPid != -1 ) {
0170         problemHeader = i18nc("@info", "Failed to lock the session <em>%1</em>, "
0171                               "already locked by %2 on %3 (PID %4).",
0172                               sessionName, runInfo.holderApp, runInfo.holderHostname, runInfo.holderPid);
0173     } else {
0174         problemHeader = i18nc("@info", "Failed to lock the session <em>%1</em> (lock-file unavailable).",
0175                               sessionName);
0176     }
0177 
0178     QString problemResolution = i18nc("@info", "<p>Please, close the offending application instance "
0179                                   "or choose another session to launch.</p>");
0180 
0181     QString errmsg = QLatin1String("<p>") + problemHeader + QLatin1String("<br>") + problemDescription + QLatin1String("</p>") + problemResolution;
0182 
0183     KGuiItem retry = KStandardGuiItem::cont();
0184     retry.setText(i18nc("@action:button", "Retry Startup"));
0185 
0186     KGuiItem choose = KStandardGuiItem::configure();
0187     choose.setText(i18nc("@action:button", "Choose Another Session"));
0188 
0189     KGuiItem cancel = KStandardGuiItem::quit();
0190     int ret = KMessageBox::warningTwoActionsCancel(
0191         nullptr, errmsg, i18nc("@title:window", "Failed to Lock Session %1", sessionName), retry, choose, cancel);
0192     switch( ret ) {
0193     case KMessageBox::PrimaryAction:
0194         return sessionId;
0195 
0196     case KMessageBox::SecondaryAction: {
0197         QString errmsg = i18nc("@info", "The session %1 is already active in another running instance.",
0198                                sessionName);
0199         return SessionController::showSessionChooserDialog(errmsg);
0200     }
0201 
0202     case KMessageBox::Cancel:
0203     default:
0204         break;
0205     }
0206 
0207     return QString();
0208 }
0209 
0210 }