Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2021 MuseScore Limited and others
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 : #include "multiprocessprovider.h"
23 :
24 : #include <QProcess>
25 : #include <QCoreApplication>
26 : #include <QTimer>
27 : #include <QEventLoop>
28 :
29 : #include "types/uri.h"
30 : #include "settings.h"
31 :
32 : #include "rcommand/commandtypes.h"
33 : #include "multiwindowscommands.h"
34 :
35 : #include "log.h"
36 :
37 : using namespace muse;
38 : using namespace muse::mi;
39 : using namespace muse::ipc;
40 : using namespace muse::rcommand;
41 :
42 : static const QString METHOD_PROJECT_IS_OPENED("PROJECT_IS_OPENED");
43 : static const QString METHOD_ACTIVATE_WINDOW_WITH_PROJECT("ACTIVATE_WINDOW_WITH_PROJECT");
44 : static const QString METHOD_IS_WITHOUT_PROJECT("IS_WITHOUT_PROJECT");
45 : static const QString METHOD_ACTIVATE_WINDOW_WITHOUT_PROJECT("METHOD_ACTIVATE_WINDOW_WITHOUT_PROJECT");
46 :
47 : static const muse::Uri PREFERENCES_URI("muse://preferences");
48 : static const QString METHOD_PREFERENCES_IS_OPENED("PREFERENCES_IS_OPENED");
49 : static const QString METHOD_ACTIVATE_WINDOW_WITH_OPENED_PREFERENCES("ACTIVATE_WINDOW_WITH_OPENED_PREFERENCES");
50 : static const QString METHOD_SETTINGS_BEGIN_TRANSACTION("SETTINGS_BEGIN_TRANSACTION");
51 : static const QString METHOD_SETTINGS_COMMIT_TRANSACTION("SETTINGS_COMMIT_TRANSACTION");
52 : static const QString METHOD_SETTINGS_ROLLBACK_TRANSACTION("SETTINGS_ROLLBACK_TRANSACTION");
53 : static const QString METHOD_SETTINGS_RESET("SETTINGS_RESET");
54 : static const QString METHOD_SETTINGS_SET_VALUE("SETTINGS_SET_VALUE");
55 : static const QString METHOD_QUIT("METHOD_QUIT");
56 : static const QString METHOD_QUIT_WITH_RESTART_LAST_INSTANCE("METHOD_QUIT_WITH_RESTART_LAST_INSTANCE");
57 : static const QString METHOD_QUIT_WITH_RUNING_INSTALLATION("METHOD_QUIT_WITH_RUNING_INSTALLATION");
58 : static const QString METHOD_INSTANCE_CLOSED("INSTANCE_CLOSED");
59 : static const QString METHOD_RESOURCE_CHANGED("RESOURCE_CHANGED");
60 :
61 0 : MultiProcessProvider::~MultiProcessProvider()
62 : {
63 0 : delete m_ipcChannel;
64 0 : }
65 :
66 0 : void MultiProcessProvider::initOnGlobal()
67 : {
68 0 : m_ipcChannel = new IpcChannel();
69 0 : m_selfID = m_ipcChannel->selfID().toStdString();
70 0 : LOGI() << "our instance id is: " << m_selfID;
71 :
72 0 : m_ipcChannel->msgReceived().onReceive(this, [this](const Msg& msg) { onMsg(msg); });
73 0 : m_ipcChannel->instancesChanged().onNotify(this, [this]() { m_instancesChanged.notify(); });
74 :
75 0 : m_ipcChannel->connect();
76 0 : }
77 :
78 0 : bool MultiProcessProvider::isInited() const
79 : {
80 0 : return m_ipcChannel != nullptr;
81 : }
82 :
83 0 : void MultiProcessProvider::onMsg(const Msg& msg)
84 : {
85 0 : LOGI() << msg.method << ", me: " << m_selfID << ", msg src: " << msg.srcID << ", msg dest: " << msg.destID;
86 :
87 : #define CHECK_ARGS_COUNT(c) IF_ASSERT_FAILED(msg.args.count() >= c) { return; }
88 :
89 : // Project opening
90 0 : if (msg.type == MsgType::Request && msg.method == METHOD_PROJECT_IS_OPENED) {
91 0 : CHECK_ARGS_COUNT(1);
92 0 : io::path_t scorePath = io::path_t(msg.args.at(0));
93 0 : bool isOpened = false;
94 0 : if (projectProvider()) {
95 0 : isOpened = projectProvider()->isProjectOpened(scorePath);
96 : } else {
97 0 : LOGW() << "Unable perform request, not found implementation of IProjectProvider";
98 : }
99 0 : m_ipcChannel->response(METHOD_PROJECT_IS_OPENED, { QString::number(isOpened) }, msg.srcID);
100 0 : } else if (msg.method == METHOD_ACTIVATE_WINDOW_WITH_PROJECT) {
101 0 : CHECK_ARGS_COUNT(1);
102 0 : io::path_t scorePath = io::path_t(msg.args.at(0));
103 0 : bool isOpened = false;
104 0 : if (projectProvider()) {
105 0 : isOpened = projectProvider()->isProjectOpened(scorePath);
106 : } else {
107 0 : LOGW() << "Unable perform request, not found implementation of IProjectProvider";
108 : }
109 0 : if (isOpened) {
110 0 : mainWindow()->requestShowOnFront();
111 : }
112 0 : } else if (msg.type == MsgType::Request && msg.method == METHOD_IS_WITHOUT_PROJECT) {
113 0 : bool isAnyOpened = false;
114 0 : if (projectProvider()) {
115 0 : isAnyOpened = projectProvider()->isAnyProjectOpened();
116 : } else {
117 0 : LOGW() << "Unable perform request, not found implementation of IProjectProvider";
118 : }
119 0 : m_ipcChannel->response(METHOD_IS_WITHOUT_PROJECT, { QString::number(!isAnyOpened) }, msg.srcID);
120 0 : } else if (msg.method == METHOD_ACTIVATE_WINDOW_WITHOUT_PROJECT) {
121 0 : bool isAnyOpened = false;
122 0 : if (projectProvider()) {
123 0 : isAnyOpened = projectProvider()->isAnyProjectOpened();
124 : } else {
125 0 : LOGW() << "Unable perform request, not found implementation of IProjectProvider";
126 : }
127 0 : if (!isAnyOpened) {
128 0 : mainWindow()->requestShowOnFront();
129 0 : if (msg.args.count() > 0 && !msg.args.at(0).isEmpty()) {
130 0 : commandDispatcher()->dispatch(CommandQuery(msg.args.at(0).toStdString()));
131 : }
132 : }
133 0 : m_ipcChannel->response(METHOD_ACTIVATE_WINDOW_WITH_PROJECT, { QString::number(!isAnyOpened) }, msg.srcID);
134 : }
135 : // Settings
136 0 : else if (msg.type == MsgType::Request && msg.method == METHOD_PREFERENCES_IS_OPENED) {
137 0 : bool isOpened = interactive()->isOpened(PREFERENCES_URI).val;
138 0 : m_ipcChannel->response(METHOD_PREFERENCES_IS_OPENED, { QString::number(isOpened) }, msg.srcID);
139 0 : } else if (msg.method == METHOD_ACTIVATE_WINDOW_WITH_OPENED_PREFERENCES) {
140 0 : bool isOpened = interactive()->isOpened(PREFERENCES_URI).val;
141 0 : if (isOpened) {
142 0 : mainWindow()->requestShowOnFront();
143 : }
144 0 : } else if (msg.method == METHOD_SETTINGS_BEGIN_TRANSACTION) {
145 0 : settings()->beginTransaction(false);
146 0 : } else if (msg.method == METHOD_SETTINGS_COMMIT_TRANSACTION) {
147 0 : settings()->commitTransaction(false);
148 0 : } else if (msg.method == METHOD_SETTINGS_ROLLBACK_TRANSACTION) {
149 0 : settings()->rollbackTransaction(false);
150 0 : } else if (msg.method == METHOD_SETTINGS_RESET) {
151 0 : settings()->reset(true, true, false);
152 0 : } else if (msg.method == METHOD_SETTINGS_SET_VALUE) {
153 0 : CHECK_ARGS_COUNT(3);
154 0 : Settings::Key key("", msg.args.at(0).toStdString());
155 0 : Val val(msg.args.at(1).toStdString());
156 0 : val.setType(static_cast<Val::Type>(msg.args.at(2).toInt()));
157 0 : settings()->setLocalValue(key, val);
158 0 : } else if (msg.method == METHOD_QUIT) {
159 0 : commandDispatcher()->dispatch(APP_QUIT_COMMAND, { { "all_instances", Val(false) } });
160 0 : } else if (msg.method == METHOD_QUIT_WITH_RESTART_LAST_INSTANCE) {
161 0 : commandDispatcher()->dispatch(APP_RESTART_COMMAND);
162 0 : } else if (msg.method == METHOD_QUIT_WITH_RUNING_INSTALLATION) {
163 0 : CHECK_ARGS_COUNT(1);
164 0 : commandDispatcher()->dispatch(APP_QUIT_COMMAND, {
165 0 : { "all_instances", Val(false) },
166 0 : { "installer_path", Val(msg.args.at(0).toStdString()) } });
167 0 : } else if (msg.method == METHOD_INSTANCE_CLOSED && msg.type == ipc::MsgType::Request) {
168 0 : m_ipcChannel->response(METHOD_INSTANCE_CLOSED, { }, msg.srcID);
169 0 : } else if (msg.method == METHOD_RESOURCE_CHANGED) {
170 0 : resourceChanged().send(msg.args.at(0).toStdString());
171 : }
172 0 : }
173 :
174 0 : bool MultiProcessProvider::isProjectAlreadyOpened(const io::path_t& projectPath) const
175 : {
176 0 : if (!isInited()) {
177 0 : return false;
178 : }
179 :
180 0 : int ret = 0;
181 0 : m_ipcChannel->syncRequestToAll(METHOD_PROJECT_IS_OPENED, { projectPath.toQString() }, [&ret](const QStringList& args, const ID&) {
182 0 : IF_ASSERT_FAILED(!args.empty()) {
183 0 : return false;
184 : }
185 0 : ret = args.at(0).toInt();
186 0 : if (ret) {
187 0 : return true;
188 : }
189 :
190 0 : return false;
191 : });
192 0 : return ret;
193 0 : }
194 :
195 0 : void MultiProcessProvider::activateWindowWithProject(const io::path_t& projectPath)
196 : {
197 0 : if (!isInited()) {
198 0 : return;
199 : }
200 :
201 : #ifndef Q_OS_MAC
202 : // On macOS, this is not desirable, since raising the main window of the other instance works properly without this
203 0 : mainWindow()->requestShowOnBack();
204 : #endif
205 :
206 0 : m_ipcChannel->broadcast(METHOD_ACTIVATE_WINDOW_WITH_PROJECT, { projectPath.toQString() });
207 0 : }
208 :
209 0 : bool MultiProcessProvider::isHasWindowWithoutProject() const
210 : {
211 0 : if (!isInited()) {
212 0 : return false;
213 : }
214 :
215 0 : bool ret = false;
216 0 : m_ipcChannel->syncRequestToAll(METHOD_IS_WITHOUT_PROJECT, {}, [&ret](const QStringList& args, const ID&) {
217 0 : IF_ASSERT_FAILED(!args.empty()) {
218 0 : return false;
219 : }
220 0 : if (args.at(0).toInt()) {
221 0 : ret = true;
222 0 : return true;
223 : }
224 0 : return false;
225 : });
226 0 : return ret;
227 : }
228 :
229 0 : void MultiProcessProvider::activateWindowWithoutProject(const QStringList& args)
230 : {
231 0 : if (!isInited()) {
232 0 : return;
233 : }
234 :
235 : #ifndef Q_OS_MAC
236 : // On macOS, this is not desirable, since raising the main window of the other instance works properly without this
237 0 : mainWindow()->requestShowOnBack();
238 : #endif
239 :
240 0 : ID idWithNoProject;
241 0 : m_ipcChannel->syncRequestToAll(METHOD_IS_WITHOUT_PROJECT, {}, [&idWithNoProject](const QStringList& retArgs, const ID& srcId) {
242 0 : IF_ASSERT_FAILED(!retArgs.empty()) {
243 0 : return false;
244 : }
245 0 : if (retArgs.at(0).toInt()) {
246 0 : idWithNoProject = srcId;
247 0 : return true;
248 : }
249 0 : return false;
250 : });
251 0 : if (!idWithNoProject.isEmpty()) {
252 0 : m_ipcChannel->response(METHOD_ACTIVATE_WINDOW_WITHOUT_PROJECT, args, idWithNoProject);
253 0 : return;
254 : }
255 0 : }
256 :
257 0 : bool MultiProcessProvider::openNewWindow(const QStringList& args)
258 : {
259 0 : if (!isInited()) {
260 0 : return false;
261 : }
262 :
263 0 : QList<ipc::ID> currentApps = m_ipcChannel->instances();
264 :
265 0 : QString appPath = QCoreApplication::applicationFilePath();
266 0 : bool ok = QProcess::startDetached(appPath, args);
267 0 : if (ok) {
268 0 : LOGI() << "success start: " << appPath << ", args: " << args;
269 : } else {
270 0 : LOGE() << "failed start: " << appPath << ", args: " << args;
271 0 : return ok;
272 : }
273 :
274 0 : auto sleep = [](int msec) {
275 0 : QTimer timer;
276 0 : QEventLoop loop;
277 0 : QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
278 0 : timer.start(msec);
279 0 : loop.exec();
280 0 : };
281 :
282 0 : auto waitNewInstance = [this, sleep, currentApps](int waitMs, int count) {
283 0 : for (int i = 0; i < count; ++i) {
284 0 : sleep(waitMs);
285 0 : QList<ipc::ID> apps = m_ipcChannel->instances();
286 0 : for (const ipc::ID& id : apps) {
287 0 : if (!currentApps.contains(id)) {
288 0 : LOGI() << "created new instance with ID: " << id;
289 0 : return true;
290 : }
291 : }
292 0 : }
293 0 : return false;
294 0 : };
295 :
296 : //! NOTE Waiting for a new instance to be created
297 0 : ok = waitNewInstance(100, 50);
298 0 : if (!ok) {
299 0 : LOGE() << "we didn't wait for registration and response from the new instance";
300 : }
301 :
302 0 : return ok;
303 0 : }
304 :
305 0 : bool MultiProcessProvider::isPreferencesAlreadyOpened() const
306 : {
307 0 : if (!isInited()) {
308 0 : return false;
309 : }
310 :
311 0 : int ret = 0;
312 0 : m_ipcChannel->syncRequestToAll(METHOD_PREFERENCES_IS_OPENED, {}, [&ret](const QStringList& args, const ID&) {
313 0 : IF_ASSERT_FAILED(!args.empty()) {
314 0 : return false;
315 : }
316 0 : ret = args.at(0).toInt();
317 0 : if (ret) {
318 0 : return true;
319 : }
320 :
321 0 : return false;
322 : });
323 0 : return ret;
324 : }
325 :
326 0 : void MultiProcessProvider::activateWindowWithOpenedPreferences() const
327 : {
328 0 : if (!isInited()) {
329 0 : return;
330 : }
331 :
332 : #ifndef Q_OS_MAC
333 : // On macOS, this is not desirable, since raising the main window of the other instance works properly without this
334 0 : mainWindow()->requestShowOnBack();
335 : #endif
336 :
337 0 : m_ipcChannel->broadcast(METHOD_ACTIVATE_WINDOW_WITH_OPENED_PREFERENCES);
338 : }
339 :
340 0 : void MultiProcessProvider::settingsBeginTransaction()
341 : {
342 0 : if (!isInited()) {
343 0 : return;
344 : }
345 :
346 0 : m_ipcChannel->broadcast(METHOD_SETTINGS_BEGIN_TRANSACTION);
347 : }
348 :
349 0 : void MultiProcessProvider::settingsCommitTransaction()
350 : {
351 0 : if (!isInited()) {
352 0 : return;
353 : }
354 :
355 0 : m_ipcChannel->broadcast(METHOD_SETTINGS_COMMIT_TRANSACTION);
356 : }
357 :
358 0 : void MultiProcessProvider::settingsRollbackTransaction()
359 : {
360 0 : if (!isInited()) {
361 0 : return;
362 : }
363 :
364 0 : m_ipcChannel->broadcast(METHOD_SETTINGS_ROLLBACK_TRANSACTION);
365 : }
366 :
367 0 : void MultiProcessProvider::settingsReset()
368 : {
369 0 : if (!isInited()) {
370 0 : return;
371 : }
372 :
373 0 : m_ipcChannel->broadcast(METHOD_SETTINGS_RESET);
374 : }
375 :
376 0 : void MultiProcessProvider::settingsSetValue(const std::string& key, const Val& value)
377 : {
378 0 : if (!isInited()) {
379 0 : return;
380 : }
381 :
382 0 : QStringList args;
383 0 : args << QString::fromStdString(key);
384 0 : args << value.toQString();
385 0 : args << QString::number(static_cast<int>(value.type()));
386 0 : m_ipcChannel->broadcast(METHOD_SETTINGS_SET_VALUE, args);
387 0 : }
388 :
389 0 : muse::ipc::IpcLock* MultiProcessProvider::lock(const std::string& name)
390 : {
391 0 : auto it = m_locks.find(name);
392 0 : if (it != m_locks.end()) {
393 0 : return it->second;
394 : }
395 0 : ipc::IpcLock* l = new ipc::IpcLock(QString::fromStdString(name));
396 0 : m_locks[name] = l;
397 0 : return l;
398 : }
399 :
400 0 : bool MultiProcessProvider::lockResource(const std::string& name)
401 : {
402 0 : return lock(name)->lock();
403 : }
404 :
405 0 : bool MultiProcessProvider::unlockResource(const std::string& name)
406 : {
407 0 : return lock(name)->unlock();
408 : }
409 :
410 0 : void MultiProcessProvider::notifyAboutResourceChanged(const std::string& name)
411 : {
412 0 : if (!isInited()) {
413 0 : return;
414 : }
415 :
416 0 : QStringList args;
417 0 : args << QString::fromStdString(name);
418 0 : m_ipcChannel->broadcast(METHOD_RESOURCE_CHANGED, args);
419 0 : }
420 :
421 0 : async::Channel<std::string> MultiProcessProvider::resourceChanged()
422 : {
423 0 : return m_resourceChanged;
424 : }
425 :
426 0 : size_t MultiProcessProvider::windowCount() const
427 : {
428 0 : return m_ipcChannel ? m_ipcChannel->instances().size() : 1;
429 : }
430 :
431 0 : bool MultiProcessProvider::isFirstWindow() const
432 : {
433 0 : return windowCount() <= 1;
434 : }
435 :
436 0 : const std::string& MultiProcessProvider::selfID() const
437 : {
438 0 : return m_selfID;
439 : }
440 :
441 0 : bool MultiProcessProvider::isMainInstance() const
442 : {
443 0 : return m_ipcChannel ? m_ipcChannel->isServer() : false;
444 : }
445 :
446 0 : std::vector<InstanceMeta> MultiProcessProvider::instances() const
447 : {
448 0 : std::vector<InstanceMeta> ret;
449 0 : QList<ipc::ID> ints = m_ipcChannel->instances();
450 0 : for (const ipc::ID& id : std::as_const(ints)) {
451 0 : InstanceMeta im;
452 0 : im.id = id.toStdString();
453 0 : ret.push_back(std::move(im));
454 0 : }
455 :
456 0 : return ret;
457 0 : }
458 :
459 0 : async::Notification MultiProcessProvider::instancesChanged() const
460 : {
461 0 : return m_instancesChanged;
462 : }
463 :
464 0 : void MultiProcessProvider::notifyAboutWindowWasQuited()
465 : {
466 0 : if (!isInited()) {
467 0 : return;
468 : }
469 :
470 0 : m_ipcChannel->broadcast(METHOD_INSTANCE_CLOSED);
471 : }
472 :
473 0 : async::Promise<Ret> MultiProcessProvider::quitForAll(const modularity::ContextPtr& ctx)
474 : {
475 0 : return async::make_promise<Ret>([this, ctx](auto resolve) {
476 0 : if (isInited()) {
477 0 : m_ipcChannel->broadcast(METHOD_QUIT);
478 : }
479 :
480 : // current
481 0 : quitWindow(ctx);
482 :
483 0 : return resolve(make_ok());
484 0 : });
485 : }
486 :
487 0 : void MultiProcessProvider::quitWindow(const modularity::ContextPtr&)
488 : {
489 0 : QCoreApplication::exit();
490 0 : }
491 :
492 0 : void MultiProcessProvider::quitAllAndRestartLast()
493 : {
494 0 : if (!isInited()) {
495 0 : return;
496 : }
497 :
498 0 : m_ipcChannel->broadcast(METHOD_QUIT_WITH_RESTART_LAST_INSTANCE);
499 : }
500 :
501 0 : void MultiProcessProvider::quitAllAndRunInstallation(const io::path_t& installerPath)
502 : {
503 : //! NOTE: Path can be null in some test modes...
504 0 : if (!isInited() || installerPath.empty()) {
505 0 : return;
506 : }
507 :
508 0 : QStringList args;
509 0 : args << installerPath.toQString();
510 0 : m_ipcChannel->broadcast(METHOD_QUIT_WITH_RUNING_INSTALLATION, args);
511 0 : }
|