LCOV - code coverage report
Current view: top level - muse/framework/update/internal - appupdateservice.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 77.7 % 462 359
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 "appupdateservice.h"
      24              : 
      25              : #include <QBuffer>
      26              : #include <QColor>
      27              : #include <QJsonParseError>
      28              : #include <QJsonObject>
      29              : #include <QJsonValue>
      30              : #include <QJsonArray>
      31              : #include <QJsonDocument>
      32              : #include <QUrlQuery>
      33              : 
      34              : #include "update/updateerrors.h"
      35              : 
      36              : #include "downloadfiledevice.h"
      37              : 
      38              : #include "async/async.h"
      39              : #include "global/concurrency/concurrent.h"
      40              : #include "runtime.h"
      41              : #include "defer.h"
      42              : #include "translation.h"
      43              : #include "log.h"
      44              : 
      45              : using namespace muse;
      46              : using namespace muse::update;
      47              : using namespace muse::network;
      48              : using namespace muse::async;
      49              : using namespace muse::io;
      50              : 
      51              : const QString INSTALLED_WEEK_BEGINNING_KEY("Installed-Week-Beginning");
      52              : const QString PREVIOUS_REQUEST_DAY_KEY("Previous-Request-Day");
      53              : 
      54              : static const std::string PARTIAL_SUFFIX(".part");
      55              : 
      56              : //! NOTE: In-place install unpacks the package next to it, so reserve room for
      57              : //! the unpacked app on top of the package itself.
      58              : static constexpr uint64_t UNPACK_SIZE_FACTOR = 2;
      59              : static constexpr uint64_t DISK_SPACE_RESERVE = 100ull * 1024 * 1024;
      60              : 
      61              : static constexpr uint64_t MAX_PACKAGE_SIZE = 500ull * 1024 * 1024;
      62              : 
      63              : //! NOTE: The download takes the first 90% of the progress, the preparation
      64              : //! for in-place install takes the rest.
      65              : static constexpr int64_t PROGRESS_TOTAL = 1000;
      66              : static constexpr int64_t PROGRESS_DOWNLOAD_PART = 900;
      67              : 
      68            0 : static QDate calculateWeekBeginForDate(const QDate& date)
      69              : {
      70              :     // 1 (Monday) + 6 mod 7 = 0
      71              :     // 2 (Tuesday) + 6 mod 7 = 1
      72              :     // etc...
      73            0 :     int diffToWeekBegin = (date.dayOfWeek() + 6) % 7;
      74            0 :     return QDate::currentDate().addDays(-diffToWeekBegin);
      75              : }
      76              : 
      77            6 : static ISystemInfo::CpuArchitecture assetArch(const QString& asset)
      78              : {
      79            6 :     if (asset.contains("aarch64")) {
      80            1 :         return ISystemInfo::CpuArchitecture::Arm64;
      81            5 :     } else if (asset.contains("arm")) {
      82            2 :         return ISystemInfo::CpuArchitecture::Arm;
      83              :     }
      84              : 
      85            3 :     return ISystemInfo::CpuArchitecture::x86_64;
      86              : }
      87              : 
      88            7 : static PrevReleasesNotesList parsePreviousReleasesNotes(const QByteArray& json)
      89              : {
      90            7 :     PrevReleasesNotesList result;
      91              : 
      92            7 :     QJsonParseError err;
      93            7 :     QJsonDocument jsonDoc = QJsonDocument::fromJson(json, &err);
      94            7 :     if (err.error != QJsonParseError::NoError || !jsonDoc.isObject()) {
      95            0 :         LOGE() << "failed parse, err: " << err.errorString();
      96            0 :         return result;
      97              :     }
      98              : 
      99            7 :     QJsonObject jsonObject = jsonDoc.object();
     100              : 
     101            7 :     if (jsonObject.empty()) {
     102            0 :         LOGE() << "failed parse, no jsonObject";
     103            0 :         return result;
     104              :     }
     105              : 
     106            7 :     QJsonArray releases = jsonObject.value("releases").toArray();
     107           35 :     for (const QJsonValue& release : releases) {
     108           28 :         QJsonObject releaseObj = release.toObject();
     109           28 :         std::string version = releaseObj.value("version").toString().toStdString();
     110              : 
     111           28 :         result.push_back({ std::move(version), releaseObj.value("notes").toString().toStdString() });
     112           28 :     }
     113              : 
     114            7 :     return result;
     115            7 : }
     116              : 
     117            0 : void AppUpdateService::init()
     118              : {
     119            0 :     TRACEFUNC;
     120              : 
     121            0 :     io::path_t historyPath = configuration()->updateRequestHistoryJsonPath();
     122            0 :     if (fileSystem()->exists(historyPath)) {
     123            0 :         return;
     124              :     }
     125              : 
     126              :     // If the request history file doesn't exist, perform a first time setup using today's date...
     127            0 :     UpdateRequestHistory updateRequestHistory;
     128            0 :     updateRequestHistory.installedWeekBeginning = calculateWeekBeginForDate(QDate::currentDate());
     129            0 :     updateRequestHistory.previousRequestDay = QDate::currentDate();
     130              : 
     131            0 :     Ret ret = writeUpdateRequestHistory(historyPath, updateRequestHistory);
     132            0 :     if (!ret) {
     133            0 :         LOGE() << ret.toString();
     134              :     }
     135            0 : }
     136              : 
     137            7 : Promise<RetVal<ReleaseInfo> > AppUpdateService::checkForUpdate()
     138              : {
     139            0 :     return Promise<RetVal<ReleaseInfo> >([this](auto resolve, auto) {
     140            7 :         clear();
     141              : 
     142            7 :         if (!m_networkManager) {
     143            7 :             m_networkManager = networkManagerCreator()->makeNetworkManager();
     144              :         }
     145              : 
     146              :         // Read request history file...
     147            7 :         io::path_t historyPath = configuration()->updateRequestHistoryJsonPath();
     148            7 :         RetVal<UpdateRequestHistory> historyRv = readUpdateRequestHistory(historyPath);
     149            7 :         if (!historyRv.ret) {
     150            7 :             LOGE() << historyRv.ret.toString();
     151              :         }
     152              : 
     153            7 :         QUrl url = QString::fromStdString(configuration()->checkForAppUpdateUrl());
     154            7 :         if (requestParamsProvider()) {
     155            0 :             QUrlQuery query(url);
     156            0 :             for (const auto& [key, value] : requestParamsProvider()->updateRequestParams()) {
     157            0 :                 query.addQueryItem(QString::fromStdString(key), QString::fromStdString(value));
     158              :             }
     159              : 
     160            0 :             if (!query.isEmpty()) {
     161            0 :                 url.setQuery(query);
     162              :             }
     163            0 :         }
     164              : 
     165            7 :         RequestHeaders headers = prepareHeaders(historyRv.val);
     166            7 :         auto buff = std::make_shared<QBuffer>();
     167              : 
     168            7 :         RetVal<Progress> progress = m_networkManager->get(url, buff, headers);
     169            7 :         if (!progress.ret) {
     170            0 :             m_lastCheckResult.ret = progress.ret;
     171            0 :             return resolve(m_lastCheckResult);
     172              :         }
     173              : 
     174            7 :         progress.val.finished().onReceive(this, [this, historyPath, historyRv, buff, resolve](const ProgressResult& res) {
     175            7 :             if (!res.ret) {
     176            0 :                 m_lastCheckResult.ret = res.ret;
     177            0 :                 (void)resolve(m_lastCheckResult);
     178            0 :                 return;
     179              :             }
     180              : 
     181              :             // Successfully performed request, update history file...
     182            7 :             UpdateRequestHistory updateRequestHistory = historyRv.val;
     183            7 :             updateRequestHistory.previousRequestDay = QDate::currentDate();
     184            7 :             Ret writeHistoryRet = writeUpdateRequestHistory(historyPath, updateRequestHistory);
     185            7 :             if (!writeHistoryRet) {
     186            7 :                 LOGE() << writeHistoryRet.toString();
     187              :             }
     188              : 
     189            7 :             QByteArray json = buff->data();
     190            7 :             RetVal<ReleaseInfo> releaseInfo = parseRelease(json);
     191              : 
     192            7 :             if (!releaseInfo.ret) {
     193            0 :                 m_lastCheckResult.ret = releaseInfo.ret;
     194            0 :                 (void)resolve(m_lastCheckResult);
     195            0 :                 return;
     196              :             }
     197              : 
     198            7 :             Version current(application()->fullVersion());
     199            7 :             Version update(releaseInfo.val.version);
     200              : 
     201            7 :             bool allowUpdateOnPreRelease = configuration()->allowUpdateOnPreRelease();
     202            7 :             bool isPreRelease = update.preRelease();
     203              : 
     204            7 :             if (!allowUpdateOnPreRelease && isPreRelease) {
     205            0 :                 cleanupStalePackages(/*keepFileName*/ std::string());
     206            0 :                 m_lastCheckResult.ret = make_ret(Err::NoUpdate);
     207            0 :                 (void)resolve(m_lastCheckResult);
     208            0 :                 return;
     209              :             }
     210              : 
     211            7 :             if (update <= current) {
     212            0 :                 cleanupStalePackages(/*keepFileName*/ std::string());
     213            0 :                 m_lastCheckResult.ret = make_ret(Err::NoUpdate);
     214            0 :                 (void)resolve(m_lastCheckResult);
     215            0 :                 return;
     216              :             }
     217              : 
     218            7 :             m_lastCheckResult = releaseInfo;
     219              : 
     220              :             //! NOTE: Keep an already-downloaded package for this release; drop stale ones.
     221            7 :             cleanupStalePackages(releaseInfo.val.fileName);
     222              : 
     223            7 :             downloadPreviousReleasesNotes(update, [this, resolve](const PrevReleasesNotesList& notes) {
     224            7 :                 m_lastCheckResult.val.previousReleasesNotes = notes;
     225            7 :                 (void)resolve(m_lastCheckResult);
     226              :             });
     227            7 :         });
     228              : 
     229            7 :         return Promise<RetVal<ReleaseInfo> >::dummy_result();
     230           14 :     });
     231              : }
     232              : 
     233            0 : const RetVal<ReleaseInfo>& AppUpdateService::lastCheckResult() const
     234              : {
     235            0 :     return m_lastCheckResult;
     236              : }
     237              : 
     238           21 : RetVal<Progress> AppUpdateService::downloadRelease()
     239              : {
     240           21 :     if (m_downloadInProgress) {
     241            1 :         return RetVal<Progress>::make_ok(m_updateProgress);
     242              :     }
     243              : 
     244           20 :     if (!m_networkManager) {
     245           18 :         m_networkManager = networkManagerCreator()->makeNetworkManager();
     246              :     }
     247              : 
     248           20 :     if (!m_lastCheckResult.ret) {
     249            0 :         return RetVal<Progress>::make_ret(m_lastCheckResult.ret);
     250              :     }
     251              : 
     252           20 :     const ReleaseInfo info = m_lastCheckResult.val;
     253           20 :     if (info.fileName.empty()) {
     254            0 :         return RetVal<Progress>::make_ret(make_ret(Err::NoUpdate));
     255              :     }
     256              : 
     257           20 :     const path_t finalPath = packagesDir() + "/" + info.fileName;
     258              : 
     259           20 :     m_updateProgress.canceled().disconnect(this);
     260              : 
     261              :     //! NOTE: Already downloaded (e.g. in a previous session), only the preparation is left
     262           20 :     if (fileSystem()->exists(finalPath)) {
     263            4 :         m_downloadInProgress = true;
     264            4 :         m_updateProgress.start();
     265            4 :         prepareUpdate(finalPath);
     266            4 :         return RetVal<Progress>::make_ok(m_updateProgress);
     267              :     }
     268              : 
     269           16 :     const QUrl fileUrl = QUrl::fromUserInput(QString::fromStdString(info.fileUrl));
     270           16 :     const path_t partialPath = finalPath + PARTIAL_SUFFIX;
     271           16 :     fileSystem()->makePath(muse::io::absoluteDirpath(partialPath));
     272              : 
     273              :     //! NOTE: Resume an interrupted download by appending to the partial file and
     274              :     //! requesting the remaining bytes via a Range header.
     275           16 :     uint64_t offset = 0;
     276           16 :     if (fileSystem()->exists(partialPath)) {
     277            3 :         RetVal<uint64_t> sz = fileSystem()->fileSize(partialPath);
     278            3 :         offset = sz.ret ? sz.val : 0;
     279            3 :     }
     280              : 
     281           16 :     Ret spaceRet = checkDiskSpace(DiskSpaceFor::Download, info.fileSize, offset);
     282           16 :     if (!spaceRet) {
     283            2 :         return RetVal<Progress>::make_ret(spaceRet);
     284              :     }
     285              : 
     286           14 :     configuration()->setLastDownloadedPackagePath(finalPath);
     287              : 
     288           14 :     RequestHeaders headers;
     289           14 :     io::IODevice::OpenMode mode = io::IODevice::WriteOnly;
     290           14 :     if (offset > 0) {
     291            3 :         headers.rawHeaders["Range"] = QByteArray("bytes=") + QByteArray::number(static_cast<qulonglong>(offset)) + "-";
     292            3 :         mode = io::IODevice::Append;
     293              :     }
     294              : 
     295           14 :     auto device = std::make_shared<DownloadFileDevice>(partialPath, mode);
     296              : 
     297           14 :     m_downloadInProgress = true;
     298              : 
     299           14 :     RetVal<Progress> downloadProgress = m_networkManager->get(fileUrl, device, headers);
     300           14 :     if (!downloadProgress.ret) {
     301            0 :         m_downloadInProgress = false;
     302            0 :         return RetVal<Progress>::make_ret(downloadProgress.ret);
     303              :     }
     304              : 
     305           14 :     m_updateProgress.start();
     306              : 
     307           14 :     m_updateProgress.canceled().onNotify(this, [this, downloadProgress]() {
     308            1 :         Progress mutProgress = downloadProgress.val;
     309            1 :         mutProgress.cancel();
     310            1 :         m_updateProgress.canceled().disconnect(this);
     311            1 :     }, Asyncable::Mode::SetReplace);
     312              : 
     313           14 :     downloadProgress.val.progressChanged().onReceive(this, [this, offset](int64_t current, int64_t total, const std::string& msg) {
     314            1 :         const int64_t downloaded = static_cast<int64_t>(offset) + current;
     315            1 :         const int64_t size = static_cast<int64_t>(offset) + total;
     316            1 :         if (size <= 0) {
     317            0 :             return;
     318              :         }
     319              : 
     320            1 :         m_updateProgress.progress(downloaded * PROGRESS_DOWNLOAD_PART / size, PROGRESS_TOTAL, msg);
     321              :     }, Asyncable::Mode::SetReplace);
     322              : 
     323           14 :     downloadProgress.val.finished().onReceive(this, [this, finalPath, partialPath, offset](const ProgressResult& res) {
     324            7 :         if (!res.ret) {
     325              :             //! NOTE: Keep the partial file so the next attempt can resume from it.
     326            2 :             m_downloadInProgress = false;
     327            2 :             m_updateProgress.finish(ProgressResult::make_ret(res.ret));
     328            3 :             return;
     329              :         }
     330              : 
     331           10 :         const int status = res.ret.data<int>("status", 0);
     332              : 
     333              :         //! NOTE: We requested a range but the server sent the full file (200) or
     334              :         //! rejected the range (416); the partial file is now stale/corrupt - drop
     335              :         //! it so the next attempt starts clean.
     336            5 :         if (offset > 0 && (status == 200 || status == 416)) {
     337            1 :             fileSystem()->remove(partialPath);
     338            1 :             m_downloadInProgress = false;
     339            2 :             m_updateProgress.finish(ProgressResult::make_ret(make_ret(Err::NetworkError, "range request not honoured")));
     340            1 :             return;
     341              :         }
     342              : 
     343              :         //! Success (200 fresh download or 206 resumed): promote the partial file
     344              :         //! to the final package name.
     345            4 :         const Ret ret = fileSystem()->move(partialPath, finalPath, /*replace*/ true);
     346            4 :         if (!ret) {
     347            0 :             m_downloadInProgress = false;
     348            0 :             m_updateProgress.finish(ProgressResult::make_ret(ret));
     349            0 :             return;
     350              :         }
     351              : 
     352            4 :         m_updateProgress.canceled().disconnect(this);
     353            4 :         prepareUpdate(finalPath);
     354            4 :     }, Asyncable::Mode::SetReplace);
     355              : 
     356           14 :     return RetVal<Progress>::make_ok(m_updateProgress);
     357           20 : }
     358              : 
     359            8 : void AppUpdateService::prepareUpdate(const muse::io::path_t& packagePath)
     360              : {
     361            8 :     resetPreparedUpdate();
     362              : 
     363            8 :     async::Async::call(this, [this, packagePath]() {
     364            7 :         if (m_updateProgress.isCanceled()) {
     365            0 :             m_downloadInProgress = false;
     366            2 :             return;
     367              :         }
     368              : 
     369            7 :         if (!canAutoInstall()) {
     370            1 :             m_downloadInProgress = false;
     371            1 :             m_updateProgress.progress(PROGRESS_TOTAL, PROGRESS_TOTAL);
     372            1 :             m_updateProgress.finish(ProgressResult::make_ok(Val(packagePath)));
     373            1 :             return;
     374              :         }
     375              : 
     376            6 :         m_updateProgress.progress(PROGRESS_DOWNLOAD_PART, PROGRESS_TOTAL,
     377            6 :                                   muse::qtrc("update", "Preparing %1 %2")
     378           12 :                                   .arg(application()->title().toQString(), QString::fromStdString(m_lastCheckResult.val.version))
     379           12 :                                   .toStdString());
     380              : 
     381            6 :         RetVal<uint64_t> packageSize = fileSystem()->fileSize(packagePath);
     382            6 :         const Ret spaceRet = checkDiskSpace(DiskSpaceFor::Unpack, packageSize.ret ? packageSize.val : 0);
     383            6 :         if (!spaceRet) {
     384            1 :             m_downloadInProgress = false;
     385            1 :             m_updateProgress.finish(ProgressResult::make_ret(spaceRet));
     386            1 :             return;
     387              :         }
     388              : 
     389            5 :         auto installer = updateInstaller();
     390              : 
     391            5 :         Concurrent::run([this, installer, packagePath]() {
     392            5 :             const RetVal<io::path_t> prepared = installer->prepareUpdate(packagePath);
     393              : 
     394            5 :             async::Async::call(this, [this, packagePath, prepared]() {
     395            5 :                 m_downloadInProgress = false;
     396              : 
     397            5 :                 if (m_updateProgress.isCanceled()) {
     398            1 :                     if (prepared.ret && prepared.val != packagePath) {
     399            1 :                         fileSystem()->remove(prepared.val);
     400              :                     }
     401            1 :                     return;
     402              :                 }
     403              : 
     404            4 :                 if (prepared.ret) {
     405            3 :                     m_preparedPackagePath = packagePath;
     406            3 :                     m_preparedPath = prepared.val;
     407              :                 } else {
     408              :                     //! NOTE: The package is downloaded anyway, so it can still be installed manually
     409            1 :                     LOGE() << "failed to prepare update: " << prepared.ret.toString();
     410              :                 }
     411              : 
     412            4 :                 m_updateProgress.progress(PROGRESS_TOTAL, PROGRESS_TOTAL);
     413            4 :                 m_updateProgress.finish(ProgressResult::make_ok(Val(packagePath)));
     414            5 :             }, runtime::mainThreadId());
     415            5 :         });
     416            7 :     });
     417            8 : }
     418              : 
     419            8 : muse::io::path_t AppUpdateService::preparedUpdatePath() const
     420              : {
     421            8 :     if (m_preparedPath.empty() || m_preparedPackagePath != downloadedReleasePath()) {
     422            4 :         return {};
     423              :     }
     424              : 
     425              :     //! NOTE: The prepared files may have been removed since
     426            4 :     if (!fileSystem()->exists(m_preparedPath)) {
     427            0 :         return {};
     428              :     }
     429              : 
     430            4 :     return m_preparedPath;
     431              : }
     432              : 
     433           16 : void AppUpdateService::resetPreparedUpdate()
     434              : {
     435           16 :     m_preparedPackagePath = io::path_t();
     436           16 :     m_preparedPath = io::path_t();
     437           16 : }
     438              : 
     439           22 : Ret AppUpdateService::checkDiskSpace(DiskSpaceFor purpose, uint64_t packageSize, uint64_t downloadedBytes) const
     440              : {
     441           22 :     if (packageSize == 0) {
     442           17 :         return make_ok();
     443              :     }
     444              : 
     445            5 :     if (packageSize > MAX_PACKAGE_SIZE) {
     446            1 :         LOGW() << "package size is not sane: " << packageSize;
     447            1 :         return make_ret(Err::NotEnoughDiskSpace);
     448              :     }
     449              : 
     450            4 :     io::path_t dir;
     451            4 :     uint64_t required = DISK_SPACE_RESERVE;
     452              : 
     453            4 :     switch (purpose) {
     454            3 :     case DiskSpaceFor::Download:
     455            3 :         dir = packagesDir();
     456            3 :         required += packageSize - std::min(downloadedBytes, packageSize);
     457            3 :         if (canAutoInstall()) {
     458            0 :             required += packageSize * UNPACK_SIZE_FACTOR;
     459              :         }
     460            3 :         break;
     461            1 :     case DiskSpaceFor::Unpack:
     462            1 :         dir = configuration()->updateDataPath();
     463            1 :         required += packageSize * UNPACK_SIZE_FACTOR;
     464            1 :         break;
     465              :     }
     466              : 
     467            8 :     RetVal<uint64_t> available = fileSystem()->availableSpace(dir);
     468            4 :     if (!available.ret) {
     469            0 :         LOGW() << "failed to query available disk space: " << available.ret.toString();
     470            0 :         return make_ok();
     471              :     }
     472              : 
     473            4 :     if (available.val >= required) {
     474            2 :         return make_ok();
     475              :     }
     476              : 
     477            2 :     LOGW() << "not enough disk space for update: required " << required << " bytes, available " << available.val;
     478              : 
     479            2 :     const uint64_t mb = 1024 * 1024;
     480            2 :     const uint64_t missingMb = (required - available.val + mb - 1) / mb;
     481            2 :     const std::string text = muse::qtrc("update", "Free up at least %1 MB of disk space and try again.")
     482            4 :                              .arg(QString::number(static_cast<qulonglong>(missingMb))).toStdString();
     483              : 
     484            2 :     return make_ret(Err::NotEnoughDiskSpace, text);
     485            4 : }
     486              : 
     487            7 : RequestHeaders AppUpdateService::prepareHeaders(const UpdateRequestHistory& history) const
     488              : {
     489            7 :     RequestHeaders headers = configuration()->updateHeaders();
     490              : 
     491            7 :     if (history.isValid()) {
     492            0 :         std::string iwbString = history.installedWeekBeginning.toString(Qt::ISODate).toStdString();
     493            0 :         QByteArray iwbKey = QByteArray::fromStdString(INSTALLED_WEEK_BEGINNING_KEY.toStdString());
     494            0 :         headers.rawHeaders[iwbKey] = QByteArray::fromStdString(iwbString);
     495              : 
     496            0 :         std::string prdString = history.previousRequestDay.toString(Qt::ISODate).toStdString();
     497            0 :         QByteArray prdKey = QByteArray::fromStdString(PREVIOUS_REQUEST_DAY_KEY.toStdString());
     498            0 :         headers.rawHeaders[prdKey] = QByteArray::fromStdString(prdString);
     499            0 :     }
     500              : 
     501            7 :     return headers;
     502            0 : }
     503              : 
     504            7 : RetVal<AppUpdateService::UpdateRequestHistory> AppUpdateService::readUpdateRequestHistory(const io::path_t& path) const
     505              : {
     506            7 :     RetVal<ByteArray> rv = fileSystem()->readFile(path);
     507            7 :     if (!rv.ret) {
     508            7 :         return RetVal<UpdateRequestHistory>(rv.ret);
     509              :     }
     510              : 
     511            0 :     QJsonDocument historyDoc = QJsonDocument::fromJson(rv.val.toQByteArrayNoCopy());
     512            0 :     QJsonObject historyObject = historyDoc.object();
     513              : 
     514            0 :     UpdateRequestHistory updateRequestHistory;
     515              : 
     516            0 :     QString installedWeekBeginning = historyObject.value(INSTALLED_WEEK_BEGINNING_KEY).toString();
     517            0 :     updateRequestHistory.installedWeekBeginning = QDate::fromString(installedWeekBeginning, Qt::ISODate);
     518              : 
     519            0 :     QString previousRequestDay = historyObject.value(PREVIOUS_REQUEST_DAY_KEY).toString();
     520            0 :     updateRequestHistory.previousRequestDay = QDate::fromString(previousRequestDay, Qt::ISODate);
     521              : 
     522            0 :     return RetVal<UpdateRequestHistory>::make_ok(updateRequestHistory);
     523            7 : }
     524              : 
     525            7 : Ret AppUpdateService::writeUpdateRequestHistory(const io::path_t& path, const UpdateRequestHistory& updateRequestHistory)
     526              : {
     527            7 :     if (!updateRequestHistory.isValid()) {
     528            7 :         return make_ret(Ret::Code::UnknownError);
     529              :     }
     530              : 
     531            0 :     QJsonObject historyObject;
     532              : 
     533            0 :     const QDate& iwb = updateRequestHistory.installedWeekBeginning;
     534            0 :     const QDate& prd = updateRequestHistory.previousRequestDay;
     535              : 
     536            0 :     historyObject[INSTALLED_WEEK_BEGINNING_KEY] = iwb.toString(Qt::ISODate);
     537            0 :     historyObject[PREVIOUS_REQUEST_DAY_KEY] = prd.toString(Qt::ISODate);
     538              : 
     539            0 :     QByteArray byteArray = QJsonDocument(historyObject).toJson();
     540              : 
     541            0 :     return fileSystem()->writeFile(path, ByteArray::fromQByteArrayNoCopy(byteArray));
     542            0 : }
     543              : 
     544            7 : RetVal<ReleaseInfo> AppUpdateService::parseRelease(const QByteArray& json) const
     545              : {
     546            7 :     QJsonParseError err;
     547            7 :     QJsonDocument jsonDoc = QJsonDocument::fromJson(json, &err);
     548            7 :     if (err.error != QJsonParseError::NoError || !jsonDoc.isObject()) {
     549            0 :         return make_ret(Err::ReleaseInfoParseError, err.errorString().toStdString());
     550              :     }
     551              : 
     552            7 :     QJsonObject release = jsonDoc.object();
     553            7 :     QJsonObject assetObj = resolveReleaseAsset(release);
     554            7 :     if (assetObj.empty()) {
     555            0 :         return make_ret(Err::ReleaseInfoParseError, "No release asset");
     556              :     }
     557              : 
     558            7 :     QString tagName = release.value("tag_name").toString();
     559            7 :     QString version = tagName.replace("v", "");
     560            7 :     if (version.isEmpty()) {
     561            0 :         return make_ret(Err::ReleaseInfoParseError, "No release version");
     562              :     }
     563              : 
     564            7 :     RetVal<ReleaseInfo> result;
     565            7 :     result.ret = muse::make_ok();
     566            7 :     result.val.fileName = assetObj.value("name").toString().toStdString();
     567            7 :     result.val.fileUrl = assetObj.value("browser_download_url").toString().toStdString();
     568            7 :     result.val.fileSize = static_cast<uint64_t>(std::max<qint64>(assetObj.value("size").toInteger(), 0));
     569            7 :     result.val.version = version.toStdString();
     570            7 :     result.val.notes = release.value("bodyMarkdown").toString().toStdString();
     571              : 
     572            7 :     return result;
     573            7 : }
     574              : 
     575            7 : std::vector<std::string> AppUpdateService::platformFileSuffixes() const
     576              : {
     577            7 :     switch (systemInfo()->productType()) {
     578            3 :     case ISystemInfo::ProductType::Windows: return { "msi" };
     579            3 :     case ISystemInfo::ProductType::MacOS: return { "dmg" };
     580           15 :     case ISystemInfo::ProductType::Linux: return { "appimage" };
     581            0 :     case ISystemInfo::ProductType::Unknown: break;
     582              :     }
     583              : 
     584            0 :     return {};
     585              : }
     586              : 
     587            7 : QJsonObject AppUpdateService::resolveReleaseAsset(const QJsonObject& release) const
     588              : {
     589            7 :     const std::vector<std::string> fileSuffixes = platformFileSuffixes();
     590            7 :     ISystemInfo::ProductType productType = systemInfo()->productType();
     591            7 :     ISystemInfo::CpuArchitecture arch = systemInfo()->cpuArchitecture();
     592              : 
     593            7 :     QJsonArray assets = release.value("assets").toArray();
     594           21 :     for (const QJsonValue& asset : release.value("assetsNew").toArray()) {
     595           14 :         assets.push_back(asset);
     596           21 :     }
     597              : 
     598              :     // Honour suffix priority: scan all assets for the most preferred suffix
     599              :     // before considering the next one.
     600            7 :     for (const std::string& fileSuffix : fileSuffixes) {
     601           27 :         for (const QJsonValue asset : assets) {
     602           27 :             QJsonObject assetObj = asset.toObject();
     603              : 
     604           27 :             QString name = assetObj.value("name").toString();
     605           27 :             if (io::suffix(name) != fileSuffix) {
     606           17 :                 continue;
     607              :             }
     608              : 
     609           10 :             if (productType == ISystemInfo::ProductType::Linux) {
     610            8 :                 if (arch != ISystemInfo::CpuArchitecture::Unknown && arch != assetArch(name)) {
     611            3 :                     continue;
     612              :                 }
     613              :             }
     614              : 
     615            7 :             return assetObj;
     616           74 :         }
     617              :     }
     618              : 
     619            0 :     return QJsonObject();
     620            7 : }
     621              : 
     622           51 : bool AppUpdateService::canAutoInstall() const
     623              : {
     624           51 :     return updateInstaller()->isInPlaceUpdateSupported();
     625              : }
     626              : 
     627            2 : Ret AppUpdateService::installUpdate()
     628              : {
     629            2 :     const io::path_t preparedPath = preparedUpdatePath();
     630            2 :     if (preparedPath.empty()) {
     631            1 :         LOGE() << "the update is not ready to install";
     632            1 :         return make_ret(Err::UnknownError);
     633              :     }
     634              : 
     635            1 :     return updateInstaller()->finalizeUpdate(preparedPath, makeInstallProgressUi());
     636            2 : }
     637              : 
     638            1 : InstallProgressUi AppUpdateService::makeInstallProgressUi() const
     639              : {
     640            1 :     const QString appName = application()->title().toQString();
     641            1 :     const QString version = QString::fromStdString(m_lastCheckResult.val.version);
     642              : 
     643            1 :     InstallProgressUi progressUi;
     644            1 :     progressUi.title = appName.toStdString();
     645              : 
     646            1 :     progressUi.message = version.isEmpty()
     647            4 :                          ? muse::qtrc("update", "Installing %1").arg(appName).toStdString()
     648            3 :                          : muse::qtrc("update", "Installing %1 %2").arg(appName, version).toStdString();
     649              : 
     650            1 :     const muse::ui::ThemeInfo& theme = uiConfiguration()->currentTheme();
     651              : 
     652            3 :     auto themeColor = [&theme](muse::ui::ThemeStyleKey key) {
     653            3 :         const QColor color = theme.values.value(key).value<QColor>();
     654            3 :         return color.isValid() ? color.name(QColor::HexRgb).toStdString() : std::string();
     655            1 :     };
     656              : 
     657            1 :     progressUi.backgroundColor = themeColor(muse::ui::BACKGROUND_PRIMARY_COLOR);
     658            1 :     progressUi.accentColor = themeColor(muse::ui::ACCENT_COLOR);
     659            1 :     progressUi.textColor = themeColor(muse::ui::FONT_PRIMARY_COLOR);
     660              : 
     661            1 :     return progressUi;
     662            1 : }
     663              : 
     664            7 : void AppUpdateService::downloadPreviousReleasesNotes(const Version& updateVersion, const PrevReleaseNotesCallback& finished)
     665              : {
     666            7 :     QUrl url = QString::fromStdString(configuration()->previousAppReleasesNotesUrl());
     667            7 :     auto buff = std::make_shared<QBuffer>();
     668            7 :     RetVal<Progress> progress = m_networkManager->get(url, buff);
     669              : 
     670            7 :     if (!progress.ret) {
     671            0 :         LOGE() << "Failed to get previous release notes: " << progress.ret.toString();
     672            0 :         finished({});
     673            0 :         return;
     674              :     }
     675              : 
     676            7 :     progress.val.finished().onReceive(this, [this, updateVersion, buff, finished](const ProgressResult& res) {
     677            7 :         PrevReleasesNotesList result;
     678              : 
     679            7 :         DEFER {
     680            7 :             finished(result);
     681            7 :             return;
     682            7 :         };
     683              : 
     684            7 :         if (!res.ret) {
     685            0 :             LOGE() << "Failed to get previous release notes: " << res.ret.toString();
     686            0 :             return;
     687              :         }
     688              : 
     689            7 :         QByteArray json = buff->data();
     690            7 :         PrevReleasesNotesList previousReleasesNotes = parsePreviousReleasesNotes(json);
     691            7 :         if (previousReleasesNotes.empty()) {
     692            0 :             return;
     693              :         }
     694              : 
     695            7 :         Version currentVersion = Version(application()->fullVersion());
     696              : 
     697           35 :         for (const PrevReleaseNotes& releaseNotes : previousReleasesNotes) {
     698           28 :             Version previousVersion = Version(releaseNotes.version);
     699           28 :             if (updateVersion == previousVersion) {
     700            0 :                 continue;
     701              :             }
     702              : 
     703           28 :             if (currentVersion < previousVersion) {
     704           14 :                 String notesStr = String::fromStdString(releaseNotes.notes);
     705           14 :                 if (notesStr.startsWith(u"###")) {
     706              :                     //! Release notes may be in the format of: ### MuseScore x.y.z is now available!\r\n...notes...\r\n
     707              :                     //! We need to remove the title of the release notes to get the actual notes.
     708            0 :                     static const std::regex titleRegex(R"(^###.*?\r\n)");
     709            0 :                     std::string notesWithoutTitle = notesStr.remove(titleRegex).toStdString();
     710            0 :                     result.emplace_back(releaseNotes.version, notesWithoutTitle);
     711            0 :                 } else {
     712           14 :                     result.emplace_back(releaseNotes.version, releaseNotes.notes);
     713              :                 }
     714           14 :             }
     715           28 :         }
     716              : 
     717            7 :         std::sort(result.begin(), result.end(), [](const PrevReleaseNotes& a,
     718              :                                                    const PrevReleaseNotes& b) {
     719           14 :             return Version(a.version) < Version(b.version);
     720              :         });
     721            7 :     });
     722            7 : }
     723              : 
     724            7 : void AppUpdateService::clear()
     725              : {
     726            7 :     m_lastCheckResult = RetVal<ReleaseInfo>::make_ok(ReleaseInfo());
     727            7 : }
     728              : 
     729           12 : void AppUpdateService::cleanupStalePackages(const std::string& keepFileName)
     730              : {
     731           12 :     const io::path_t recorded = configuration()->lastDownloadedPackagePath();
     732           12 :     if (!recorded.empty() && io::filename(recorded).toStdString() != keepFileName) {
     733            1 :         fileSystem()->remove(recorded);
     734            1 :         fileSystem()->remove(recorded + PARTIAL_SUFFIX);
     735            1 :         configuration()->setLastDownloadedPackagePath(io::path_t());
     736              :     }
     737              : 
     738           12 :     if (io::filename(m_preparedPackagePath).toStdString() != keepFileName) {
     739            8 :         resetPreparedUpdate();
     740              :     }
     741              : 
     742           12 :     const io::path_t dir = configuration()->updateDataPath();
     743           12 :     if (!fileSystem()->exists(dir)) {
     744           10 :         return;
     745              :     }
     746              : 
     747            2 :     RetVal<io::paths_t> entries = fileSystem()->scanFiles(dir, {}, io::ScanMode::FilesAndFoldersInCurrentDir);
     748            2 :     if (!entries.ret) {
     749            0 :         return;
     750              :     }
     751              : 
     752              :     //! NOTE: Keep both the finished package and its in-progress ".part" file so an
     753              :     //! interrupted download of the current release can still be resumed.
     754            2 :     const std::string keepPartial = keepFileName + PARTIAL_SUFFIX;
     755              : 
     756              :     //! NOTE: Also keep the update prepared for the kept package
     757            2 :     const std::string prepared = m_preparedPath.toStdString();
     758            5 :     auto isPrepared = [&prepared](const io::path_t& entry) {
     759            5 :         const std::string path = entry.toStdString();
     760           10 :         return !prepared.empty() && (prepared == path || prepared.rfind(path + "/", 0) == 0);
     761            5 :     };
     762              : 
     763            8 :     for (const io::path_t& entry : entries.val) {
     764            6 :         const std::string name = io::filename(entry).toStdString();
     765            6 :         if (name != keepFileName && name != keepPartial && !isPrepared(entry)) {
     766            4 :             fileSystem()->remove(entry);
     767              :         }
     768            6 :     }
     769           22 : }
     770              : 
     771            7 : bool AppUpdateService::isReleaseReadyToInstall() const
     772              : {
     773            7 :     if (downloadedReleasePath().empty()) {
     774            0 :         return false;
     775              :     }
     776              : 
     777              :     //! NOTE: In-place install also needs the downloaded release to be prepared
     778            7 :     return !canAutoInstall() || !preparedUpdatePath().empty();
     779              : }
     780              : 
     781           11 : io::path_t AppUpdateService::downloadedReleasePath() const
     782              : {
     783           11 :     if (!m_lastCheckResult.ret) {
     784            0 :         return {};
     785              :     }
     786              : 
     787           11 :     const std::string& fileName = m_lastCheckResult.val.fileName;
     788           11 :     if (fileName.empty()) {
     789            0 :         return {};
     790              :     }
     791              : 
     792           11 :     const io::path_t path = packagesDir() + "/" + fileName;
     793           11 :     if (!fileSystem()->exists(path)) {
     794            0 :         return {};
     795              :     }
     796              : 
     797           11 :     return path;
     798           11 : }
     799              : 
     800            3 : void AppUpdateService::removeDownloadedRelease()
     801              : {
     802            3 :     if (m_downloadInProgress) {
     803            2 :         m_updateProgress.cancel();
     804              :     }
     805              : 
     806            3 :     cleanupStalePackages(/*keepFileName*/ std::string());
     807            3 : }
     808              : 
     809           34 : io::path_t AppUpdateService::packagesDir() const
     810              : {
     811           34 :     return canAutoInstall() ? configuration()->updateDataPath() : configuration()->downloadsPath();
     812              : }
        

Generated by: LCOV version 2.0-1