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 }