File indexing completed on 2024-05-12 04:50:47

0001 /*
0002   This file is part of KDSingleApplication.
0003 
0004   SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
0005 
0006   SPDX-License-Identifier: MIT
0007 
0008   Contact KDAB at <info@kdab.com> for commercial licensing options.
0009 */
0010 
0011 #include "kdsingleapplication_localsocket_p.h"
0012 
0013 #include <QtCore/QDir>
0014 #include <QtCore/QDeadlineTimer>
0015 #include <QtCore/QTimer>
0016 #include <QtCore/QLockFile>
0017 #include <QtCore/QDataStream>
0018 
0019 #include <QtCore/QtDebug>
0020 #include <QtCore/QLoggingCategory>
0021 
0022 #include <QtNetwork/QLocalServer>
0023 #include <QtNetwork/QLocalSocket>
0024 
0025 #include <chrono>
0026 #include <algorithm>
0027 
0028 #if defined(Q_OS_UNIX)
0029 // for ::getuid()
0030 #include <sys/types.h>
0031 #include <unistd.h>
0032 #endif
0033 
0034 #if defined(Q_OS_WIN)
0035 #include <qt_windows.h>
0036 #endif
0037 
0038 static const auto LOCALSOCKET_CONNECTION_TIMEOUT = std::chrono::seconds(5);
0039 static const char LOCALSOCKET_PROTOCOL_VERSION = 2;
0040 
0041 Q_LOGGING_CATEGORY(kdsaLocalSocket, "kdsingleapplication.localsocket", QtWarningMsg);
0042 
0043 KDSingleApplicationLocalSocket::KDSingleApplicationLocalSocket(const QString &name, QObject *parent)
0044     : QObject(parent)
0045 {
0046 #if defined(Q_OS_UNIX)
0047     /* cppcheck-suppress useInitializationList */
0048     m_socketName = QStringLiteral("kdsingleapp-%1-%2-%3")
0049                        .arg(::getuid())
0050                        .arg(qEnvironmentVariable("XDG_SESSION_ID"), name);
0051 #elif defined(Q_OS_WIN)
0052     // I'm not sure of a "global session identifier" on Windows; are
0053     // multiple logins from the same user a possibility? For now, following this:
0054     // https://docs.microsoft.com/en-us/windows/desktop/devnotes/getting-the-session-id-of-the-current-process
0055 
0056     DWORD sessionId;
0057     BOOL haveSessionId = ProcessIdToSessionId(GetCurrentProcessId(), &sessionId);
0058 
0059     m_socketName = QString::fromUtf8("kdsingleapp-%1-%2")
0060                        .arg(haveSessionId ? sessionId : 0)
0061                        .arg(name);
0062 #else
0063 #error "KDSingleApplication has not been ported to this platform"
0064 #endif
0065 
0066     const QString lockFilePath =
0067         QDir::tempPath() + QLatin1Char('/') + m_socketName + QLatin1String(".lock");
0068 
0069     qCDebug(kdsaLocalSocket) << "Socket name is" << m_socketName;
0070     qCDebug(kdsaLocalSocket) << "Lock file path is" << lockFilePath;
0071 
0072     std::unique_ptr<QLockFile> lockFile(new QLockFile(lockFilePath));
0073     lockFile->setStaleLockTime(0);
0074 
0075     if (!lockFile->tryLock()) {
0076         // someone else has the lock => we're secondary
0077         qCDebug(kdsaLocalSocket) << "Secondary instance";
0078         return;
0079     }
0080 
0081     qCDebug(kdsaLocalSocket) << "Primary instance";
0082 
0083     std::unique_ptr<QLocalServer> server = std::make_unique<QLocalServer>();
0084     if (!server->listen(m_socketName)) {
0085         // maybe the primary crashed, leaving a stale socket; delete it and try again
0086         QLocalServer::removeServer(m_socketName);
0087         if (!server->listen(m_socketName)) {
0088             // TODO: better error handling.
0089             qWarning("KDSingleApplication: unable to make the primary instance listen on %ls: %ls",
0090                      qUtf16Printable(m_socketName),
0091                      qUtf16Printable(server->errorString()));
0092 
0093             return;
0094         }
0095     }
0096 
0097     connect(server.get(), &QLocalServer::newConnection,
0098             this, &KDSingleApplicationLocalSocket::handleNewConnection);
0099 
0100     m_lockFile = std::move(lockFile);
0101     m_localServer = std::move(server);
0102 }
0103 
0104 KDSingleApplicationLocalSocket::~KDSingleApplicationLocalSocket() = default;
0105 
0106 bool KDSingleApplicationLocalSocket::isPrimaryInstance() const
0107 {
0108     return m_localServer != nullptr;
0109 }
0110 
0111 bool KDSingleApplicationLocalSocket::sendMessage(const QByteArray &message, int timeout)
0112 {
0113     Q_ASSERT(!isPrimaryInstance());
0114     QLocalSocket socket;
0115 
0116     qCDebug(kdsaLocalSocket) << "Preparing to send message" << message << "with timeout" << timeout;
0117 
0118     QDeadlineTimer deadline(timeout);
0119 
0120     // There is an inherent race here with the setup of the server side.
0121     // Even if the socket lock is held by the server, the server may not
0122     // be listening yet. So this connection may fail; keep retrying
0123     // until we hit the timeout.
0124     do {
0125         socket.connectToServer(m_socketName);
0126         if (socket.waitForConnected(deadline.remainingTime()))
0127             break;
0128     } while (!deadline.hasExpired());
0129 
0130     qCDebug(kdsaLocalSocket) << "Socket state:" << socket.state() << "Timer remaining" << deadline.remainingTime() << "Expired?" << deadline.hasExpired();
0131 
0132     if (deadline.hasExpired()) {
0133         qCWarning(kdsaLocalSocket) << "Connection timed out";
0134         return false;
0135     }
0136 
0137     socket.write(&LOCALSOCKET_PROTOCOL_VERSION, 1);
0138 
0139     {
0140         QByteArray encodedMessage;
0141         QDataStream ds(&encodedMessage, QIODevice::WriteOnly);
0142         ds << message;
0143         socket.write(encodedMessage);
0144     }
0145 
0146     qCDebug(kdsaLocalSocket) << "Wrote message in the socket"
0147                              << "Timer remaining" << deadline.remainingTime() << "Expired?" << deadline.hasExpired();
0148 
0149     // There is no acknowledgement mechanism here.
0150     // Should there be one?
0151 
0152     while (socket.bytesToWrite() > 0) {
0153         if (!socket.waitForBytesWritten(deadline.remainingTime())) {
0154             qCWarning(kdsaLocalSocket) << "Message to primary timed out";
0155             return false;
0156         }
0157     }
0158 
0159     qCDebug(kdsaLocalSocket) << "Bytes written, now disconnecting"
0160                              << "Timer remaining" << deadline.remainingTime() << "Expired?" << deadline.hasExpired();
0161 
0162     socket.disconnectFromServer();
0163 
0164     if (socket.state() == QLocalSocket::UnconnectedState) {
0165         qCDebug(kdsaLocalSocket) << "Disconnected -- success!";
0166         return true;
0167     }
0168 
0169     if (!socket.waitForDisconnected(deadline.remainingTime())) {
0170         qCWarning(kdsaLocalSocket) << "Disconnection from primary timed out";
0171         return false;
0172     }
0173 
0174     qCDebug(kdsaLocalSocket) << "Disconnected -- success!";
0175 
0176     return true;
0177 }
0178 
0179 void KDSingleApplicationLocalSocket::handleNewConnection()
0180 {
0181     Q_ASSERT(m_localServer);
0182 
0183     QLocalSocket *socket;
0184     while ((socket = m_localServer->nextPendingConnection())) {
0185         qCDebug(kdsaLocalSocket) << "Got new connection on" << m_socketName << "state" << socket->state();
0186 
0187         Connection c(std::move(socket));
0188         socket = c.socket.get();
0189 
0190         c.readDataConnection = QObjectConnectionHolder(
0191             connect(socket, &QLocalSocket::readyRead,
0192                     this, &KDSingleApplicationLocalSocket::readDataFromSecondary));
0193 
0194         c.secondaryDisconnectedConnection = QObjectConnectionHolder(
0195             connect(socket, &QLocalSocket::disconnected,
0196                     this, &KDSingleApplicationLocalSocket::secondaryDisconnected));
0197 
0198         c.abortConnection = QObjectConnectionHolder(
0199             connect(c.timeoutTimer.get(), &QTimer::timeout,
0200                     this, &KDSingleApplicationLocalSocket::abortConnectionToSecondary));
0201 
0202         m_clients.push_back(std::move(c));
0203 
0204         // Note that by the time we get here, the socket could've already been closed,
0205         // and no signals emitted (hello, Windows!). Read what's already in the socket.
0206         if (readDataFromSecondarySocket(socket))
0207             return;
0208 
0209         if (socket->state() == QLocalSocket::UnconnectedState)
0210             secondarySocketDisconnected(socket);
0211     }
0212 }
0213 
0214 template<typename Container>
0215 static auto findConnectionBySocket(Container &container, QLocalSocket *socket)
0216 {
0217     auto i = std::find_if(container.begin(),
0218                           container.end(),
0219                           [socket](const auto &c) { return c.socket.get() == socket; });
0220     Q_ASSERT(i != container.end());
0221     return i;
0222 }
0223 
0224 template<typename Container>
0225 static auto findConnectionByTimer(Container &container, QTimer *timer)
0226 {
0227     auto i = std::find_if(container.begin(),
0228                           container.end(),
0229                           [timer](const auto &c) { return c.timeoutTimer.get() == timer; });
0230     Q_ASSERT(i != container.end());
0231     return i;
0232 }
0233 
0234 void KDSingleApplicationLocalSocket::readDataFromSecondary()
0235 {
0236     QLocalSocket *socket = static_cast<QLocalSocket *>(sender());
0237     readDataFromSecondarySocket(socket);
0238 }
0239 
0240 bool KDSingleApplicationLocalSocket::readDataFromSecondarySocket(QLocalSocket *socket)
0241 {
0242     auto i = findConnectionBySocket(m_clients, socket);
0243     Connection &c = *i;
0244     c.readData.append(socket->readAll());
0245 
0246     qCDebug(kdsaLocalSocket) << "Got more data from a secondary. Data read so far:" << c.readData;
0247 
0248     const QByteArray &data = c.readData;
0249 
0250     if (data.size() >= 1) {
0251         if (data[0] != LOCALSOCKET_PROTOCOL_VERSION) {
0252             qCDebug(kdsaLocalSocket) << "Got an invalid protocol version";
0253             m_clients.erase(i);
0254             return true;
0255         }
0256     }
0257 
0258     QDataStream ds(data);
0259     ds.skipRawData(1);
0260 
0261     ds.startTransaction();
0262     QByteArray message;
0263     ds >> message;
0264 
0265     if (ds.commitTransaction()) {
0266         qCDebug(kdsaLocalSocket) << "Got a complete message:" << message;
0267         Q_EMIT messageReceived(message);
0268         m_clients.erase(i);
0269         return true;
0270     }
0271 
0272     return false;
0273 }
0274 
0275 void KDSingleApplicationLocalSocket::secondaryDisconnected()
0276 {
0277     QLocalSocket *socket = static_cast<QLocalSocket *>(sender());
0278     secondarySocketDisconnected(socket);
0279 }
0280 
0281 void KDSingleApplicationLocalSocket::secondarySocketDisconnected(QLocalSocket *socket)
0282 {
0283     auto i = findConnectionBySocket(m_clients, socket);
0284     Connection c = std::move(*i);
0285     m_clients.erase(i);
0286 
0287     qCDebug(kdsaLocalSocket) << "Secondary disconnected. Data read:" << c.readData;
0288 }
0289 
0290 void KDSingleApplicationLocalSocket::abortConnectionToSecondary()
0291 {
0292     QTimer *timer = static_cast<QTimer *>(sender());
0293 
0294     auto i = findConnectionByTimer(m_clients, timer);
0295     Connection c = std::move(*i);
0296     m_clients.erase(i);
0297 
0298     qCDebug(kdsaLocalSocket) << "Secondary timed out. Data read:" << c.readData;
0299 }
0300 
0301 KDSingleApplicationLocalSocket::Connection::Connection(QLocalSocket *_socket)
0302     : socket(_socket)
0303     , timeoutTimer(new QTimer)
0304 {
0305     timeoutTimer->start(LOCALSOCKET_CONNECTION_TIMEOUT);
0306 }