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

© 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 Tue Jul 9 2024 00:00:38 for KDSingleApplication API Documentation by doxygen 1.9.8