LCOV - code coverage report
Current view: top level - muse/framework/update/internal/platform/linux - linuxupdateinstaller.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 0.0 % 79 0
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) 2026 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 "linuxupdateinstaller.h"
      23              : 
      24              : #include <cstring>
      25              : #include <unistd.h>
      26              : 
      27              : #include <QByteArray>
      28              : #include <QCoreApplication>
      29              : #include <QFile>
      30              : #include <QFileInfo>
      31              : #include <QProcess>
      32              : #include <QStringList>
      33              : 
      34              : #include "../../../updateerrors.h"
      35              : 
      36              : #include "log.h"
      37              : 
      38              : using namespace muse;
      39              : using namespace muse::update;
      40              : 
      41              : static const QString HELPER_NAME("museupdater");
      42              : 
      43              : namespace {
      44              : //! An AppImage is an ELF executable with a marker in the bytes the ELF header
      45              : //! reserves for the OS ABI: "AI" followed by the AppImage format version.
      46              : //! Only type 2 is produced nowadays, and it is what we publish.
      47              : constexpr int APPIMAGE_HEADER_SIZE = 11;
      48              : constexpr char APPIMAGE_TYPE_2 = 0x02;
      49              : 
      50            0 : bool isAppImageFile(const QString& path)
      51              : {
      52            0 :     QFile file(path);
      53            0 :     if (!file.open(QIODevice::ReadOnly)) {
      54            0 :         LOGE() << "failed to open for reading: " << path;
      55            0 :         return false;
      56              :     }
      57              : 
      58            0 :     char header[APPIMAGE_HEADER_SIZE] = { 0 };
      59            0 :     if (file.read(header, APPIMAGE_HEADER_SIZE) != APPIMAGE_HEADER_SIZE) {
      60            0 :         LOGE() << "file is too small to be an AppImage: " << path;
      61            0 :         return false;
      62              :     }
      63              : 
      64              :     static const char ELF_MAGIC[] = { 0x7f, 'E', 'L', 'F' };
      65            0 :     if (std::memcmp(header, ELF_MAGIC, sizeof(ELF_MAGIC)) != 0) {
      66            0 :         LOGE() << "not an ELF file: " << path;
      67            0 :         return false;
      68              :     }
      69              : 
      70            0 :     if (header[8] != 'A' || header[9] != 'I' || header[10] != APPIMAGE_TYPE_2) {
      71            0 :         LOGE() << "not a type 2 AppImage: " << path;
      72            0 :         return false;
      73              :     }
      74              : 
      75            0 :     return true;
      76            0 : }
      77              : 
      78            0 : bool isWritable(const QString& path)
      79              : {
      80            0 :     return ::access(path.toUtf8().constData(), W_OK) == 0;
      81              : }
      82              : }
      83              : 
      84            0 : io::path_t LinuxUpdateInstaller::currentAppImagePath() const
      85              : {
      86              :     //! NOTE: Set by the AppImage runtime. Absent for every other way of running
      87              :     //! the application, none of which can be updated by replacing one file.
      88            0 :     const QByteArray appImage = qgetenv("APPIMAGE");
      89            0 :     if (appImage.isEmpty()) {
      90            0 :         return {};
      91              :     }
      92              : 
      93            0 :     const QFileInfo info(QFile::decodeName(appImage));
      94            0 :     if (!info.exists() || !info.isFile()) {
      95            0 :         return {};
      96              :     }
      97              : 
      98            0 :     return io::path_t(info.canonicalFilePath());
      99            0 : }
     100              : 
     101            0 : io::path_t LinuxUpdateInstaller::helperPath() const
     102              : {
     103            0 :     return io::path_t(QCoreApplication::applicationDirPath() + "/" + HELPER_NAME);
     104              : }
     105              : 
     106            0 : bool LinuxUpdateInstaller::isInPlaceUpdateSupported() const
     107              : {
     108            0 :     const QString appImage = currentAppImagePath().toQString();
     109            0 :     if (appImage.isEmpty()) {
     110            0 :         return false;
     111              :     }
     112              : 
     113            0 :     if (!QFileInfo::exists(helperPath().toQString())) {
     114            0 :         return false;
     115              :     }
     116              : 
     117              :     //! NOTE: Replacing the AppImage creates a new directory entry, so the
     118              :     //! containing directory has to be writable too - a writable file inside a
     119              :     //! read-only directory (a system-wide install) is not enough.
     120            0 :     if (!isWritable(appImage) || !isWritable(QFileInfo(appImage).absolutePath())) {
     121            0 :         return false;
     122              :     }
     123              : 
     124            0 :     return true;
     125            0 : }
     126              : 
     127            0 : RetVal<muse::io::path_t> LinuxUpdateInstaller::prepareUpdate(const muse::io::path_t& packagePath)
     128              : {
     129            0 :     const QString package = packagePath.toQString();
     130            0 :     if (!QFileInfo::exists(package)) {
     131            0 :         LOGE() << "update package does not exist: " << package;
     132            0 :         return RetVal<muse::io::path_t>(make_ret(Err::UnknownError));
     133              :     }
     134              : 
     135              :     // 1. The downloaded package replaces the running file as-is, so make sure it
     136              :     //    really is an AppImage before letting the helper move it into place.
     137            0 :     if (!isAppImageFile(package)) {
     138            0 :         LOGE() << "update package is not a valid AppImage: " << package;
     139            0 :         return RetVal<muse::io::path_t>(make_ret(Err::UnknownError));
     140              :     }
     141              : 
     142              :     // 2. The download has no executable bit; the swapped-in file must be
     143              :     //    runnable.
     144            0 :     const QFile::Permissions permissions
     145              :         = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner
     146              :           | QFile::ReadGroup | QFile::ExeGroup
     147              :           | QFile::ReadOther | QFile::ExeOther;
     148              : 
     149            0 :     if (!QFile::setPermissions(package, permissions)) {
     150            0 :         LOGE() << "failed to make update package executable: " << package;
     151            0 :         return RetVal<muse::io::path_t>(make_ret(Err::UnknownError));
     152              :     }
     153              : 
     154            0 :     return RetVal<muse::io::path_t>::make_ok(packagePath);
     155            0 : }
     156              : 
     157            0 : Ret LinuxUpdateInstaller::finalizeUpdate(const muse::io::path_t& preparedPath, const InstallProgressUi&)
     158              : {
     159            0 :     const QString package = preparedPath.toQString();
     160            0 :     if (!QFileInfo::exists(package)) {
     161            0 :         LOGE() << "prepared update does not exist: " << package;
     162            0 :         return make_ret(Err::UnknownError);
     163              :     }
     164              : 
     165            0 :     const QString appImagePath = currentAppImagePath().toQString();
     166            0 :     if (appImagePath.isEmpty()) {
     167            0 :         LOGE() << "not running from an AppImage, cannot update in place";
     168            0 :         return make_ret(Ret::Code::NotSupported);
     169              :     }
     170              : 
     171            0 :     const QFile::Permissions permissions
     172              :         = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner
     173              :           | QFile::ReadGroup | QFile::ExeGroup
     174              :           | QFile::ReadOther | QFile::ExeOther;
     175              : 
     176              :     // 1. Copy the helper out of the AppImage. Its mount point disappears as soon
     177              :     //    as this process exits, which is exactly when the helper starts working.
     178            0 :     fileSystem()->makePath(configuration()->updateDataPath());
     179              : 
     180            0 :     const QString helperRun = configuration()->updateDataPath().toQString() + "/" + HELPER_NAME;
     181            0 :     QFile::remove(helperRun);
     182            0 :     if (!QFile::copy(helperPath().toQString(), helperRun)) {
     183            0 :         LOGE() << "failed to copy helper to " << helperRun;
     184            0 :         return make_ret(Err::UnknownError);
     185              :     }
     186            0 :     QFile::setPermissions(helperRun, permissions);
     187              : 
     188              :     // 2. Spawn the detached helper. It waits for us to quit, replaces the
     189              :     //    AppImage and relaunches it.
     190            0 :     const QString logPath = configuration()->helperLogPath().toQString();
     191              :     const QStringList args = {
     192              :         "--wait-pid", QString::number(QCoreApplication::applicationPid()),
     193              :         "--src", package,
     194              :         "--dst", appImagePath,
     195              :         "--relaunch", appImagePath,
     196              :         "--log", logPath
     197            0 :     };
     198              : 
     199            0 :     if (!QProcess::startDetached(helperRun, args)) {
     200            0 :         LOGE() << "failed to start update helper";
     201            0 :         return make_ret(Err::UnknownError);
     202              :     }
     203              : 
     204            0 :     LOGI() << "update helper started, will replace " << appImagePath << " after quit";
     205            0 :     return make_ok();
     206            0 : }
        

Generated by: LCOV version 2.0-1