KDSingleApplication API Documentation 1.0
Loading...
Searching...
No Matches
kdsingleapplication_localsocket.cpp
Go to the documentation of this file.
1/*
2 This file is part of KDSingleApplication.
3
4 SPDX-FileCopyrightText: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
5
6 SPDX-License-Identifier: MIT
7
8 Contact KDAB at <info@kdab.com> for commercial licensing options.
9*/
10
11#include "kdsingleapplication_localsocket_p.h"
12
13#include <QtCore/QDir>
14#include <QtCore/QDeadlineTimer>
15#include <QtCore/QTimer>
16#include <QtCore/QLockFile>
17#include <QtCore/QDataStream>
18
19#include <QtCore/QtDebug>
20#include <QtCore/QLoggingCategory>
21
22#include <QtNetwork/QLocalServer>
23#include <QtNetwork/QLocalSocket>
24
25#include <chrono>
26#include <algorithm>
27
28#if defined(Q_OS_UNIX)
29// for ::getuid()
30#include <sys/types.h>
31#include <unistd.h>
32#endif
33
34#if defined(Q_OS_WIN)
35#include <qt_windows.h>
36#endif
37
38static const auto LOCALSOCKET_CONNECTION_TIMEOUT = std::chrono::seconds(5);
39static const char LOCALSOCKET_PROTOCOL_VERSION = 2;
40
41Q_LOGGING_CATEGORY(kdsaLocalSocket, "kdsingleapplication.localsocket", QtWarningMsg);
42
43KDSingleApplicationLocalSocket::KDSingleApplicationLocalSocket(const QString &name, QObject *parent)
44 : QObject(parent)
45{
46#if defined(Q_OS_UNIX)
47 /* cppcheck-suppress useInitializationList */
48 m_socketName = QStringLiteral("kdsingleapp-%1-%2-%3")
49 .arg(::getuid())
50 .arg(qEnvironmentVariable("XDG_SESSION_ID"), name);
51#elif defined(Q_OS_WIN)
52 // I'm not sure of a "global session identifier" on Windows; are
53 // multiple logins from the same user a possibility? For now, following this:
54 // https://docs.microsoft.com/en-us/windows/desktop/devnotes/getting-the-session-id-of-the-current-process
55
56 DWORD sessionId;
57 BOOL haveSessionId = ProcessIdToSessionId(GetCurrentProcessId(), &sessionId);
58
59 m_socketName = QString::fromUtf8("kdsingleapp-%1-%2")
60 .arg(haveSessionId ? sessionId : 0)
61 .arg(name);
62#else
63#error "KDSingleApplication has not been ported to this platform"
64#endif
65
66 const QString lockFilePath =
67 QDir::tempPath() + QLatin1Char('/') + m_socketName + QLatin1String(".lock");
68
69 qCDebug(kdsaLocalSocket) << "Socket name is" << m_socketName;
70 qCDebug(kdsaLocalSocket) << "Lock file path is" << lockFilePath;
71
72 std::unique_ptr<QLockFile> lockFile(new QLockFile(lockFilePath));
73 lockFile->setStaleLockTime(0);
74
75 if (!lockFile->tryLock()) {
76 // someone else has the lock => we're secondary
77 qCDebug(kdsaLocalSocket) << "Secondary instance";
78 return;
79 }
80
81 qCDebug(kdsaLocalSocket) << "Primary instance";
82
83 std::unique_ptr<QLocalServer> server = std::make_unique<QLocalServer>();
84 if (!server->listen(m_socketName)) {
85 // maybe the primary crashed, leaving a stale socket; delete it and try again
86 QLocalServer::removeServer(m_socketName);
87 if (!server->listen(m_socketName)) {
88 // TODO: better error handling.
89 qWarning("KDSingleApplication: unable to make the primary instance listen on %ls: %ls",
90 qUtf16Printable(m_socketName),
91 qUtf16Printable(server->errorString()));
92
93 return;
94 }
95 }
96
97 connect(server.get(), &QLocalServer::newConnection,
98 this, &KDSingleApplicationLocalSocket::handleNewConnection);
99
100 m_lockFile = std::move(lockFile);
101 m_localServer = std::move(server);
102}
103
104KDSingleApplicationLocalSocket::~KDSingleApplicationLocalSocket() = default;
105
106bool KDSingleApplicationLocalSocket::isPrimaryInstance() const
107{
108 return m_localServer != nullptr;
109}
110
111bool KDSingleApplicationLocalSocket::sendMessage(const QByteArray &message, int timeout)
112{
113 Q_ASSERT(!isPrimaryInstance());
114 QLocalSocket socket;
115
116 qCDebug(kdsaLocalSocket) << "Preparing to send message" << message << "with timeout" << timeout;
117
118 QDeadlineTimer deadline(timeout);
119
120 // There is an inherent race here with the setup of the server side.
121 // Even if the socket lock is held by the server, the server may not
122 // be listening yet. So this connection may fail; keep retrying
123 // until we hit the timeout.
124 do {
125 socket.connectToServer(m_socketName);
126 if (socket.waitForConnected(deadline.remainingTime()))
127 break;
128 } while (!deadline.hasExpired());
129
130 qCDebug(kdsaLocalSocket) << "Socket state:" << socket.state() << "Timer remaining" << deadline.remainingTime() << "Expired?" << deadline.hasExpired();
131
132 if (deadline.hasExpired()) {
133 qCWarning(kdsaLocalSocket) << "Connection timed out";
134 return false;
135 }
136
138
139 {
140 QByteArray encodedMessage;
141 QDataStream ds(&encodedMessage, QIODevice::WriteOnly);
142 ds << message;
143 socket.write(encodedMessage);
144 }
145
146 qCDebug(kdsaLocalSocket) << "Wrote message in the socket"
147 << "Timer remaining" << deadline.remainingTime() << "Expired?" << deadline.hasExpired();
148
149 // There is no acknowledgement mechanism here.
150 // Should there be one?
151
152 while (socket.bytesToWrite() > 0) {
153 if (!socket.waitForBytesWritten(deadline.remainingTime())) {
154 qCWarning(kdsaLocalSocket) << "Message to primary timed out";
155 return false;
156 }
157 }
158
159 qCDebug(kdsaLocalSocket) << "Bytes written, now disconnecting"
160 << "Timer remaining" << deadline.remainingTime() << "Expired?" << deadline.hasExpired();
161
162 socket.disconnectFromServer();
163
164 if (socket.state() == QLocalSocket::UnconnectedState) {
165 qCDebug(kdsaLocalSocket) << "Disconnected -- success!";
166 return true;
167 }
168
169 if (!socket.waitForDisconnected(deadline.remainingTime())) {
170 qCWarning(kdsaLocalSocket) << "Disconnection from primary timed out";
171 return false;
172 }
173
174 qCDebug(kdsaLocalSocket) << "Disconnected -- success!";
175
176 return true;
177}
178
179void KDSingleApplicationLocalSocket::handleNewConnection()
180{
181 Q_ASSERT(m_localServer);
182
183 QLocalSocket *socket;
184 while ((socket = m_localServer->nextPendingConnection())) {
185 qCDebug(kdsaLocalSocket) << "Got new connection on" << m_socketName << "state" << socket->state();
186
187 Connection c(std::move(socket));
188 socket = c.socket.get();
189
190 c.readDataConnection = QObjectConnectionHolder(
191 connect(socket, &QLocalSocket::readyRead,
192 this, &KDSingleApplicationLocalSocket::readDataFromSecondary));
193
194 c.secondaryDisconnectedConnection = QObjectConnectionHolder(
195 connect(socket, &QLocalSocket::disconnected,
196 this, &KDSingleApplicationLocalSocket::secondaryDisconnected));
197
198 c.abortConnection = QObjectConnectionHolder(
199 connect(c.timeoutTimer.get(), &QTimer::timeout,
200 this, &KDSingleApplicationLocalSocket::abortConnectionToSecondary));
201
202 m_clients.push_back(std::move(c));
203
204 // Note that by the time we get here, the socket could've already been closed,
205 // and no signals emitted (hello, Windows!). Read what's already in the socket.
206 if (readDataFromSecondarySocket(socket))
207 return;
208
209 if (socket->state() == QLocalSocket::UnconnectedState)
210 secondarySocketDisconnected(socket);
211 }
212}
213
214template<typename Container>
215static auto findConnectionBySocket(Container &container, QLocalSocket *socket)
216{
217 auto i = std::find_if(container.begin(),
218 container.end(),
219 [socket](const auto &c) { return c.socket.get() == socket; });
220 Q_ASSERT(i != container.end());
221 return i;
222}
223
224template<typename Container>
225static auto findConnectionByTimer(Container &container, QTimer *timer)
226{
227 auto i = std::find_if(container.begin(),
228 container.end(),
229 [timer](const auto &c) { return c.timeoutTimer.get() == timer; });
230 Q_ASSERT(i != container.end());
231 return i;
232}
233
234void KDSingleApplicationLocalSocket::readDataFromSecondary()
235{
236 QLocalSocket *socket = static_cast<QLocalSocket *>(sender());
237 readDataFromSecondarySocket(socket);
238}
239
240bool KDSingleApplicationLocalSocket::readDataFromSecondarySocket(QLocalSocket *socket)
241{
242 auto i = findConnectionBySocket(m_clients, socket);
243 Connection &c = *i;
244 c.readData.append(socket->readAll());
245
246 qCDebug(kdsaLocalSocket) << "Got more data from a secondary. Data read so far:" << c.readData;
247
248 const QByteArray &data = c.readData;
249
250 if (data.size() >= 1) {
251 if (data[0] != LOCALSOCKET_PROTOCOL_VERSION) {
252 qCDebug(kdsaLocalSocket) << "Got an invalid protocol version";
253 m_clients.erase(i);
254 return true;
255 }
256 }
257
258 QDataStream ds(data);
259 ds.skipRawData(1);
260
261 ds.startTransaction();
262 QByteArray message;
263 ds >> message;
264
265 if (ds.commitTransaction()) {
266 qCDebug(kdsaLocalSocket) << "Got a complete message:" << message;
267 Q_EMIT messageReceived(message);
268 m_clients.erase(i);
269 return true;
270 }
271
272 return false;
273}
274
275void KDSingleApplicationLocalSocket::secondaryDisconnected()
276{
277 QLocalSocket *socket = static_cast<QLocalSocket *>(sender());
278 secondarySocketDisconnected(socket);
279}
280
281void KDSingleApplicationLocalSocket::secondarySocketDisconnected(QLocalSocket *socket)
282{
283 auto i = findConnectionBySocket(m_clients, socket);
284 Connection c = std::move(*i);
285 m_clients.erase(i);
286
287 qCDebug(kdsaLocalSocket) << "Secondary disconnected. Data read:" << c.readData;
288}
289
290void KDSingleApplicationLocalSocket::abortConnectionToSecondary()
291{
292 QTimer *timer = static_cast<QTimer *>(sender());
293
294 auto i = findConnectionByTimer(m_clients, timer);
295 Connection c = std::move(*i);
296 m_clients.erase(i);
297
298 qCDebug(kdsaLocalSocket) << "Secondary timed out. Data read:" << c.readData;
299}
300
301KDSingleApplicationLocalSocket::Connection::Connection(QLocalSocket *_socket)
302 : socket(_socket)
303 , timeoutTimer(new QTimer)
304{
305 timeoutTimer->start(LOCALSOCKET_CONNECTION_TIMEOUT);
306}
static auto findConnectionByTimer(Container &container, QTimer *timer)
static auto findConnectionBySocket(Container &container, QLocalSocket *socket)
static const auto LOCALSOCKET_CONNECTION_TIMEOUT
static const char LOCALSOCKET_PROTOCOL_VERSION
Q_LOGGING_CATEGORY(kdsaLocalSocket, "kdsingleapplication.localsocket", QtWarningMsg)
int size() const const
QString tempPath()
QByteArray readAll()
void readyRead()
qint64 write(const char *data, qint64 maxSize)
void newConnection()
bool removeServer(const QString &name)
virtual qint64 bytesToWrite() const const override
void connectToServer(QIODevice::OpenMode openMode)
void disconnectFromServer()
void disconnected()
QLocalSocket::LocalSocketState state() const const
virtual bool waitForBytesWritten(int msecs) override
bool waitForConnected(int msecs)
bool waitForDisconnected(int msecs)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString fromUtf8(const char *str, int size)
void timeout()

© 2019-2023 Klarälvdalens Datakonsult AB (KDAB)
"The Qt, C++ and OpenGL Experts"
https://www.kdab.com/
KDSingleApplication
A helper class for single-instance policy Qt applications
Generated on Wed Dec 20 2023 00:04:51 for KDSingleApplication API Documentation by doxygen 1.9.8