diff options
author | Robert Griebl <robert.griebl@pelagicore.com> | 2019-04-03 00:48:22 +0200 |
---|---|---|
committer | Robert Griebl <robert.griebl@pelagicore.com> | 2019-08-01 11:23:31 +0200 |
commit | b4aee167d3bc6b9f64229317fbc428b3f3b83c0d (patch) | |
tree | 6b526d57203f34f4b85a82e2e958ace227c83960 /src/installer-lib/packagemanager.cpp | |
parent | 3bc3dc4c8e912beb18aec7ab84af40c0129d84c0 (diff) |
Add new package abstraction, which allows multiple executables per package
This is part 1 which is missing doc updates and missing the update-builtin-
applications functionality. Both will be added in a follow-up commit.
Change-Id: I2b493cfb7585143962067674690b02cc132ef78b
Reviewed-by: Dominik Holland <dominik.holland@pelagicore.com>
Diffstat (limited to 'src/installer-lib/packagemanager.cpp')
-rw-r--r-- | src/installer-lib/packagemanager.cpp | 1243 |
1 files changed, 1243 insertions, 0 deletions
diff --git a/src/installer-lib/packagemanager.cpp b/src/installer-lib/packagemanager.cpp new file mode 100644 index 00000000..4b2e09b9 --- /dev/null +++ b/src/installer-lib/packagemanager.cpp @@ -0,0 +1,1243 @@ +/**************************************************************************** +** +** 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 <QMetaMethod> +#include <QQmlEngine> +#include <QVersionNumber> +#include "packagemanager.h" +#include "packagedatabase.h" +#include "packagemanager_p.h" +#include "package.h" +#include "logging.h" +#include "installationreport.h" +#include "exception.h" +#include "sudo.h" +#include "utilities.h" + +QT_BEGIN_NAMESPACE_AM + +enum Roles +{ + Id = Qt::UserRole, + Name, + Description, + Icon, + + IsBlocked, + IsUpdating, + IsRemovable, + + UpdateProgress, + + Version, + PackageItem, +}; + +PackageManager *PackageManager::s_instance = nullptr; +QHash<int, QByteArray> PackageManager::s_roleNames; + +PackageManager *PackageManager::createInstance(PackageDatabase *packageDatabase, + const QVector<InstallationLocation> &installationLocations) +{ + if (Q_UNLIKELY(s_instance)) + qFatal("PackageManager::createInstance() was called a second time."); + + Q_ASSERT(packageDatabase); + + QScopedPointer<PackageManager> pm(new PackageManager(packageDatabase, installationLocations)); + registerQmlTypes(); + + // map all the built-in packages first + const auto builtinPackages = packageDatabase->builtInPackages(); + for (auto packageInfo : builtinPackages) { + auto *package = new Package(packageInfo); + QQmlEngine::setObjectOwnership(package, QQmlEngine::CppOwnership); + pm->d->packages << package; + } + + // next, map all the installed packages, making sure to detect updates to built-in ones + const auto installedPackages = packageDatabase->installedPackages(); + for (auto packageInfo : installedPackages) { + Package *builtInPackage = pm->fromId(packageInfo->id()); + + if (builtInPackage) { // update + if (builtInPackage->updatedInfo()) { // but there already is an update applied!? + throw Exception(Error::Package, "Found more than one update for the built-in package '%1'") + .arg(builtInPackage->id()); + //TODO: can we get the paths to both info.yaml here? + } + builtInPackage->setUpdatedInfo(packageInfo); + } else { + auto *package = new Package(packageInfo); + QQmlEngine::setObjectOwnership(package, QQmlEngine::CppOwnership); + pm->d->packages << package; + } + } + + return s_instance = pm.take(); +} + +PackageManager *PackageManager::instance() +{ + if (!s_instance) + qFatal("PackageManager::instance() was called before createInstance()."); + return s_instance; +} + +QObject *PackageManager::instanceForQml(QQmlEngine *, QJSEngine *) +{ + QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); + return instance(); +} + +QVector<Package *> PackageManager::packages() const +{ + return d->packages; +} + +void PackageManager::registerQmlTypes() +{ + qmlRegisterSingletonType<PackageManager>("QtApplicationManager.SystemUI", 2, 0, "PackageManager", + &PackageManager::instanceForQml); + qmlRegisterUncreatableType<Package>("QtApplicationManager.SystemUI", 2, 0, "PackageObject", + qSL("Cannot create objects of type PackageObject")); + qRegisterMetaType<Package *>("Package*"); + + s_roleNames.insert(Id, "packageId"); + s_roleNames.insert(Name, "name"); + s_roleNames.insert(Description, "description"); + s_roleNames.insert(Icon, "icon"); + s_roleNames.insert(IsBlocked, "isBlocked"); + s_roleNames.insert(IsUpdating, "isUpdating"); + s_roleNames.insert(IsRemovable, "isRemovable"); + s_roleNames.insert(UpdateProgress, "updateProgress"); + s_roleNames.insert(Version, "version"); + s_roleNames.insert(PackageItem, "package"); +} + +PackageManager::PackageManager(PackageDatabase *packageDatabase, + const QVector<InstallationLocation> &installationLocations) + : QAbstractListModel() + , d(new PackageManagerPrivate()) +{ + d->database = packageDatabase; + d->installationLocations = installationLocations; +} + +PackageManager::~PackageManager() +{ + delete d->database; + delete d; + s_instance = nullptr; +} + +Package *PackageManager::fromId(const QString &id) const +{ + for (auto package : d->packages) { + if (package->id() == id) + return package; + } + return nullptr; +} + +void PackageManager::emitDataChanged(Package *package, const QVector<int> &roles) +{ + int row = d->packages.indexOf(package); + if (row >= 0) { + emit dataChanged(index(row), index(row), roles); + + static const auto pkgChanged = QMetaMethod::fromSignal(&PackageManager::packageChanged); + if (isSignalConnected(pkgChanged)) { + QStringList stringRoles; + for (auto role : roles) + stringRoles << qL1S(s_roleNames[role]); + emit packageChanged(package->id(), stringRoles); + } + } +} + +// item model part + +int PackageManager::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + return d->packages.count(); +} + +QVariant PackageManager::data(const QModelIndex &index, int role) const +{ + if (index.parent().isValid() || !index.isValid()) + return QVariant(); + + Package *package = d->packages.at(index.row()); + + switch (role) { + case Id: + return package->id(); + case Name: + return package->name(); + case Description: + return package->description(); + case Icon: + return package->icon(); + case IsBlocked: + return false; //TODO package->isBlocked(); + case IsUpdating: + return package->state() != Package::Installed; + case UpdateProgress: + return package->progress(); + case IsRemovable: + return !package->isBuiltIn(); + case Version: + return package->version(); + case PackageItem: + return QVariant::fromValue(package); + } + return QVariant(); +} + +QHash<int, QByteArray> PackageManager::roleNames() const +{ + return s_roleNames; +} + +int PackageManager::count() const +{ + return rowCount(); +} + +/*! + \qmlmethod object PackageManager::get(int index) + + Retrieves the model data at \a index as a JavaScript object. See the + \l {PackageManager Roles}{role names} for the expected object fields. + + Returns an empty object if the specified \a index is invalid. + + \note This is very inefficient if you only want to access a single property from QML; use + package() instead to access the Package object's properties directly. +*/ +QVariantMap PackageManager::get(int index) const +{ + if (index < 0 || index >= count()) { + qCWarning(LogSystem) << "PackageManager::get(index): invalid index:" << index; + return QVariantMap(); + } + + QVariantMap map; + QHash<int, QByteArray> roles = roleNames(); + for (auto it = roles.begin(); it != roles.end(); ++it) + map.insert(qL1S(it.value()), data(this->index(index), it.key())); + return map; +} + +/*! + \qmlmethod PackageObject PackageManager::package(int index) + + Returns the \l{PackageObject}{package} corresponding to the given \a index in the + model, or \c null if the index is invalid. + + \note The object ownership of the returned Package object stays with the application-manager. + If you want to store this pointer, you can use the PackageManager's QAbstractListModel + signals or the packageAboutToBeRemoved signal to get notified if the object is about + to be deleted on the C++ side. +*/ +Package *PackageManager::package(int index) const +{ + if (index < 0 || index >= count()) { + qCWarning(LogSystem) << "PackageManager::application(index): invalid index:" << index; + return nullptr; + } + return d->packages.at(index); +} + +/*! + \qmlmethod PackageObject PackageManager::package(string id) + + Returns the \l{PackageObject}{package} corresponding to the given package \a id, + or \c null if the id does not exist. + + \note The object ownership of the returned Package object stays with the application-manager. + If you want to store this pointer, you can use the PackageManager's QAbstractListModel + signals or the packageAboutToBeRemoved signal to get notified if the object is about + to be deleted on the C++ side. +*/ +Package *PackageManager::package(const QString &id) const +{ + auto index = indexOfPackage(id); + return (index < 0) ? nullptr : package(index); +} + +/*! + \qmlmethod int PackageManager::indexOfPackage(string id) + + Maps the package \a id to its position within the model. + + Returns \c -1 if the specified \a id is invalid. +*/ +int PackageManager::indexOfPackage(const QString &id) const +{ + for (int i = 0; i < d->packages.size(); ++i) { + if (d->packages.at(i)->id() == id) + return i; + } + return -1; +} + +bool PackageManager::developmentMode() const +{ + return d->developmentMode; +} + +void PackageManager::setDevelopmentMode(bool enable) +{ + d->developmentMode = enable; +} + +bool PackageManager::allowInstallationOfUnsignedPackages() const +{ + return d->allowInstallationOfUnsignedPackages; +} + +void PackageManager::setAllowInstallationOfUnsignedPackages(bool enable) +{ + d->allowInstallationOfUnsignedPackages = enable; +} + +QString PackageManager::hardwareId() const +{ + return d->hardwareId; +} + +void PackageManager::setHardwareId(const QString &hwId) +{ + d->hardwareId = hwId; +} + +bool PackageManager::isApplicationUserIdSeparationEnabled() const +{ + return d->userIdSeparation; +} + +uint PackageManager::commonApplicationGroupId() const +{ + return d->commonGroupId; +} + +bool PackageManager::enableApplicationUserIdSeparation(uint minUserId, uint maxUserId, uint commonGroupId) +{ + if (minUserId >= maxUserId || minUserId == uint(-1) || maxUserId == uint(-1)) + return false; + d->userIdSeparation = true; + d->minUserId = minUserId; + d->maxUserId = maxUserId; + d->commonGroupId = commonGroupId; + return true; +} + +uint PackageManager::findUnusedUserId() const Q_DECL_NOEXCEPT_EXPR(false) +{ + if (!isApplicationUserIdSeparationEnabled()) + return uint(-1); + + for (uint uid = d->minUserId; uid <= d->maxUserId; ++uid) { + bool match = false; + for (Package *package : d->packages) { + if (package->info()->uid() == uid) { + match = true; + break; + } + } + if (!match) + return uid; + } + throw Exception("could not find a free user-id for application separation in the range %1 to %2") + .arg(d->minUserId).arg(d->maxUserId); +} + +QList<QByteArray> PackageManager::caCertificates() const +{ + return d->chainOfTrust; +} + +void PackageManager::setCACertificates(const QList<QByteArray> &chainOfTrust) +{ + d->chainOfTrust = chainOfTrust; +} + +void PackageManager::cleanupBrokenInstallations() Q_DECL_NOEXCEPT_EXPR(false) +{ + // Check that everything in the app-db is available + // -> if not, remove from app-db + + // key: baseDirPath, value: subDirName/ or fileName + QMultiMap<QString, QString> validPaths; + for (const InstallationLocation &il : qAsConst(d->installationLocations)) { + validPaths.insert(il.documentPath(), QString()); + validPaths.insert(il.installationPath(), QString()); + } + + for (Package *pkg : d->packages) { // we want to detach here! + const InstallationReport *ir = pkg->info()->installationReport(); + if (ir) { + const InstallationLocation &il = installationLocationFromId(ir->installationLocationId()); + + bool valid = il.isValid(); + + if (!valid) + qCDebug(LogInstaller) << "cleanup: uninstalling" << pkg->id() << "- installationLocation is invalid"; + + if (valid) { + QString pkgDir = il.installationPath() + pkg->id(); + QStringList checkDirs; + QStringList checkFiles; + + checkFiles << pkgDir + qSL("/info.yaml"); + checkFiles << pkgDir + qSL("/.installation-report.yaml"); + checkDirs << pkgDir; + checkDirs << il.installationPath() + pkg->id(); + + for (const QString &checkFile : qAsConst(checkFiles)) { + QFileInfo fi(checkFile); + if (!fi.exists() || !fi.isFile() || !fi.isReadable()) { + valid = false; + qCDebug(LogInstaller) << "cleanup: uninstalling" << pkg->id() << "- file missing:" << checkFile; + break; + } + } + for (const QString &checkDir : checkDirs) { + QFileInfo fi(checkDir); + if (!fi.exists() || !fi.isDir() || !fi.isReadable()) { + valid = false; + qCDebug(LogInstaller) << "cleanup: uninstalling" << pkg->id() << "- directory missing:" << checkDir; + break; + } + } + + if (valid) { + validPaths.insertMulti(il.installationPath(), pkg->id() + qL1C('/')); + validPaths.insertMulti(il.documentPath(), pkg->id() + qL1C('/')); + } + } + if (!valid) { + if (startingPackageRemoval(pkg->id())) { + if (finishedPackageInstall(pkg->id())) + continue; + } + throw Exception(Error::Package, "could not remove broken installation of package %1 from database").arg(pkg->id()); + } + } + } + + // Remove everything that is not referenced from the app-db + + for (auto it = validPaths.cbegin(); it != validPaths.cend(); ) { + const QString currentDir = it.key(); + + // collect all values for the unique key currentDir + QVector<QString> validNames; + for ( ; it != validPaths.cend() && it.key() == currentDir; ++it) + validNames << it.value(); + + const QFileInfoList &dirEntries = QDir(currentDir).entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot); + + // check if there is anything in the filesystem that is NOT listed in the validNames + for (const QFileInfo &fi : dirEntries) { + QString name = fi.fileName(); + if (fi.isDir()) + name.append(qL1C('/')); + + if ((!fi.isDir() && !fi.isFile()) || !validNames.contains(name)) { + qCDebug(LogInstaller) << "cleanup: removing unreferenced inode" << name; + + if (SudoClient::instance()) { + if (!SudoClient::instance()->removeRecursive(fi.absoluteFilePath())) { + throw Exception(Error::IO, "could not remove broken installation leftover %1: %2") + .arg(fi.absoluteFilePath()).arg(SudoClient::instance()->lastError()); + } + } else { + if (!recursiveOperation(fi.absoluteFilePath(), safeRemove)) { + throw Exception(Error::IO, "could not remove broken installation leftover %1 (maybe due to missing root privileges)") + .arg(fi.absoluteFilePath()); + } + } + } + } + } +} + + +QVector<InstallationLocation> PackageManager::installationLocations() const +{ + return d->installationLocations; +} + +const InstallationLocation &PackageManager::defaultInstallationLocation() const +{ + for (const InstallationLocation &il : d->installationLocations) { + if (il.isDefault()) + return il; + } + return InstallationLocation::invalid; +} + +const InstallationLocation &PackageManager::installationLocationFromId(const QString &installationLocationId) const +{ + for (const InstallationLocation &il : d->installationLocations) { + if (il.id() == installationLocationId) + return il; + } + return InstallationLocation::invalid; +} + +const InstallationLocation &PackageManager::installationLocationFromPackage(const QString &packageId) const +{ + if (Package *package = fromId(packageId)) { + if (const InstallationReport *report = package->info()->installationReport()) + return installationLocationFromId(report->installationLocationId()); + } + return InstallationLocation::invalid; +} + +/*! + \qmlmethod list<string> PackageManager::packageIds() + + Returns a list of all available package ids. This can be used to further query for specific + information via get(). +*/ +QStringList PackageManager::packageIds() const +{ + QStringList ids; + ids.reserve(d->packages.size()); + for (int i = 0; i < d->packages.size(); ++i) + ids << d->packages.at(i)->id(); + return ids; +} + +/*! + \qmlmethod object PackageManager::get(string id) + + Retrieves the model data for the package identified by \a id as a JavaScript object. + See the \l {PackageManager Roles}{role names} for the expected object fields. + + Returns an empty object if the specified \a id is invalid. +*/ +QVariantMap PackageManager::get(const QString &id) const +{ + int index = indexOfPackage(id); + return (index < 0) ? QVariantMap{} : get(index); +} + +/*! + \qmlmethod list<string> PackageManager::installationLocationIds() + + Retuns a list of all known installation location ids. Calling getInstallationLocation() on one of + the returned identifiers will yield specific information about the individual installation locations. +*/ +QStringList PackageManager::installationLocationIds() const +{ + QStringList ids; + for (const InstallationLocation &il : d->installationLocations) + ids << il.id(); + return ids; +} + +/*! + \qmlmethod string PackageManager::installationLocationIdFromApplication(string packageId) + + Returns the installation location id for the package identified by \a packageId. Returns + an empty string in case the package is not installed. + + \sa installationLocationIds() +*/ +QString PackageManager::installationLocationIdFromPackage(const QString &packageId) const +{ + const InstallationLocation &il = installationLocationFromPackage(packageId); + return il.isValid() ? il.id() : QString(); +} + +/*! + \qmlmethod object PackageManager::getInstallationLocation(string installationLocationId) + + Returns an object describing the installation location identified by \a installationLocationId + in detail. + + The returned object has the following members: + + \table + \header + \li \c Name + \li \c Type + \li Description + \row + \li \c id + \li \c string + \li The installation location id that is used as the handle for all other ApplicationInstaller + function calls. The \c id consists of the \c type and \c index field, concatenated by + a single dash (for example, \c internal-0). + \row + \li \c type + \li \c string + \li The type of device this installation location is connected to. Valid values are \c + internal (for any kind of built-in storage, e.g. flash) and \c invalid. + \row + \li \c index + \li \c int + \li In case there is more than one installation location for the same type of device, this + \c zero-based index is used for disambiguation. Otherwise, the index is always \c 0. + \row + \li \c isDefault + \li \c bool + + \li Exactly one installation location is the default location which must be mounted and + accessible at all times. This can be used by an UI application to get a sensible + default for the installation location that it needs to pass to startPackageInstallation(). + \row + \li \c installationPath + \li \c string + \li The absolute file-system path to the base directory under which applications are installed. + \row + \li \c installationDeviceSize + \li \c int + \li The size of the device holding \c installationPath in bytes. + \row + \li \c installationDeviceFree + \li \c int + \li The amount of bytes available on the device holding \c installationPath. + \row + \li \c documentPath + \li \c string + \li The absolute file-system path to the base directory under which per-user document + directories are created. + \row + \li \c documentDeviceSize + \li \c int + \li The size of the device holding \c documentPath in bytes. + \row + \li \c documentDeviceFree + \li \c int + \li The amount of bytes available on the device holding \c documentPath. + \endtable + + Returns an empty object in case the \a installationLocationId is not valid. +*/ +QVariantMap PackageManager::getInstallationLocation(const QString &installationLocationId) const +{ + const InstallationLocation &il = installationLocationFromId(installationLocationId); + return il.isValid() ? il.toVariantMap() : QVariantMap(); +} + +/*! + \qmlmethod int PackageManager::installedPackageSize(string packageId) + + Returns the size in bytes that the package identified by \a packageId is occupying on the storage + device. + + Returns \c -1 in case the package \a packageId is not valid, or the package is not installed. +*/ +qint64 PackageManager::installedPackageSize(const QString &packageId) const +{ + if (Package *package = fromId(packageId)) { + if (const InstallationReport *report = package->info()->installationReport()) + return static_cast<qint64>(report->diskSpaceUsed()); + } + return -1; +} + +/*! + \qmlmethod var PackageManager::installedPackageExtraMetaData(string packageId) + + Returns a map of all extra metadata in the package header of the package identified by \a packageId. + + Returns an empty map in case the package \a packageId is not valid, or the package is not installed. +*/ +QVariantMap PackageManager::installedPackageExtraMetaData(const QString &packageId) const +{ + if (Package *package = fromId(packageId)) { + if (const InstallationReport *report = package->info()->installationReport()) + return report->extraMetaData(); + } + return QVariantMap(); +} + +/*! + \qmlmethod var PackageManager::installedApplicationExtraSignedMetaData(string packageId) + + Returns a map of all signed extra metadata in the package header of the package identified + by \a packageId. + + Returns an empty map in case the package \a packageId is not valid, or the package is not installed. +*/ +QVariantMap PackageManager::installedPackageExtraSignedMetaData(const QString &packageId) const +{ + if (Package *package = fromId(packageId)) { + if (const InstallationReport *report = package->info()->installationReport()) + return report->extraSignedMetaData(); + } + return QVariantMap(); +} + +/*! \internal + Type safe convenience function, since DBus does not like QUrl +*/ +QString PackageManager::startPackageInstallation(const QString &installationLocationId, const QUrl &sourceUrl) +{ + AM_TRACE(LogInstaller, installationLocationId, sourceUrl); + + const InstallationLocation &il = installationLocationFromId(installationLocationId); + + return enqueueTask(new InstallationTask(il, sourceUrl)); +} + +/*! + \qmlmethod string PackageManager::startPackageInstallation(string installationLocationId, string sourceUrl) + + Downloads an application package from \a sourceUrl and installs it to the installation location + described by \a installationLocationId. + + The actual download and installation will happen asynchronously in the background. The + PackageManager emits the signals \l taskStarted, \l taskProgressChanged, \l + taskRequestingInstallationAcknowledge, \l taskFinished, \l taskFailed, and \l taskStateChanged + for the returned taskId when applicable. + + \note Simply calling this function is not enough to complete a package installation: The + taskRequestingInstallationAcknowledge() signal needs to be connected to a slot where the + supplied package meta-data can be validated (either programmatically or by asking the user). + If the validation is successful, the installation can be completed by calling + acknowledgePackageInstallation() or, if the validation was unsuccessful, the installation should + be canceled by calling cancelTask(). + Failing to do one or the other will leave an unfinished "zombie" installation. + + Returns a unique \c taskId. This can also be an empty string, if the task could not be + created (in this case, no signals will be emitted). +*/ +QString PackageManager::startPackageInstallation(const QString &installationLocationId, const QString &sourceUrl) +{ + QUrl url(sourceUrl); + if (url.scheme().isEmpty()) + url = QUrl::fromLocalFile(sourceUrl); + return startPackageInstallation(installationLocationId, url); +} + +/*! + \qmlmethod void PackageManager::acknowledgePackageInstallation(string taskId) + + Calling this function enables the installer to complete the installation task identified by \a + taskId. Normally, this function is called after receiving the taskRequestingInstallationAcknowledge() + signal, and the user and/or the program logic decided to proceed with the installation. + + \sa startPackageInstallation() + */ +void PackageManager::acknowledgePackageInstallation(const QString &taskId) +{ + AM_TRACE(LogInstaller, taskId) + + const auto allTasks = d->allTasks(); + + for (AsynchronousTask *task : allTasks) { + if (qobject_cast<InstallationTask *>(task) && (task->id() == taskId)) { + static_cast<InstallationTask *>(task)->acknowledge(); + break; + } + } +} + +/*! + \qmlmethod string PackageManager::removePackage(string packageId, bool keepDocuments, bool force) + + Uninstalls the package identified by \a id. Normally, the documents directory of the + package is deleted on removal, but this can be prevented by setting \a keepDocuments to \c true. + + The actual removal will happen asynchronously in the background. The PackageManager will + emit the signals \l taskStarted, \l taskProgressChanged, \l taskFinished, \l taskFailed and \l + taskStateChanged for the returned \c taskId when applicable. + + Normally, \a force should only be set to \c true if a previous call to removePackage() failed. + This may be necessary if the installation process was interrupted, or or has file-system issues. + + Returns a unique \c taskId. This can also be an empty string, if the task could not be created + (in this case, no signals will be emitted). +*/ +QString PackageManager::removePackage(const QString &packageId, bool keepDocuments, bool force) +{ + AM_TRACE(LogInstaller, packageId, keepDocuments) + + if (Package *package = fromId(packageId)) { + if (const InstallationReport *report = package->info()->installationReport()) { + const InstallationLocation &il = installationLocationFromId(report->installationLocationId()); + + if (il.isValid() && (il.id() == report->installationLocationId())) + return enqueueTask(new DeinstallationTask(package->info(), il, force, keepDocuments)); + } + } + return QString(); +} + + +/*! + \qmlmethod enumeration PackageManager::taskState(string taskId) + + Returns the current state of the installation task identified by \a taskId. + \l {TaskStates}{See here} for a list of valid task states. + + Returns \c PackageManager.Invalid if the \a taskId is invalid. +*/ +AsynchronousTask::TaskState PackageManager::taskState(const QString &taskId) const +{ + const auto allTasks = d->allTasks(); + + for (const AsynchronousTask *task : allTasks) { + if (task && (task->id() == taskId)) + return task->state(); + } + return AsynchronousTask::Invalid; +} + +/*! + \qmlmethod string PackageManager::taskPackageId(string taskId) + + Returns the package id associated with the task identified by \a taskId. The task may not + have a valid package id at all times though and in this case the function will return an + empty string (this will be the case for installations before the taskRequestingInstallationAcknowledge + signal has been emitted). + + Returns an empty string if the \a taskId is invalid. +*/ +QString PackageManager::taskPackageId(const QString &taskId) const +{ + const auto allTasks = d->allTasks(); + + for (const AsynchronousTask *task : allTasks) { + if (task && (task->id() == taskId)) + return task->packageId(); + } + return QString(); +} + +/*! + \qmlmethod list<string> PackageManager::activeTaskIds() + + Retuns a list of all currently active (as in not yet finished or failed) installation task ids. +*/ +QStringList PackageManager::activeTaskIds() const +{ + const auto allTasks = d->allTasks(); + + QStringList result; + for (const AsynchronousTask *task : allTasks) + result << task->id(); + return result; +} + +/*! + \qmlmethod bool PackageManager::cancelTask(string taskId) + + Tries to cancel the installation task identified by \a taskId. + + Returns \c true if the task was canceled, \c false otherwise. +*/ +bool PackageManager::cancelTask(const QString &taskId) +{ + AM_TRACE(LogInstaller, taskId) + + // incoming tasks can be forcefully cancelled right away + for (AsynchronousTask *task : qAsConst(d->incomingTaskList)) { + if (task->id() == taskId) { + task->forceCancel(); + task->deleteLater(); + + handleFailure(task); + + d->incomingTaskList.removeOne(task); + triggerExecuteNextTask(); + return true; + } + } + + // the active task and async tasks might be in a state where cancellation is not possible, + // so we have to ask them nicely + if (d->activeTask && d->activeTask->id() == taskId) + return d->activeTask->cancel(); + + for (AsynchronousTask *task : qAsConst(d->installationTaskList)) { + if (task->id() == taskId) + return task->cancel(); + } + return false; +} + +/*! + \qmlmethod int PackageManager::compareVersions(string version1, string version2) + + Convenience method for app-store implementations or taskRequestingInstallationAcknowledge() + callbacks for comparing version numbers, as the actual version comparison algorithm is not + trivial. + + Returns \c -1, \c 0 or \c 1 if \a version1 is smaller than, equal to, or greater than \a + version2 (similar to how \c strcmp() works). +*/ +int PackageManager::compareVersions(const QString &version1, const QString &version2) +{ + int vn1Suffix = -1; + int vn2Suffix = -1; + QVersionNumber vn1 = QVersionNumber::fromString(version1, &vn1Suffix); + QVersionNumber vn2 = QVersionNumber::fromString(version2, &vn2Suffix); + + int d = QVersionNumber::compare(vn1, vn2); + return d < 0 ? -1 : (d > 0 ? 1 : version1.mid(vn1Suffix).compare(version2.mid(vn2Suffix))); +} + +/*! + \qmlmethod int PackageManager::validateDnsName(string name, int minimalPartCount) + + Convenience method for app-store implementations or taskRequestingInstallationAcknowledge() + callbacks for checking if the given \a name is a valid DNS (or reverse-DNS) name according to + RFC 1035/1123. If the optional parameter \a minimalPartCount is specified, this function will + also check if \a name contains at least this amount of parts/sub-domains. + + Returns \c true if the name is a valid DNS name or \c false otherwise. +*/ +bool PackageManager::validateDnsName(const QString &name, int minimalPartCount) +{ + try { + // check if we have enough parts: e.g. "tld.company.app" would have 3 parts + QStringList parts = name.split('.'); + if (parts.size() < minimalPartCount) { + throw Exception(Error::Parse, "the minimum amount of parts (subdomains) is %1 (found %2)") + .arg(minimalPartCount).arg(parts.size()); + } + + // standard RFC compliance tests (RFC 1035/1123) + + auto partCheck = [](const QString &part) { + int len = part.length(); + + if (len < 1 || len > 63) + throw Exception(Error::Parse, "domain parts must consist of at least 1 and at most 63 characters (found %2 characters)").arg(len); + + for (int pos = 0; pos < len; ++pos) { + ushort ch = part.at(pos).unicode(); + bool isFirst = (pos == 0); + bool isLast = (pos == (len - 1)); + bool isDash = (ch == '-'); + bool isDigit = (ch >= '0' && ch <= '9'); + bool isLower = (ch >= 'a' && ch <= 'z'); + + if ((isFirst || isLast || !isDash) && !isDigit && !isLower) + throw Exception(Error::Parse, "domain parts must consist of only the characters '0-9', 'a-z', and '-' (which cannot be the first or last character)"); + } + }; + + for (const QString &part : parts) + partCheck(part); + + return true; + } catch (const Exception &e) { + qCDebug(LogInstaller).noquote() << "validateDnsName failed:" << e.errorString(); + return false; + } +} + +QString PackageManager::enqueueTask(AsynchronousTask *task) +{ + d->incomingTaskList.append(task); + triggerExecuteNextTask(); + return task->id(); +} + +void PackageManager::triggerExecuteNextTask() +{ + if (!QMetaObject::invokeMethod(this, "executeNextTask", Qt::QueuedConnection)) + qCCritical(LogSystem) << "ERROR: failed to invoke method checkQueue"; +} + +void PackageManager::executeNextTask() +{ + if (d->activeTask || d->incomingTaskList.isEmpty()) + return; + + AsynchronousTask *task = d->incomingTaskList.takeFirst(); + + if (task->hasFailed()) { + task->setState(AsynchronousTask::Failed); + + handleFailure(task); + + task->deleteLater(); + triggerExecuteNextTask(); + return; + } + + connect(task, &AsynchronousTask::started, this, [this, task]() { + emit taskStarted(task->id()); + }); + + connect(task, &AsynchronousTask::stateChanged, this, [this, task](AsynchronousTask::TaskState newState) { + emit taskStateChanged(task->id(), newState); + }); + + connect(task, &AsynchronousTask::progress, this, [this, task](qreal p) { + emit taskProgressChanged(task->id(), p); + + Package *package = fromId(task->packageId()); + if (package && (package->state() != Package::Installed)) { + package->setProgress(p); + // Icon will be in a "+" suffixed directory during installation. So notify about a change on its + // location as well. + emitDataChanged(package, QVector<int> { Icon, UpdateProgress }); + } + }); + + connect(task, &AsynchronousTask::finished, this, [this, task]() { + task->setState(task->hasFailed() ? AsynchronousTask::Failed : AsynchronousTask::Finished); + + if (task->hasFailed()) { + handleFailure(task); + } else { + qCDebug(LogInstaller) << "emit finished" << task->id(); + emit taskFinished(task->id()); + } + + if (d->activeTask == task) + d->activeTask = nullptr; + d->installationTaskList.removeOne(task); + + delete task; + triggerExecuteNextTask(); + }); + + if (qobject_cast<InstallationTask *>(task)) { + connect(static_cast<InstallationTask *>(task), &InstallationTask::finishedPackageExtraction, this, [this, task]() { + qCDebug(LogInstaller) << "emit blockingUntilInstallationAcknowledge" << task->id(); + emit taskBlockingUntilInstallationAcknowledge(task->id()); + + // we can now start the next download in parallel - the InstallationTask will take care + // of serializing the final installation steps on its own as soon as it gets the + // required acknowledge (or cancel). + if (d->activeTask == task) + d->activeTask = nullptr; + d->installationTaskList.append(task); + triggerExecuteNextTask(); + }); + } + + + d->activeTask = task; + task->setState(AsynchronousTask::Executing); + task->start(); +} + +void PackageManager::handleFailure(AsynchronousTask *task) +{ + qCDebug(LogInstaller) << "emit failed" << task->id() << task->errorCode() << task->errorString(); + emit taskFailed(task->id(), int(task->errorCode()), task->errorString()); +} + +bool PackageManager::startingPackageInstallation(PackageInfo *info) +{ + // ownership of info is transferred to PackageManager + QScopedPointer<PackageInfo> newInfo(info); + + if (!newInfo || newInfo->id().isEmpty()) + return false; + Package *package = fromId(newInfo->id()); +// if (!RuntimeFactory::instance()->manager(newInfo->runtimeName())) +// return false; + + if (package) { // update +// if (!blockApplication(app->id())) +// return false; + + if (package->isBuiltIn()) { + // overlay the existing base info + // we will rollback to the base one if this update is removed. + package->setUpdatedInfo(newInfo.take()); + } else { + // overwrite the existing base info + // we're not keeping track of the original. so removing the updated base version removes the + // application entirely. + package->setBaseInfo(newInfo.take()); + } + package->setState(Package::BeingUpdated); + package->setProgress(0); + emitDataChanged(package); + } else { // installation + package = new Package(newInfo.take(), Package::BeingInstalled); + + //app->block(); + + beginInsertRows(QModelIndex(), d->packages.count(), d->packages.count()); + + QQmlEngine::setObjectOwnership(package, QQmlEngine::CppOwnership); + d->packages << package; + + endInsertRows(); + + emitDataChanged(package); + + emit packageAdded(package->id()); + } + return true; +} + +bool PackageManager::startingPackageRemoval(const QString &id) +{ + Package *package = fromId(id); + if (!package) + return false; + + if (/*package->isBlocked()*/ false || (package->state() != Package::Installed)) + return false; + + if (package->isBuiltIn() && !package->canBeRevertedToBuiltIn()) + return false; + +// if (!blockApplication(id)) +// return false; + + package->setState(package->canBeRevertedToBuiltIn() ? Package::BeingDowngraded + : Package::BeingRemoved); + + package->setProgress(0); + emitDataChanged(package, QVector<int> { IsUpdating }); + return true; +} + +bool PackageManager::finishedPackageInstall(const QString &id) +{ + Package *package = fromId(id); + if (!package) + return false; + + switch (package->state()) { + case Package::Installed: + return false; + + case Package::BeingInstalled: + case Package::BeingUpdated: { + // The Package object has been updated right at the start of the installation/update. + // Now's the time to update the InstallationReport that was written by the installer. + QFile irfile(QDir(package->info()->baseDir()).absoluteFilePath(qSL(".installation-report.yaml"))); + QScopedPointer<InstallationReport> ir(new InstallationReport(package->id())); + if (!irfile.open(QFile::ReadOnly) || !ir->deserialize(&irfile)) { + qCCritical(LogInstaller) << "Could not read the new installation-report for package" + << package->id() << "at" << irfile.fileName(); + return false; + } + package->info()->setInstallationReport(ir.take()); + package->setState(Package::Installed); + package->setProgress(0); + + emitDataChanged(package); + + // unblockApplication(id); + emit package->bulkChange(); // not ideal, but icon and codeDir have changed + break; + } + case Package::BeingDowngraded: + package->setUpdatedInfo(nullptr); + package->setState(Package::Installed); + break; + + case Package::BeingRemoved: { + int row = d->packages.indexOf(package); + if (row >= 0) { + emit packageAboutToBeRemoved(package->id()); + beginRemoveRows(QModelIndex(), row, row); + d->packages.removeAt(row); + endRemoveRows(); + } + delete package; + break; + } + } + + //emit internalSignals.applicationsChanged(); + + return true; +} + +bool PackageManager::canceledPackageInstall(const QString &id) +{ + Package *package = fromId(id); + if (!package) + return false; + + switch (package->state()) { + case Package::Installed: + return false; + + case Package::BeingInstalled: { + int row = d->packages.indexOf(package); + if (row >= 0) { + emit packageAboutToBeRemoved(package->id()); + beginRemoveRows(QModelIndex(), row, row); + d->packages.removeAt(row); + endRemoveRows(); + } + delete package; + break; + } + case Package::BeingUpdated: + case Package::BeingDowngraded: + case Package::BeingRemoved: + package->setState(Package::Installed); + package->setProgress(0); + emitDataChanged(package, QVector<int> { IsUpdating }); + + // unblockApplication(id); + break; + } + return true; +} + + +bool removeRecursiveHelper(const QString &path) +{ + if (PackageManager::instance()->isApplicationUserIdSeparationEnabled() && SudoClient::instance()) + return SudoClient::instance()->removeRecursive(path); + else + return recursiveOperation(path, safeRemove); +} + +QT_END_NAMESPACE_AM |