LCOV - code coverage report
Current view: top level - muse/framework/update/internal - appupdatescenario.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 80.7 % 207 167
Test Date: 2026-11-05 04:07:02

            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 : }
        

Generated by: LCOV version 2.0-1