Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2026 MuseScore Limited
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include "../singleinstance.h"
24 :
25 : #include <QLocalServer>
26 : #include <QLocalSocket>
27 :
28 : #include "multiprocess/ipc/ipc.h"
29 : #include "multiprocess/ipc/ipclock.h"
30 :
31 : #include "log.h"
32 :
33 : namespace muse::mi {
34 : static const QString SINGLE_INSTANCE_ACTIVATE("single_instance_activate");
35 : constexpr int ACK_WAIT_MSEC = 5000;
36 : }
37 :
38 0 : bool muse::mi::activateExistingInstance(const QString& applicationId, const QStringList& args)
39 : {
40 0 : if (applicationId.isEmpty()) {
41 0 : LOGE() << "empty applicationId";
42 0 : return false;
43 : }
44 :
45 0 : ipc::IpcLock lock(applicationId);
46 0 : ipc::IpcLockGuard guard(&lock);
47 :
48 0 : QLocalSocket socket;
49 0 : socket.connectToServer(applicationId);
50 0 : if (!socket.waitForConnected(ipc::TIMEOUT_MSEC)) {
51 0 : LOGD() << "no listener for " << applicationId << "; this is the first instance";
52 0 : return false;
53 : }
54 :
55 0 : ipc::Msg msg;
56 0 : msg.type = ipc::MsgType::Notify;
57 0 : msg.method = SINGLE_INSTANCE_ACTIVATE;
58 0 : msg.args = args;
59 :
60 0 : QByteArray data;
61 0 : ipc::serialize(msg, data);
62 :
63 0 : if (!ipc::writeToSocket(&socket, data)) {
64 0 : LOGE() << "failed to write activation message";
65 0 : return false;
66 : }
67 :
68 0 : if (!socket.waitForReadyRead(ACK_WAIT_MSEC)) {
69 0 : LOGW() << "no ack from existing instance within " << ACK_WAIT_MSEC << "ms";
70 0 : return false;
71 : }
72 :
73 0 : const QByteArray ack = socket.readAll();
74 0 : if (ack != ipc::ACK) {
75 0 : LOGW() << "unexpected ack payload: " << ack;
76 0 : return false;
77 : }
78 :
79 0 : socket.disconnectFromServer();
80 0 : LOGI() << "handoff acked by existing " << applicationId;
81 0 : return true;
82 0 : }
83 :
84 : namespace muse::mi {
85 0 : SingleInstanceListener::SingleInstanceListener() = default;
86 :
87 0 : SingleInstanceListener::~SingleInstanceListener()
88 : {
89 0 : stop();
90 0 : }
91 :
92 0 : bool SingleInstanceListener::start(const QString& applicationId)
93 : {
94 0 : if (m_server) {
95 0 : return true;
96 : }
97 0 : if (applicationId.isEmpty()) {
98 0 : LOGE() << "empty applicationId";
99 0 : return false;
100 : }
101 :
102 0 : m_server = new QLocalServer();
103 :
104 0 : bool ok = m_server->listen(applicationId);
105 :
106 0 : if (!ok && m_server->serverError() == QAbstractSocket::AddressInUseError) {
107 0 : LOGW() << "stale endpoint for " << applicationId << ", removing";
108 0 : QLocalServer::removeServer(applicationId);
109 0 : ok = m_server->listen(applicationId);
110 : }
111 :
112 0 : if (!ok) {
113 0 : LOGE() << "failed to listen on " << applicationId << ": " << m_server->errorString();
114 0 : delete m_server;
115 0 : m_server = nullptr;
116 0 : return false;
117 : }
118 :
119 0 : QObject::connect(m_server, &QLocalServer::newConnection, m_server, [this]() {
120 0 : onNewConnection();
121 0 : });
122 :
123 0 : LOGI() << "single-instance listener bound: " << applicationId;
124 0 : return true;
125 : }
126 :
127 0 : void SingleInstanceListener::stop()
128 : {
129 0 : if (!m_server) {
130 0 : return;
131 : }
132 0 : m_server->close();
133 0 : delete m_server;
134 0 : m_server = nullptr;
135 : }
136 :
137 0 : bool SingleInstanceListener::isListening() const
138 : {
139 0 : return m_server && m_server->isListening();
140 : }
141 :
142 0 : async::Channel<QStringList> SingleInstanceListener::messageReceived() const
143 : {
144 0 : return m_messageReceived;
145 : }
146 :
147 0 : void SingleInstanceListener::onNewConnection()
148 : {
149 0 : QLocalSocket* socket = m_server->nextPendingConnection();
150 0 : if (!socket) {
151 0 : return;
152 : }
153 :
154 0 : QObject::connect(socket, &QLocalSocket::disconnected, socket, &QObject::deleteLater);
155 :
156 0 : auto channel = m_messageReceived;
157 :
158 0 : auto handleData = [socket, channel]() mutable {
159 0 : ipc::readFromSocket(socket, [socket, channel](const QByteArray& data) mutable {
160 0 : ipc::Msg msg;
161 0 : ipc::deserialize(data, msg);
162 :
163 0 : if (msg.method != SINGLE_INSTANCE_ACTIVATE) {
164 0 : LOGW() << "ignoring unexpected method: " << msg.method;
165 0 : return;
166 : }
167 :
168 0 : socket->write(ipc::ACK);
169 0 : socket->flush();
170 :
171 0 : LOGI() << "received activation from second instance";
172 0 : LOGD() << "args: " << msg.args;
173 0 : channel.send(msg.args);
174 0 : });
175 0 : };
176 :
177 0 : QObject::connect(socket, &QLocalSocket::readyRead, socket, handleData);
178 :
179 0 : if (socket->bytesAvailable() > 0) {
180 0 : handleData();
181 : }
182 0 : }
183 : }
|