summaryrefslogtreecommitdiffstats
path: root/src/installer-lib/packagemanager.cpp
diff options
context:
space:
mode:
authorRobert Griebl <robert.griebl@pelagicore.com>2019-04-03 00:48:22 +0200
committerRobert Griebl <robert.griebl@pelagicore.com>2019-08-01 11:23:31 +0200
commitb4aee167d3bc6b9f64229317fbc428b3f3b83c0d (patch)
tree6b526d57203f34f4b85a82e2e958ace227c83960 /src/installer-lib/packagemanager.cpp
parent3bc3dc4c8e912beb18aec7ab84af40c0129d84c0 (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.cpp1243
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