• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IJHack / QtPass / 34835706449

14 Sep 2026 10:57AM UTC coverage: 68.179% (+0.4%) from 67.818%
34835706449

push

github

web-flow
Backport: Harden the single-instance IPC (#1709) (#1728)

* Harden the single-instance IPC (#1682)

A socket file left behind by a crashed QtPass made QLocalServer::listen()
fail with "address in use"; the return value was ignored, so the new
instance ran without a server and every later launch silently exited
after a failed sendMessage(). Launcher clicks did nothing until the user
deleted the file by hand.

- Probe the socket when the shared memory segment attaches; when nobody
  answers the segment is stale (System V shm outlives a crashed
  process), detach and take over instead of forwarding into the void.
- removeServer() before listen(), set UserAccessOption so only the
  owning user can connect, and qWarning() when listen() still fails.
- Read accepted sockets asynchronously: a peer that connects without
  sending anything no longer blocks the GUI thread for a second, an
  empty connection is not reported as a message, and every accepted
  QLocalSocket is deleted once the peer disconnects.
- main() falls through to a normal start when sendMessage() fails
  instead of exiting silently.

QSharedMemory stays: its exclusive create() still arbitrates two
simultaneous launches, which a socket probe alone cannot do once
UserAccessOption makes listen() rename over an existing socket path.

New tst_singleapplication plants a stale socket file before constructing
the application and checks that a client can connect, that the socket is
0700, that messages (and the empty-payload NUL) arrive, that a silent
peer emits nothing and leaks nothing, and that the first instance
reports isRunning() == false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JuQsrHonihp1nARE7bzstc
(cherry picked from commit 38d387321)

* Address review: take over when the peer dies mid-forward

- SingleApplication::sendMessage(): when forwarding fails, clear
  _isRunning, detach the shared-memory se... (continued)

40 of 54 new or added lines in 1 file covered. (74.07%)

1 existing line in 1 file now uncovered.

5003 of 7338 relevant lines covered (68.18%)

66.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.33
/src/singleapplication.cpp
1
// SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "singleapplication.h"
4
#include <QDebug>
5
#include <QLocalSocket>
6
#include <QSharedPointer>
7
#include <QThread>
8
#include <utility>
9
#ifdef QT_DEBUG
10
#include "debughelper.h"
11
#endif
12

13
/**
14
 * @brief SingleApplication::SingleApplication this replaces the QApplication
15
 * allowing for local socket based communications.
16
 * @param argc
17
 * @param argv
18
 * @param uniqueKey
19
 */
20
SingleApplication::SingleApplication(
1 ✔
21
    int &argc, char *argv[], // NOLINT(modernize-avoid-c-arrays)
22
    QString uniqueKey)
1 ✔
23
    : QApplication(argc, argv), _isRunning(false),
1 ✔
24
      _uniqueKey(std::move(uniqueKey)) {
1 ✔
25
  sharedMemory.setKey(_uniqueKey);
1 ✔
26
  if (sharedMemory.attach()) {
1 ✔
27
    if (peerIsListening()) {
1 ✔
28
      _isRunning = true;
1 ✔
29
      return;
1 ✔
30
    }
31
    // The segment outlived the instance that created it (a crash leaves
32
    // System V shared memory behind on Unix). Nobody answers on the socket,
33
    // so take over instead of forwarding into the void forever.
34
#ifdef QT_DEBUG
35
    dbg() << "Stale single instance segment, taking over.";
36
#endif
NEW
37
    sharedMemory.detach();
×
38
  }
NEW
39
  becomePrimary();
×
UNCOV
40
}
×
41

42
// public slots.
43

44
/**
45
 * @brief SingleApplication::receiveMessage we have received (a command line)
46
 * message.
47
 *
48
 * Each accepted socket is read asynchronously so a peer that connects without
49
 * sending anything cannot stall the GUI thread; the message is delivered once
50
 * the peer disconnects and the socket is then deleted.
51
 */
52
void SingleApplication::receiveMessage() {
6 ✔
53
  while (QLocalSocket *localSocket = localServer->nextPendingConnection()) {
12 ✔
54
    auto buffer = QSharedPointer<QByteArray>::create();
6 ✔
55
    connect(localSocket, &QLocalSocket::readyRead, this,
6 ✔
56
            [localSocket, buffer]() {
13 ✔
57
              buffer->append(localSocket->readAll());
14 ✔
58
              if (buffer->size() > maxMessageBytes) {
7 ✔
59
                // Not a command line any more; drop the peer and its data.
60
                buffer->clear();
1 ✔
61
                localSocket->abort();
1 ✔
62
              }
63
            });
7 ✔
64
    connect(localSocket, &QLocalSocket::disconnected, this,
6 ✔
65
            [this, localSocket, buffer]() {
18 ✔
66
              buffer->append(localSocket->readAll());
12 ✔
67
              if (!buffer->isEmpty()) {
6 ✔
68
                emit messageAvailable(QString::fromUtf8(buffer->constData()));
6 ✔
69
              }
70
              localSocket->deleteLater();
6 ✔
71
            });
6 ✔
72
  }
6 ✔
73
}
6 ✔
74

75
// public functions.
76
/**
77
 * @brief SingleApplication::isRunning is there already a QtPass instance
78
 * running, to check whether to be server or client.
79
 * @return
80
 */
81
auto SingleApplication::isRunning() -> bool { return _isRunning; }
10 ✔
82

83
/**
84
 * @brief SingleApplication::sendMessage send a message (from commandline) to an
85
 * already running QtPass instance.
86
 * @param message
87
 * @return
88
 */
89
auto SingleApplication::sendMessage(const QString &message) -> bool {
2 ✔
90
  if (!_isRunning) {
2 ✔
91
    return false;
92
  }
93
  if (forwardMessage(message)) {
1 ✔
94
    return true;
95
  }
96
  // The peer vanished between the probe in the constructor and now. Let go of
97
  // its segment and take its place, so the window main() opens next is the
98
  // reachable instance. Staying attached without listening would make every
99
  // later launch fail create() and open yet another window without IPC.
100
  _isRunning = false;
1 ✔
101
  sharedMemory.detach();
1 ✔
102
  becomePrimary();
1 ✔
103
  return false;
1 ✔
104
}
105

106
// private functions.
107
/**
108
 * @brief SingleApplication::becomePrimary claim the shared-memory segment and
109
 * start listening for messages from later launches.
110
 *
111
 * Does nothing (and leaves isRunning() false) when another instance holds the
112
 * segment; the caller then runs without IPC rather than not at all.
113
 */
114
void SingleApplication::becomePrimary() {
1 ✔
115
  // create shared memory.
116
  if (!sharedMemory.create(1)) {
1 ✔
117
    // Another launch claimed the segment first (two launchers recovering
118
    // from the same crash). Give it a moment to start listening and become
119
    // its secondary, so main() forwards instead of opening a second window.
NEW
120
    if (sharedMemory.attach()) {
×
NEW
121
      for (int attempt = 0; attempt < takeoverProbes; ++attempt) {
×
NEW
122
        if (peerIsListening()) {
×
NEW
123
          _isRunning = true;
×
NEW
124
          return;
×
125
        }
NEW
126
        QThread::msleep(takeoverProbeIntervalMs);
×
127
      }
NEW
128
      sharedMemory.detach();
×
129
    }
130
#ifdef QT_DEBUG
131
    dbg() << "Unable to create single instance.";
132
#endif
NEW
133
    return;
×
134
  }
135
  // create local server and listen to incoming messages from other
136
  // instances. A socket file left behind by a crashed instance would make
137
  // listen() fail (address in use) and silently disable IPC for good, so
138
  // clear it first: no live peer answered on it, or create() would have
139
  // failed.
140
  QLocalServer::removeServer(_uniqueKey);
1 ✔
141
  localServer.reset(new QLocalServer(this));
1 ✔
142
  localServer->setSocketOptions(QLocalServer::UserAccessOption);
1 ✔
143
  connect(localServer.data(), &QLocalServer::newConnection, this,
1 ✔
144
          &SingleApplication::receiveMessage);
2 ✔
145
  if (!localServer->listen(_uniqueKey)) {
1 ✔
NEW
146
    qWarning() << "SingleApplication: cannot listen on" << _uniqueKey << ":"
×
NEW
147
               << localServer->errorString();
×
148
    // Holding the segment without a server would make every later launch
149
    // attach, fail the probe and then fail create(): no instance could ever
150
    // take over while this one lives. Release it and run without IPC.
NEW
151
    localServer.reset();
×
NEW
152
    sharedMemory.detach();
×
153
  }
154
}
155

156
/**
157
 * @brief SingleApplication::forwardMessage deliver a message to the instance
158
 * listening on the local socket.
159
 * @param message
160
 * @return true if the peer accepted the whole payload.
161
 */
162
auto SingleApplication::forwardMessage(const QString &message) -> bool {
1 ✔
163
  QLocalSocket localSocket(this);
1 ✔
164
  localSocket.connectToServer(_uniqueKey, QIODevice::WriteOnly);
1 ✔
165
  if (!localSocket.waitForConnected(timeout)) {
1 ✔
166
#ifdef QT_DEBUG
167
    dbg() << localSocket.errorString().toLatin1();
168
#endif
169
    return false;
170
  }
171
  QByteArray payload = message.toUtf8();
172
  if (payload.isEmpty()) {
×
173
    payload = QByteArray(1, '\0');
×
174
  }
175
  localSocket.write(payload);
×
176
  if (!localSocket.waitForBytesWritten(timeout)) {
×
177
#ifdef QT_DEBUG
178
    dbg() << localSocket.errorString().toLatin1();
179
#endif
180
    return false;
181
  }
182
  localSocket.disconnectFromServer();
×
183
  return true;
184
}
1 ✔
185

186
/**
187
 * @brief SingleApplication::peerIsListening probe whether an instance is
188
 * actually accepting connections on the local socket.
189
 * @return true if a connection to the unique key succeeded.
190
 */
191
auto SingleApplication::peerIsListening() -> bool {
1 ✔
192
  QLocalSocket probe;
1 ✔
193
  probe.connectToServer(_uniqueKey, QIODevice::WriteOnly);
1 ✔
194
  if (!probe.waitForConnected(timeout)) {
1 ✔
195
    return false;
196
  }
197
  probe.disconnectFromServer();
1 ✔
198
  return true;
199
}
1 ✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc