Line data Source code
1 : // SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2 : // SPDX-License-Identifier: GPL-3.0-or-later
3 : /**
4 : * @file
5 : * @brief Implements SingleApplication; stale-segment and takeover-race
6 : * recovery lives in the constructor and becomePrimary().
7 : */
8 : #include "singleapplication.h"
9 : #include "qtpasslogging.h"
10 : #include <QDebug>
11 : #include <QLocalSocket>
12 : #include <QSharedPointer>
13 : #include <QThread>
14 : #include <utility>
15 :
16 6 : SingleApplication::SingleApplication(
17 : int &argc, char *argv[], // NOLINT(modernize-avoid-c-arrays)
18 6 : QString uniqueKey)
19 6 : : QApplication(argc, argv), _isRunning(false),
20 6 : _uniqueKey(std::move(uniqueKey)) {
21 6 : sharedMemory.setKey(_uniqueKey);
22 6 : if (sharedMemory.attach()) {
23 5 : if (peerIsListening()) {
24 3 : _isRunning = true;
25 3 : return;
26 : }
27 : // The segment outlived the instance that created it (a crash leaves
28 : // System V shared memory behind on Unix). Nobody answers on the socket,
29 : // so take over instead of forwarding into the void forever.
30 4 : qCDebug(lcQtPass) << "Stale single instance segment, taking over.";
31 2 : sharedMemory.detach();
32 : }
33 3 : becomePrimary();
34 0 : }
35 :
36 : // public slots.
37 :
38 9 : void SingleApplication::receiveMessage() {
39 18 : while (QLocalSocket *localSocket = localServer->nextPendingConnection()) {
40 9 : auto buffer = QSharedPointer<QByteArray>::create();
41 9 : connect(localSocket, &QLocalSocket::readyRead, this,
42 18 : [localSocket, buffer]() {
43 18 : buffer->append(localSocket->readAll());
44 9 : if (buffer->size() > maxMessageBytes) {
45 : // Not a command line any more; drop the peer and its data.
46 1 : buffer->clear();
47 1 : localSocket->abort();
48 : }
49 9 : });
50 9 : connect(localSocket, &QLocalSocket::disconnected, this,
51 27 : [this, localSocket, buffer]() {
52 18 : buffer->append(localSocket->readAll());
53 9 : if (!buffer->isEmpty()) {
54 12 : emit messageAvailable(QString::fromUtf8(buffer->constData()));
55 : }
56 9 : localSocket->deleteLater();
57 9 : });
58 9 : }
59 9 : }
60 :
61 : // public functions.
62 24 : auto SingleApplication::isRunning() -> bool { return _isRunning; }
63 :
64 5 : auto SingleApplication::sendMessage(const QString &message) -> bool {
65 5 : if (!_isRunning) {
66 : return false;
67 : }
68 4 : if (forwardMessage(message)) {
69 : return true;
70 : }
71 : // The peer vanished between the probe in the constructor and now. Let go of
72 : // its segment and take its place, so the window main() opens next is the
73 : // reachable instance. Staying attached without listening would make every
74 : // later launch fail create() and open yet another window without IPC.
75 2 : _isRunning = false;
76 2 : sharedMemory.detach();
77 2 : becomePrimary();
78 2 : return false;
79 : }
80 :
81 : // private functions.
82 5 : void SingleApplication::becomePrimary() {
83 : // create shared memory.
84 5 : if (!sharedMemory.create(1)) {
85 : // Another launch claimed the segment first (two launchers recovering
86 : // from the same crash). Give it a moment to start listening and become
87 : // its secondary, so main() forwards instead of opening a second window.
88 2 : if (sharedMemory.attach()) {
89 7 : for (int attempt = 0; attempt < takeoverProbes; ++attempt) {
90 6 : if (peerIsListening()) {
91 1 : _isRunning = true;
92 1 : return;
93 : }
94 5 : QThread::msleep(takeoverProbeIntervalMs);
95 : }
96 1 : sharedMemory.detach();
97 : }
98 2 : qCDebug(lcQtPass) << "Unable to create single instance.";
99 1 : return;
100 : }
101 : // create local server and listen to incoming messages from other
102 : // instances. A socket file left behind by a crashed instance would make
103 : // listen() fail (address in use) and silently disable IPC for good, so
104 : // clear it first: no live peer answered on it, or create() would have
105 : // failed.
106 3 : QLocalServer::removeServer(_uniqueKey);
107 3 : localServer.reset(new QLocalServer(this));
108 3 : localServer->setSocketOptions(QLocalServer::UserAccessOption);
109 3 : connect(localServer.data(), &QLocalServer::newConnection, this,
110 6 : &SingleApplication::receiveMessage);
111 3 : if (!localServer->listen(_uniqueKey)) {
112 2 : qCWarning(lcQtPass) << "SingleApplication: cannot listen on" << _uniqueKey
113 2 : << ":" << localServer->errorString();
114 : // Holding the segment without a server would make every later launch
115 : // attach, fail the probe and then fail create(): no instance could ever
116 : // take over while this one lives. Release it and run without IPC.
117 1 : localServer.reset();
118 1 : sharedMemory.detach();
119 : }
120 : }
121 :
122 4 : auto SingleApplication::forwardMessage(const QString &message) -> bool {
123 4 : QLocalSocket localSocket(this);
124 4 : localSocket.connectToServer(_uniqueKey, QIODevice::WriteOnly);
125 4 : if (!localSocket.waitForConnected(timeout)) {
126 4 : qCDebug(lcQtPass) << localSocket.errorString().toLatin1();
127 2 : return false;
128 : }
129 : QByteArray payload = message.toUtf8();
130 2 : if (payload.isEmpty()) {
131 2 : payload = QByteArray(1, '\0');
132 : }
133 2 : localSocket.write(payload);
134 2 : if (!localSocket.waitForBytesWritten(timeout)) {
135 0 : qCDebug(lcQtPass) << localSocket.errorString().toLatin1();
136 0 : return false;
137 : }
138 2 : localSocket.disconnectFromServer();
139 : return true;
140 4 : }
141 :
142 11 : auto SingleApplication::peerIsListening() -> bool {
143 11 : QLocalSocket probe;
144 11 : probe.connectToServer(_uniqueKey, QIODevice::WriteOnly);
145 11 : if (!probe.waitForConnected(timeout)) {
146 : return false;
147 : }
148 4 : probe.disconnectFromServer();
149 : return true;
150 11 : }
|