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) 2025 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 :
23 : #include "appupdatescenario.h"
24 :
25 : #include <QUrl>
26 :
27 : #include "rcommand/commandtypes.h"
28 : #include "updateerrors.h"
29 :
30 : #include "types/val.h"
31 : #include "translation.h"
32 : #include "log.h"
33 :
34 : using namespace muse;
35 : using namespace muse::update;
36 : using namespace muse::async;
37 :
38 3 : void AppUpdateScenario::delayedInit()
39 : {
40 3 : const std::string installing = configuration()->installingReleaseVersion();
41 3 : if (installing.empty()) {
42 1 : return;
43 : }
44 :
45 2 : configuration()->setInstallingReleaseVersion(std::string());
46 :
47 : //! NOTE: The version differs if the user canceled the installer or it failed.
48 2 : bool hasCompletedUpdate = Version(installing) == application()->fullVersion();
49 :
50 2 : if (hasCompletedUpdate) {
51 1 : showUpdateCompletedToast();
52 : }
53 3 : }
54 :
55 0 : bool AppUpdateScenario::needCheckForUpdate() const
56 : {
57 0 : return configuration()->needCheckForUpdate();
58 : }
59 :
60 13 : void AppUpdateScenario::checkForUpdate(bool manual)
61 : {
62 13 : if (m_checkInProgress) {
63 0 : return;
64 : }
65 :
66 13 : m_checkInProgress = true;
67 :
68 13 : service()->checkForUpdate().onResolve(this, [this, manual](const RetVal<ReleaseInfo>& res) {
69 13 : const bool noUpdate = res.ret.code() == static_cast<int>(Err::NoUpdate);
70 :
71 13 : if (manual) {
72 10 : if (noUpdate) {
73 0 : showNoUpdateMsg();
74 10 : } else if (!res.ret) {
75 0 : showServerErrorMsg();
76 : } else {
77 10 : showReleaseInfo(res.val, service()->isReleaseReadyToInstall());
78 : }
79 3 : } else if (!res.ret && !noUpdate) {
80 0 : LOGE() << res.ret.toString();
81 : }
82 :
83 13 : m_checkInProgress = false;
84 :
85 13 : if (!manual && res.ret && hasUpdate()) {
86 2 : if (configuration()->autoUpdateEnabled()) {
87 0 : downloadUpdateInBackground();
88 : } else {
89 2 : showUpdateAvailableToast(res.val, /*downloaded*/ false);
90 : }
91 : }
92 13 : });
93 : }
94 :
95 13 : bool AppUpdateScenario::hasUpdate() const
96 : {
97 13 : if (m_checkInProgress) {
98 0 : return false;
99 : }
100 :
101 13 : const RetVal<ReleaseInfo>& lastCheckResult = service()->lastCheckResult();
102 13 : if (!lastCheckResult.ret) {
103 0 : return false;
104 : }
105 :
106 13 : if (lastCheckResult.ret.code() == static_cast<int>(Err::NoUpdate)) {
107 0 : return false;
108 : }
109 :
110 13 : return !shouldIgnoreUpdate(lastCheckResult.val);
111 : }
112 :
113 2 : Promise<Ret> AppUpdateScenario::processUpdateError(const Ret& error)
114 : {
115 2 : const int errorCode = error.code();
116 2 : if (errorCode == static_cast<int>(Ret::Code::Cancel)) {
117 4 : return async::make_promise<Ret>([](auto resolve, auto) {
118 2 : return resolve(muse::make_ret(Ret::Code::Cancel));
119 2 : });
120 : }
121 :
122 0 : const auto unknownError = async::make_promise<Ret>([](auto resolve, auto) {
123 0 : return resolve(muse::make_ret(Ret::Code::UnknownError));
124 0 : });
125 :
126 0 : IF_ASSERT_FAILED(errorCode >= static_cast<int>(Ret::Code::UpdateFirst)
127 : && errorCode <= static_cast<int>(Ret::Code::UpdateLast)) {
128 0 : return unknownError;
129 : }
130 :
131 0 : const Err err = static_cast<Err>(errorCode);
132 0 : IF_ASSERT_FAILED(err != Err::NoError) {
133 0 : return unknownError;
134 : }
135 :
136 0 : auto message = err == Err::NoUpdate ? showNoUpdateMsg() : showServerErrorMsg();
137 0 : return message.then<Ret>(this, [errorCode](const IInteractive::Result&, auto resolve) {
138 0 : const Ret::Code code = static_cast<Ret::Code>(errorCode);
139 0 : return resolve(muse::make_ret(code));
140 0 : });
141 0 : }
142 :
143 0 : Promise<IInteractive::Result> AppUpdateScenario::showNoUpdateMsg()
144 : {
145 0 : std::string webSiteUrl = configuration()->appWebSiteUrl();
146 0 : QUrl url(QString::fromStdString(webSiteUrl));
147 0 : const QString str = muse::qtrc("update", "You already have the latest version of %1. "
148 : "Please visit <a href=\"%2\">%3</a> for news on what’s coming next.")
149 0 : .arg(application()->title().toQString(), QString::fromStdString(webSiteUrl), url.host());
150 :
151 0 : const IInteractive::Text text(str.toStdString(), IInteractive::TextFormat::RichText);
152 0 : const IInteractive::ButtonData okBtn = interactive()->buttonData(IInteractive::Button::Ok);
153 :
154 0 : return interactive()->info(muse::trc("update", "You’re up to date!"), text, { okBtn }, okBtn.btn,
155 0 : IInteractive::Option::WithIcon);
156 0 : }
157 :
158 13 : Promise<Ret> AppUpdateScenario::showReleaseInfo(const ReleaseInfo& info, bool readyToInstall)
159 : {
160 13 : UriQuery query("muse://update/appreleaseinfo");
161 39 : query.addParam("appName", Val(application()->title().toStdString()));
162 39 : query.addParam("version", Val(info.version));
163 39 : query.addParam("notes", Val(info.notes));
164 39 : query.addParam("previousReleasesNotes", Val(releasesNotesToValList(info.previousReleasesNotes)));
165 39 : query.addParam("readyToInstall", Val(readyToInstall));
166 39 : query.addParam("autoUpdateEnabled", Val(configuration()->autoUpdateEnabled()));
167 :
168 26 : return interactive()->open(query).then<Ret>(this, [this, info, readyToInstall](const Val& val, auto resolve) {
169 13 : const std::string actionCode = applyReleaseInfoResult(val);
170 13 : if (actionCode == "close") {
171 2 : return resolve(muse::make_ret(Ret::Code::Cancel));
172 : }
173 :
174 11 : if (actionCode == "skip") {
175 1 : skipRelease(info.version);
176 1 : return resolve(muse::make_ret(Ret::Code::Cancel));
177 : }
178 :
179 : //! NOTE: The user wants this release after all (e.g. chose it in a manual check after skipping it)
180 10 : unskipRelease(info.version);
181 :
182 10 : if (actionCode == "remindLater") {
183 6 : return resolve(muse::make_ret(Ret::Code::Cancel));
184 : }
185 :
186 : //! NOTE: "Restart & update" already tells that the app restarts, no need to ask again
187 4 : downloadRelease(/*confirmRestart*/ !readyToInstall).onResolve(this, [resolve](const Ret& ret) {
188 4 : (void)resolve(ret);
189 : });
190 :
191 4 : return Promise<Ret>::dummy_result();
192 39 : });
193 13 : }
194 :
195 13 : std::string AppUpdateScenario::applyReleaseInfoResult(const Val& result)
196 : {
197 13 : const ValMap map = result.toMap();
198 :
199 13 : const auto autoUpdate = map.find("autoUpdateEnabled");
200 13 : if (autoUpdate != map.end()) {
201 12 : configuration()->setAutoUpdateEnabled(autoUpdate->second.toBool());
202 : }
203 :
204 13 : const auto action = map.find("action");
205 26 : return action != map.end() ? action->second.toString() : std::string();
206 13 : }
207 :
208 1 : void AppUpdateScenario::showUpdateCompletedToast()
209 : {
210 1 : const std::string title = muse::qtrc("update", "Updated to %1 %2")
211 2 : .arg(application()->title().toQString(), application()->fullVersion().toString().toQString())
212 1 : .toStdString();
213 :
214 1 : toastService()->showWithTimeout(title, std::string(), std::chrono::seconds(10), muse::ui::IconCode::Code::TICK_FILLED);
215 1 : }
216 :
217 10 : void AppUpdateScenario::showUpdateAvailableToast(const ReleaseInfo& info, bool downloaded)
218 : {
219 10 : constexpr int seeDetailsBtn = int(toast::ToastActionCode::Custom) + 1;
220 10 : constexpr int installBtn = int(toast::ToastActionCode::Custom) + 2;
221 :
222 10 : const std::string msg = muse::qtrc("update", "%1 %2 is now ready to install.")
223 10 : .arg(application()->title().toQString(), QString::fromStdString(info.version)).toStdString();
224 :
225 60 : toastService()->show(muse::trc("update", "New update available"), msg,
226 : muse::ui::IconCode::Code::INFO_FILLED, true,
227 : {
228 0 : { muse::trc("update", "See details"), seeDetailsBtn },
229 20 : { downloaded ? muse::trc("update", "Restart & update") : muse::trc("update", "Install update"), installBtn, /*accent*/ true },
230 10 : }).onResolve(this, [this, info, downloaded](const toast::ToastResult& result) {
231 7 : if (result.isCode(seeDetailsBtn)) {
232 3 : showReleaseInfo(info, downloaded).onResolve(this, [](const Ret&) {});
233 4 : } else if (result.isCode(installBtn)) {
234 : //! NOTE: "Restart & update" already tells that the app restarts, no need to ask again
235 4 : downloadRelease(/*confirmRestart*/ !downloaded).onResolve(this, [](const Ret&) {});
236 : }
237 7 : });
238 30 : }
239 :
240 0 : Promise<IInteractive::Result> AppUpdateScenario::showServerErrorMsg()
241 : {
242 0 : return interactive()->error(muse::trc("update", "Cannot connect to server"),
243 0 : muse::trc("update", "Sorry - please try again later"),
244 : {}, int(IInteractive::Button::NoButton), { IInteractive::WithIcon },
245 0 : muse::trc("update", "Check for update"));
246 : }
247 :
248 4 : Promise<Ret> AppUpdateScenario::askToRetryOnNotEnoughDiskSpace(const Ret& error, const std::function<Promise<Ret>()>& retry)
249 : {
250 : const IInteractive::ButtonDatas buttons = {
251 4 : interactive()->buttonData(IInteractive::Button::Cancel),
252 4 : interactive()->buttonData(IInteractive::Button::Retry)
253 16 : };
254 :
255 12 : return interactive()->error(muse::trc("update", "Not enough disk space"), error.text(),
256 : buttons, int(IInteractive::Button::Retry), { IInteractive::WithIcon },
257 8 : muse::trc("update", "Check for update"))
258 8 : .then<Ret>(this, [this, retry](const IInteractive::Result& res, auto resolve) {
259 4 : if (!res.isButton(IInteractive::Button::Retry)) {
260 2 : return resolve(muse::make_ret(Ret::Code::Cancel));
261 : }
262 :
263 2 : retry().onResolve(this, [resolve](const Ret& ret) {
264 2 : (void)resolve(ret);
265 : });
266 :
267 2 : return Promise<Ret>::dummy_result();
268 8 : });
269 16 : }
270 :
271 17 : Promise<Ret> AppUpdateScenario::downloadRelease(bool confirmRestart)
272 : {
273 17 : if (!service()->isReleaseReadyToInstall()) {
274 24 : RetVal<Val> rv = interactive()->openSync("muse://update/app?mode=download");
275 12 : if (rv.ret.code() == static_cast<int>(Err::NotEnoughDiskSpace)) {
276 6 : return askToRetryOnNotEnoughDiskSpace(rv.ret, [this, confirmRestart]() { return downloadRelease(confirmRestart); });
277 : }
278 :
279 8 : if (!rv.ret) {
280 2 : return processUpdateError(rv.ret);
281 : }
282 12 : }
283 :
284 11 : if (confirmRestart) {
285 6 : return askToCloseAppAndCompleteInstall();
286 : }
287 :
288 5 : closeAppAndCompleteInstall();
289 :
290 10 : return async::make_promise<Ret>([](auto resolve, auto) {
291 5 : return resolve(muse::make_ok());
292 5 : });
293 : }
294 :
295 6 : Promise<Ret> AppUpdateScenario::askToCloseAppAndCompleteInstall()
296 : {
297 6 : const std::string title = muse::trc("update", "Restart to finish updating");
298 6 : const std::string info = muse::qtrc("update", "%1 needs to close to complete the installation. "
299 : "If you have any unsaved changes, you will be prompted to save them before %1 closes.")
300 6 : .arg(application()->title().toQString()).toStdString();
301 6 : const int restartBtn = int(IInteractive::Button::Apply);
302 : const IInteractive::ButtonDatas buttons = {
303 6 : interactive()->buttonData(IInteractive::Button::Cancel),
304 6 : IInteractive::ButtonData(restartBtn, muse::trc("update", "Restart"), true)
305 24 : };
306 :
307 12 : return interactive()->info(title, info, buttons, restartBtn)
308 6 : .then<Ret>(this, [this](const IInteractive::Result& res, auto resolve) {
309 6 : if (res.isButton(IInteractive::Button::Cancel)) {
310 5 : return resolve(muse::make_ret(Ret::Code::Cancel));
311 : }
312 :
313 1 : closeAppAndCompleteInstall();
314 1 : return resolve(muse::make_ok());
315 12 : });
316 18 : }
317 :
318 6 : void AppUpdateScenario::closeAppAndCompleteInstall()
319 : {
320 6 : const io::path_t packagePath = service()->downloadedReleasePath();
321 :
322 6 : configuration()->setInstallingReleaseVersion(service()->lastCheckResult().val.version);
323 :
324 6 : if (multiwindowsProvider()->windowCount() != 1) {
325 4 : multiwindowsProvider()->quitAllAndRunInstallation(packagePath);
326 : }
327 :
328 30 : commandDispatcher()->dispatch(rcommand::Command("command://app/quit"), {
329 0 : { "all_instances", Val(false) },
330 12 : { "installer_path", Val(packagePath.toStdString()) } });
331 18 : }
332 :
333 13 : bool AppUpdateScenario::shouldIgnoreUpdate(const ReleaseInfo& info) const
334 : {
335 13 : return info.version == configuration()->skippedReleaseVersion();
336 : }
337 :
338 15 : void AppUpdateScenario::downloadUpdateInBackground()
339 : {
340 15 : if (m_bgDownloadInProgress) {
341 3 : return;
342 : }
343 :
344 15 : if (!service()->isReleaseReadyToInstall() && networkInformation()->isMetered()) {
345 2 : LOGI() << "background update download skipped: metered network connection";
346 2 : return;
347 : }
348 :
349 13 : RetVal<Progress> progress = service()->downloadRelease();
350 13 : if (!progress.ret) {
351 1 : LOGE() << progress.ret.toString();
352 1 : return;
353 : }
354 :
355 12 : m_bgDownloadInProgress = true;
356 :
357 12 : progress.val.finished().onReceive(this, [this](const ProgressResult& res) {
358 10 : m_bgDownloadInProgress = false;
359 :
360 : //! NOTE: The release may have been skipped while the download was running.
361 10 : if (!hasUpdate()) {
362 1 : return;
363 : }
364 :
365 9 : if (res.ret) {
366 8 : showUpdateAvailableToast(service()->lastCheckResult().val, /*downloaded*/ true);
367 : }
368 : }, Asyncable::Mode::SetReplace);
369 13 : }
370 :
371 3 : void AppUpdateScenario::skipRelease(const std::string& version)
372 : {
373 3 : configuration()->setSkippedReleaseVersion(version);
374 3 : service()->removeDownloadedRelease();
375 3 : }
376 :
377 10 : void AppUpdateScenario::unskipRelease(const std::string& version)
378 : {
379 10 : if (configuration()->skippedReleaseVersion() == version) {
380 2 : configuration()->setSkippedReleaseVersion(std::string());
381 : }
382 10 : }
|