summaryrefslogtreecommitdiffstats
path: root/src/manager-lib/installationtask.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/manager-lib/installationtask.cpp')
-rw-r--r--src/manager-lib/installationtask.cpp484
1 files changed, 484 insertions, 0 deletions
diff --git a/src/manager-lib/installationtask.cpp b/src/manager-lib/installationtask.cpp
new file mode 100644
index 00000000..ad017286
--- /dev/null
+++ b/src/manager-lib/installationtask.cpp
@@ -0,0 +1,484 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 Luxoft Sweden AB
+** Copyright (C) 2018 Pelagicore AG
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Luxoft Application Manager.
+**
+** $QT_BEGIN_LICENSE:LGPL-QTAS$
+** Commercial License Usage
+** Licensees holding valid commercial Qt Automotive Suite licenses may use
+** this file in accordance with the commercial license agreement provided
+** with the Software or, alternatively, in accordance with the terms
+** contained in a written agreement between you and The Qt Company. For
+** licensing terms and conditions see https://www.qt.io/terms-conditions.
+** For further information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+** SPDX-License-Identifier: LGPL-3.0
+**
+****************************************************************************/
+
+#include <QTemporaryDir>
+#include <QMessageAuthenticationCode>
+
+#include "logging.h"
+#include "packagemanager_p.h"
+#include "packageinfo.h"
+#include "packageextractor.h"
+#include "yamlpackagescanner.h"
+#include "exception.h"
+#include "packagemanager.h"
+#include "sudo.h"
+#include "utilities.h"
+#include "signature.h"
+#include "sudo.h"
+#include "installationtask.h"
+
+/*
+ Overview of what happens on an installation of an app with <id> to <location>:
+
+ Step 1 -- startInstallation()
+ =============================
+
+ delete <location>/<id>+
+
+ create dir <location>/<id>+
+ set <extractiondir> to <location>/<id>+
+
+
+ Step 2 -- unpack files
+ ======================
+
+ PackageExtractor does its job
+
+
+ Step 3 -- finishInstallation()
+ ================================
+
+ if (exists <location>/<id>)
+ set <isupdate> to <true>
+
+ create installation report at <extractiondir>/.installation-report.yaml
+
+ if (not <isupdate>)
+ create document directory
+
+ if (optional uid separation)
+ chown/chmod recursively in <extractiondir> and document directory
+
+
+ Step 3.1 -- final rename in finishInstallation()
+ ==================================================
+
+ if (<isupdate>)
+ rename <location>/<id> to <location>/<id>-
+ rename <location>/<id>+ to <location>/<id>
+*/
+
+QT_BEGIN_NAMESPACE_AM
+
+
+
+// The standard QTemporaryDir destructor cannot cope with read-only sub-directories.
+class TemporaryDir : public QTemporaryDir
+{
+public:
+ TemporaryDir()
+ : QTemporaryDir()
+ { }
+ explicit TemporaryDir(const QString &templateName)
+ : QTemporaryDir(templateName)
+ { }
+ ~TemporaryDir()
+ {
+ recursiveOperation(path(), safeRemove);
+ }
+private:
+ Q_DISABLE_COPY(TemporaryDir)
+};
+
+
+QMutex InstallationTask::s_serializeFinishInstallation { };
+
+InstallationTask::InstallationTask(const QString &installationPath, const QString &documentPath,
+ const QUrl &sourceUrl, QObject *parent)
+ : AsynchronousTask(parent)
+ , m_pm(PackageManager::instance())
+ , m_installationPath(installationPath)
+ , m_documentPath(documentPath)
+ , m_sourceUrl(sourceUrl)
+{ }
+
+InstallationTask::~InstallationTask()
+{ }
+
+bool InstallationTask::cancel()
+{
+ QMutexLocker locker(&m_mutex);
+
+ // we cannot cancel anymore after finishInstallation() has been called
+ if (m_installationAcknowledged)
+ return false;
+
+ m_canceled = true;
+ if (m_extractor)
+ m_extractor->cancel();
+ m_installationAcknowledgeWaitCondition.wakeAll();
+ return true;
+}
+
+void InstallationTask::acknowledge()
+{
+ QMutexLocker locker(&m_mutex);
+
+ if (m_canceled)
+ return;
+
+ m_installationAcknowledged = true;
+ m_installationAcknowledgeWaitCondition.wakeAll();
+}
+
+void InstallationTask::execute()
+{
+ try {
+ if (m_installationPath.isEmpty())
+ throw Exception("no installation location was configured");
+
+ TemporaryDir extractionDir;
+ if (!extractionDir.isValid())
+ throw Exception("could not create a temporary extraction directory");
+
+ // protect m_canceled and changes to m_extractor
+ QMutexLocker locker(&m_mutex);
+ if (m_canceled)
+ throw Exception(Error::Canceled, "canceled");
+
+ m_extractor = new PackageExtractor(m_sourceUrl, QDir(extractionDir.path()));
+ locker.unlock();
+
+ connect(m_extractor, &PackageExtractor::progress, this, &AsynchronousTask::progress);
+
+ m_extractor->setFileExtractedCallback(std::bind(&InstallationTask::checkExtractedFile,
+ this, std::placeholders::_1));
+
+ if (!m_extractor->extract())
+ throw Exception(m_extractor->errorCode(), m_extractor->errorString());
+
+ if (!m_foundInfo || !m_foundIcon)
+ throw Exception(Error::Package, "package did not contain a valid info.json and icon file");
+
+ QList<QByteArray> chainOfTrust = m_pm->caCertificates();
+
+ if (!m_pm->allowInstallationOfUnsignedPackages()) {
+ if (!m_extractor->installationReport().storeSignature().isEmpty()) {
+ // normal package from the store
+ QByteArray sigDigest = m_extractor->installationReport().digest();
+ bool sigOk = false;
+
+ if (Signature(sigDigest).verify(m_extractor->installationReport().storeSignature(), chainOfTrust)) {
+ sigOk = true;
+ } else if (!m_pm->hardwareId().isEmpty()) {
+ // did not verify - if we have a hardware-id, try to verify with it
+ sigDigest = QMessageAuthenticationCode::hash(sigDigest, m_pm->hardwareId().toUtf8(), QCryptographicHash::Sha256);
+ if (Signature(sigDigest).verify(m_extractor->installationReport().storeSignature(), chainOfTrust))
+ sigOk = true;
+ }
+ if (!sigOk)
+ throw Exception(Error::Package, "could not verify the package's store signature");
+ } else if (!m_extractor->installationReport().developerSignature().isEmpty()) {
+ // developer package - needs a device in dev mode
+ if (!m_pm->developmentMode())
+ throw Exception(Error::Package, "cannot install development packages on consumer devices");
+
+ if (!Signature(m_extractor->installationReport().digest()).verify(m_extractor->installationReport().developerSignature(), chainOfTrust))
+ throw Exception(Error::Package, "could not verify the package's developer signature");
+
+ } else {
+ throw Exception(Error::Package, "cannot install unsigned packages");
+ }
+ }
+
+ emit finishedPackageExtraction();
+ setState(AwaitingAcknowledge);
+
+ // now wait in a wait-condition until we get an acknowledge or we get canceled
+ locker.relock();
+ while (!m_canceled && !m_installationAcknowledged)
+ m_installationAcknowledgeWaitCondition.wait(&m_mutex);
+
+ // this is the last cancellation point
+ if (m_canceled)
+ throw Exception(Error::Canceled, "canceled");
+ locker.unlock();
+
+ setState(Installing);
+
+ // However many downloads are allowed to happen in parallel: we need to serialize those
+ // tasks here for the finishInstallation() step
+ QMutexLocker finishLocker(&s_serializeFinishInstallation);
+
+ finishInstallation();
+
+ // At this point, the installation is done, so we cannot throw anymore.
+
+ // we need to call those PackageManager methods in the correct thread
+ bool finishOk = false;
+ QMetaObject::invokeMethod(PackageManager::instance(), [this, &finishOk]()
+ { finishOk = PackageManager::instance()->finishedPackageInstall(m_packageId); },
+ Qt::BlockingQueuedConnection);
+
+ if (!finishOk)
+ qCWarning(LogInstaller) << "PackageManager rejected the installation of " << m_packageId;
+
+ } catch (const Exception &e) {
+ setError(e.errorCode(), e.errorString());
+
+ if (m_managerApproval) {
+ // we need to call those ApplicationManager methods in the correct thread
+ bool cancelOk = false;
+ QMetaObject::invokeMethod(PackageManager::instance(), [this, &cancelOk]()
+ { cancelOk = PackageManager::instance()->canceledPackageInstall(m_packageId); },
+ Qt::BlockingQueuedConnection);
+
+ if (!cancelOk)
+ qCWarning(LogInstaller) << "PackageManager could not remove package" << m_packageId << "after a failed installation";
+ }
+ }
+
+
+ {
+ QMutexLocker locker(&m_mutex);
+ delete m_extractor;
+ m_extractor = nullptr;
+ }
+}
+
+
+void InstallationTask::checkExtractedFile(const QString &file) Q_DECL_NOEXCEPT_EXPR(false)
+{
+ ++m_extractedFileCount;
+
+ if (m_extractedFileCount == 1) {
+ if (file != qL1S("info.yaml"))
+ throw Exception(Error::Package, "info.yaml must be the first file in the package. Got %1")
+ .arg(file);
+
+ YamlPackageScanner yps;
+ m_package.reset(yps.scan(m_extractor->destinationDirectory().absoluteFilePath(file)));
+ if (m_package->id() != m_extractor->installationReport().packageId())
+ throw Exception(Error::Package, "the package identifiers in --PACKAGE-HEADER--' and info.yaml do not match");
+
+ m_iconFileName = m_package->icon(); // store it separately as we will give away ApplicationInfo later on
+
+ if (m_iconFileName.isEmpty())
+ throw Exception(Error::Package, "the 'icon' field in info.yaml cannot be empty or absent.");
+
+ m_packageId = m_package->id();
+
+ m_foundInfo = true;
+ } else if (m_extractedFileCount == 2) {
+ // the second file must be the icon
+
+ Q_ASSERT(m_foundInfo);
+ Q_ASSERT(!m_foundIcon);
+
+ if (file != m_iconFileName)
+ throw Exception(Error::Package,
+ "The package icon (as stated in info.yaml) must be the second file in the package."
+ " Expected '%1', got '%2'").arg(m_iconFileName, file);
+
+ QFile icon(m_extractor->destinationDirectory().absoluteFilePath(file));
+
+ if (icon.size() > 256*1024)
+ throw Exception(Error::Package, "the size of %1 is too large (max. 256KB)").arg(file);
+
+ m_foundIcon = true;
+ } else {
+ throw Exception(Error::Package, "Could not find info.yaml and the icon file at the beginning of the package.");
+ }
+
+ if (m_foundIcon && m_foundInfo) {
+ qCDebug(LogInstaller) << "emit taskRequestingInstallationAcknowledge" << id() << "for package" << m_package->id();
+
+ QVariantMap nameMap;
+ auto names = m_package->names();
+ for (auto it = names.constBegin(); it != names.constEnd(); ++it)
+ nameMap.insert(it.key(), it.value());
+
+ QVariantMap applicationData {
+ { qSL("id"), m_package->id() },
+ { qSL("version"), m_package->version() },
+ { qSL("icon"), m_package->icon() },
+ { qSL("displayIcon"), m_package->icon() }, // legacy
+ { qSL("name"), nameMap },
+ { qSL("displayName"), nameMap }, // legacy
+ { qSL("baseDir"), m_package->baseDir().absolutePath() },
+ { qSL("codeDir"), m_package->baseDir().absolutePath() }, // 5.12 backward compatibility
+ { qSL("manifestDir"), m_package->baseDir().absolutePath() }, // 5.12 backward compatibility
+ { qSL("installationLocationId"), qSL("internal-0") } // 5.13 backward compatibility
+ };
+ emit m_pm->taskRequestingInstallationAcknowledge(id(), applicationData,
+ m_extractor->installationReport().extraMetaData(),
+ m_extractor->installationReport().extraSignedMetaData());
+
+ QDir oldDestinationDirectory = m_extractor->destinationDirectory();
+
+ startInstallation();
+
+ QFile::copy(oldDestinationDirectory.filePath(qSL("info.yaml")), m_extractionDir.filePath(qSL("info.yaml")));
+ QFile::copy(oldDestinationDirectory.filePath(m_iconFileName), m_extractionDir.filePath(m_iconFileName));
+
+ {
+ QMutexLocker locker(&m_mutex);
+ m_extractor->setDestinationDirectory(m_extractionDir);
+
+ QString path = m_extractionDir.absolutePath();
+ path.chop(1); // remove the '+'
+ m_package->setBaseDir(QDir(path));
+ }
+ // we need to find a free uid before we call startingApplicationInstallation
+ m_package->m_uid = m_pm->findUnusedUserId();
+ m_applicationUid = m_package->m_uid;
+
+ // we need to call those ApplicationManager methods in the correct thread
+ // this will also exclusively lock the application for us
+ // m_package ownership is transferred to the ApplicationManager
+ QString packageId = m_package->id(); // m_package is gone after the invoke
+ QMetaObject::invokeMethod(PackageManager::instance(), [this]()
+ { m_managerApproval = PackageManager::instance()->startingPackageInstallation(m_package.take()); },
+ Qt::BlockingQueuedConnection);
+
+ if (!m_managerApproval)
+ throw Exception("PackageManager declined the installation of %1").arg(packageId);
+
+ // we're not interested in any other files from here on...
+ m_extractor->setFileExtractedCallback(nullptr);
+ }
+}
+
+void InstallationTask::startInstallation() Q_DECL_NOEXCEPT_EXPR(false)
+{
+ // 2. delete old, partial installation
+
+ QDir installationDir = QString(m_installationPath + qL1C('/'));
+ QString installationTarget = m_packageId + qL1C('+');
+ if (installationDir.exists(installationTarget)) {
+ if (!removeRecursiveHelper(installationDir.absoluteFilePath(installationTarget)))
+ throw Exception("could not remove old, partial installation %1/%2").arg(installationDir).arg(installationTarget);
+ }
+
+ // 4. create new installation
+ if (!m_installationDirCreator.create(installationDir.absoluteFilePath(installationTarget)))
+ throw Exception("could not create installation directory %1/%2").arg(installationDir).arg(installationTarget);
+ m_extractionDir = installationDir;
+ if (!m_extractionDir.cd(installationTarget))
+ throw Exception("could not cd into installation directory %1/%2").arg(installationDir).arg(installationTarget);
+ m_applicationDir.setPath(installationDir.absoluteFilePath(m_packageId));
+}
+
+void InstallationTask::finishInstallation() Q_DECL_NOEXCEPT_EXPR(false)
+{
+ QDir documentDirectory(m_documentPath);
+ ScopedDirectoryCreator documentDirCreator;
+
+ enum { Installation, Update } mode = Installation;
+
+ if (m_applicationDir.exists())
+ mode = Update;
+
+ // create the installation report
+ InstallationReport report = m_extractor->installationReport();
+
+ QFile reportFile(m_extractionDir.absoluteFilePath(qSL(".installation-report.yaml")));
+ if (!reportFile.open(QFile::WriteOnly) || !report.serialize(&reportFile))
+ throw Exception(reportFile, "could not write the installation report");
+ reportFile.close();
+
+ // create the document directories when installing (not needed on updates)
+ if (mode == Installation) {
+ // this package may have been installed earlier and the document directory may not have been removed
+ if (!documentDirectory.cd(m_packageId)) {
+ if (!documentDirCreator.create(documentDirectory.absoluteFilePath(m_packageId)))
+ throw Exception(Error::IO, "could not create the document directory %1").arg(documentDirectory.filePath(m_packageId));
+ }
+ }
+#ifdef Q_OS_UNIX
+ // update the owner, group and permission bits on both the installation and document directories
+ SudoClient *root = SudoClient::instance();
+
+ if (m_pm->isApplicationUserIdSeparationEnabled() && root) {
+ uid_t uid = m_applicationUid;
+ gid_t gid = m_pm->commonApplicationGroupId();
+
+ if (!root->setOwnerAndPermissionsRecursive(documentDirectory.filePath(m_packageId), uid, gid, 02700)) {
+ throw Exception(Error::IO, "could not recursively change the owner to %1:%2 and the permission bits to %3 in %4")
+ .arg(uid).arg(gid).arg(02700, 0, 8).arg(documentDirectory.filePath(m_packageId));
+ }
+
+ if (!root->setOwnerAndPermissionsRecursive(m_extractionDir.path(), uid, gid, 0440)) {
+ throw Exception(Error::IO, "could not recursively change the owner to %1:%2 and the permission bits to %3 in %4")
+ .arg(uid).arg(gid).arg(0440, 0, 8).arg(m_extractionDir.absolutePath());
+ }
+ }
+#endif
+
+ // final rename
+
+ // POSIX cannot atomically rename directories, if the destination directory exists
+ // and is non-empty. We need to do a double-rename in this case, which might fail!
+ // The image is a file, so this limitation does not apply!
+
+ ScopedRenamer renameApplication;
+
+ if (mode == Update) {
+ if (!renameApplication.rename(m_applicationDir, ScopedRenamer::NamePlusToName | ScopedRenamer::NameToNameMinus))
+ throw Exception(Error::IO, "could not rename application directory %1+ to %1 (including a backup to %1-)").arg(m_applicationDir);
+ } else {
+ if (!renameApplication.rename(m_applicationDir, ScopedRenamer::NamePlusToName))
+ throw Exception(Error::IO, "could not rename application directory %1+ to %1").arg(m_applicationDir);
+ }
+
+ // from this point onwards, we are not allowed to throw anymore, since the installation is "done"
+
+ setState(CleaningUp);
+
+ renameApplication.take();
+ documentDirCreator.take();
+
+ m_installationDirCreator.take();
+
+ // this should not be necessary, but it also won't hurt
+ if (mode == Update)
+ removeRecursiveHelper(m_applicationDir.absolutePath() + qL1C('-'));
+
+#ifdef Q_OS_UNIX
+ // write files to the filesystem
+ sync();
+#endif
+
+ m_errorString.clear();
+}
+
+QT_END_NAMESPACE_AM