KDSingleApplication API Documentation  1.1
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 
42 static const auto LOCALSOCKET_CONNECTION_TIMEOUT = std::chrono::seconds(5);
43 static const char LOCALSOCKET_PROTOCOL_VERSION = 2;
44 
45 Q_LOGGING_CATEGORY(kdsaLocalSocket, "kdsingleapplication.localsocket", QtWarningMsg);
46 
47 KDSingleApplicationLocalSocket::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 
137 KDSingleApplicationLocalSocket::~KDSingleApplicationLocalSocket() = default;
138 
139 bool KDSingleApplicationLocalSocket::isPrimaryInstance() const
140 {
141  return m_localServer != nullptr;
142 }
143 
144 bool 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 
170  socket.write(&LOCALSOCKET_PROTOCOL_VERSION, 1);
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 
212 void 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 
247 template<typename Container>
248 static 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 
257 template<typename Container>
258 static 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 
267 void KDSingleApplicationLocalSocket::readDataFromSecondary()
268 {
269  QLocalSocket *socket = static_cast<QLocalSocket *>(sender());
270  readDataFromSecondarySocket(socket);
271 }
272 
273 bool 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 
308 void KDSingleApplicationLocalSocket::secondaryDisconnected()
309 {
310  QLocalSocket *socket = static_cast<QLocalSocket *>(sender());
311  secondarySocketDisconnected(socket);
312 }
313 
314 void 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 
323 void 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 
334 KDSingleApplicationLocalSocket::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)

© 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 by doxygen 1.9.1