From ed9dea417c1346183de52782459eb0b697778245 Mon Sep 17 00:00:00 2001 From: Robert Griebl Date: Thu, 1 Aug 2019 17:02:46 +0200 Subject: (Re)implement package and application blocking This was deliberately left out of the first patch that introduced the package abstraction. As a side effect, the classes from the installer-lib had to be moved to the manager-lib, because the PackageManager is now a central part of the AM, that has to have access to other classes in the manager-lib. Change-Id: I94c1e62d3ffa769f3e053aab8730fea9133a7bc8 Reviewed-by: Dominik Holland --- doc/QtApplicationManagerDoc | 1 - .../custom-appman/custom-appman.cpp | 2 +- src/dbus-lib/dbus-lib.pro | 1 - src/installer-lib/asynchronoustask.cpp | 133 --- src/installer-lib/asynchronoustask.h | 111 -- src/installer-lib/deinstallationtask.cpp | 155 --- src/installer-lib/deinstallationtask.h | 75 -- src/installer-lib/installationtask.cpp | 484 -------- src/installer-lib/installationtask.h | 111 -- src/installer-lib/installer-lib.pro | 39 - src/installer-lib/package.cpp | 193 ---- src/installer-lib/package.h | 138 --- src/installer-lib/packagemanager.cpp | 1193 -------------------- src/installer-lib/packagemanager.h | 210 ---- src/installer-lib/packagemanager_p.h | 97 -- src/installer-lib/scopeutilities.cpp | 215 ---- src/installer-lib/scopeutilities.h | 105 -- src/installer-lib/sudo.cpp | 495 -------- src/installer-lib/sudo.h | 154 --- src/main-lib/applicationinstaller.h | 6 +- src/main-lib/main-lib.pro | 1 - src/manager-lib/application.cpp | 34 +- src/manager-lib/application.h | 8 +- src/manager-lib/applicationmanager.cpp | 29 +- src/manager-lib/applicationmanager.h | 3 - src/manager-lib/asynchronoustask.cpp | 133 +++ src/manager-lib/asynchronoustask.h | 111 ++ src/manager-lib/deinstallationtask.cpp | 155 +++ src/manager-lib/deinstallationtask.h | 75 ++ src/manager-lib/installationtask.cpp | 484 ++++++++ src/manager-lib/installationtask.h | 111 ++ src/manager-lib/manager-lib.pro | 28 +- src/manager-lib/package.cpp | 237 ++++ src/manager-lib/package.h | 151 +++ src/manager-lib/packagemanager.cpp | 1192 +++++++++++++++++++ src/manager-lib/packagemanager.h | 210 ++++ src/manager-lib/packagemanager_p.h | 97 ++ src/manager-lib/scopeutilities.cpp | 215 ++++ src/manager-lib/scopeutilities.h | 105 ++ src/manager-lib/sudo.cpp | 495 ++++++++ src/manager-lib/sudo.h | 154 +++ src/src.pro | 12 +- src/tools/dumpqmltypes/dumpqmltypes.cpp | 2 +- src/tools/dumpqmltypes/dumpqmltypes.pro | 1 - tests/application/application.pro | 1 - .../applicationinstaller/applicationinstaller.pro | 1 - tests/packager-tool/packager-tool.pro | 1 - tests/runtime/runtime.pro | 1 - tests/sudo/sudo.pro | 2 +- 49 files changed, 3989 insertions(+), 3978 deletions(-) delete mode 100644 src/installer-lib/asynchronoustask.cpp delete mode 100644 src/installer-lib/asynchronoustask.h delete mode 100644 src/installer-lib/deinstallationtask.cpp delete mode 100644 src/installer-lib/deinstallationtask.h delete mode 100644 src/installer-lib/installationtask.cpp delete mode 100644 src/installer-lib/installationtask.h delete mode 100644 src/installer-lib/installer-lib.pro delete mode 100644 src/installer-lib/package.cpp delete mode 100644 src/installer-lib/package.h delete mode 100644 src/installer-lib/packagemanager.cpp delete mode 100644 src/installer-lib/packagemanager.h delete mode 100644 src/installer-lib/packagemanager_p.h delete mode 100644 src/installer-lib/scopeutilities.cpp delete mode 100644 src/installer-lib/scopeutilities.h delete mode 100644 src/installer-lib/sudo.cpp delete mode 100644 src/installer-lib/sudo.h create mode 100644 src/manager-lib/asynchronoustask.cpp create mode 100644 src/manager-lib/asynchronoustask.h create mode 100644 src/manager-lib/deinstallationtask.cpp create mode 100644 src/manager-lib/deinstallationtask.h create mode 100644 src/manager-lib/installationtask.cpp create mode 100644 src/manager-lib/installationtask.h create mode 100644 src/manager-lib/package.cpp create mode 100644 src/manager-lib/package.h create mode 100644 src/manager-lib/packagemanager.cpp create mode 100644 src/manager-lib/packagemanager.h create mode 100644 src/manager-lib/packagemanager_p.h create mode 100644 src/manager-lib/scopeutilities.cpp create mode 100644 src/manager-lib/scopeutilities.h create mode 100644 src/manager-lib/sudo.cpp create mode 100644 src/manager-lib/sudo.h diff --git a/doc/QtApplicationManagerDoc b/doc/QtApplicationManagerDoc index 333bf5a6..ccd77195 100644 --- a/doc/QtApplicationManagerDoc +++ b/doc/QtApplicationManagerDoc @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include diff --git a/examples/applicationmanager/custom-appman/custom-appman.cpp b/examples/applicationmanager/custom-appman/custom-appman.cpp index e6d8c7d6..0c2cb4dd 100644 --- a/examples/applicationmanager/custom-appman/custom-appman.cpp +++ b/examples/applicationmanager/custom-appman/custom-appman.cpp @@ -56,7 +56,7 @@ #include #include #include -#include +#include QT_USE_NAMESPACE_AM diff --git a/src/dbus-lib/dbus-lib.pro b/src/dbus-lib/dbus-lib.pro index 8104bdd5..5635fdf0 100644 --- a/src/dbus-lib/dbus-lib.pro +++ b/src/dbus-lib/dbus-lib.pro @@ -32,7 +32,6 @@ ADAPTORS_XML = \ org.freedesktop.notifications.xml \ !disable-installer { - QT *= appman_installer-private HEADERS += packagemanagerdbuscontextadaptor.h SOURCES += packagemanagerdbuscontextadaptor.cpp ADAPTORS_XML += io.qt.packagemanager.xml diff --git a/src/installer-lib/asynchronoustask.cpp b/src/installer-lib/asynchronoustask.cpp deleted file mode 100644 index 168dc72f..00000000 --- a/src/installer-lib/asynchronoustask.cpp +++ /dev/null @@ -1,133 +0,0 @@ -/**************************************************************************** -** -** 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 - -#include "global.h" -#include "asynchronoustask.h" - -QT_BEGIN_NAMESPACE_AM - -AsynchronousTask::AsynchronousTask(QObject *parent) - : QThread(parent) - , m_id(QUuid::createUuid().toString()) -{ - static int once = qRegisterMetaType(); - Q_UNUSED(once) -} - -QString AsynchronousTask::id() const -{ - return m_id; -} - -AsynchronousTask::TaskState AsynchronousTask::state() const -{ - return m_state; -} - -void AsynchronousTask::setState(AsynchronousTask::TaskState state) -{ - if (m_state != state) { - m_state = state; - emit stateChanged(m_state); - } -} - -bool AsynchronousTask::hasFailed() const -{ - return (m_state == Failed); -} - -Error AsynchronousTask::errorCode() const -{ - return m_errorCode; -} - -QString AsynchronousTask::errorString() const -{ - return m_errorString; -} - - -bool AsynchronousTask::cancel() -{ - return false; -} - -bool AsynchronousTask::forceCancel() -{ - if (m_state == Queued) { - setError(Error::Canceled, qSL("canceled")); - return true; - } - return cancel(); -} - -QString AsynchronousTask::packageId() const -{ - return m_packageId; -} - -bool AsynchronousTask::preExecute() -{ - return true; -} - -bool AsynchronousTask::postExecute() -{ - return true; -} - -void AsynchronousTask::setError(Error errorCode, const QString &errorString) -{ - m_errorCode = errorCode; - m_errorString = errorString; - setState(Failed); -} - -void AsynchronousTask::run() -{ - execute(); -} - -QT_END_NAMESPACE_AM diff --git a/src/installer-lib/asynchronoustask.h b/src/installer-lib/asynchronoustask.h deleted file mode 100644 index 977140df..00000000 --- a/src/installer-lib/asynchronoustask.h +++ /dev/null @@ -1,111 +0,0 @@ -/**************************************************************************** -** -** 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 -** -****************************************************************************/ - -#pragma once - -#include -#include - -#include - -QT_BEGIN_NAMESPACE_AM - -class AsynchronousTask : public QThread -{ - Q_OBJECT - -public: - enum TaskState - { - Invalid, - Queued, - Executing, - Failed, - Finished, - - // installation task only - AwaitingAcknowledge, - Installing, - CleaningUp - }; - Q_ENUM(TaskState) - - AsynchronousTask(QObject *parent = nullptr); - - QString id() const; - - TaskState state() const; - void setState(TaskState state); - - bool hasFailed() const; - Error errorCode() const; - QString errorString() const; - - virtual bool cancel(); - bool forceCancel(); // will always work in Queued state - - QString packageId() const; // convenience - - virtual bool preExecute(); - virtual bool postExecute(); - -signals: - void stateChanged(QT_PREPEND_NAMESPACE_AM(AsynchronousTask::TaskState) newState); - void progress(qreal p); - -protected: - void setError(Error errorCode, const QString &errorString); - virtual void execute() = 0; - void run() override final; - -protected: - QMutex m_mutex; - - QString m_id; - QString m_packageId; - TaskState m_state = Queued; - Error m_errorCode = Error::None; - QString m_errorString; -}; - - -QT_END_NAMESPACE_AM diff --git a/src/installer-lib/deinstallationtask.cpp b/src/installer-lib/deinstallationtask.cpp deleted file mode 100644 index 94551abe..00000000 --- a/src/installer-lib/deinstallationtask.cpp +++ /dev/null @@ -1,155 +0,0 @@ -/**************************************************************************** -** -** 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 "logging.h" -#include "packagemanager.h" -#include "packagemanager_p.h" -#include "installationreport.h" -#include "packageinfo.h" -#include "exception.h" -#include "scopeutilities.h" -#include "deinstallationtask.h" - -QT_BEGIN_NAMESPACE_AM - -DeinstallationTask::DeinstallationTask(PackageInfo *package, const QString &installationPath, - const QString &documentPath, bool forceDeinstallation, - bool keepDocuments, QObject *parent) - : AsynchronousTask(parent) - , m_package(package) - , m_installationPath(installationPath) - , m_documentPath(documentPath) - , m_forceDeinstallation(forceDeinstallation) - , m_keepDocuments(keepDocuments) -{ - m_packageId = m_package->id(); // in base class -} - -bool DeinstallationTask::cancel() -{ - if (m_canBeCanceled) - m_canceled = true; - return m_canceled; -} - -void DeinstallationTask::execute() -{ - // these have been checked in PackageManager::removePackage() already - Q_ASSERT(m_package); - Q_ASSERT(m_package->installationReport()); - - bool managerApproval = false; - - try { - // we need to call those PackageManager methods in the correct thread - // this will also exclusively lock the package for us - QMetaObject::invokeMethod(PackageManager::instance(), [this, &managerApproval]() - { managerApproval = PackageManager::instance()->startingPackageRemoval(m_package->id()); }, - Qt::BlockingQueuedConnection); - - if (!managerApproval) - throw Exception("ApplicationManager rejected the removal of package %1").arg(m_package->id()); - - // if the app was running before, we now need to wait until is has actually stopped -//TODO: this needs to be ported to the new PackageManager architecture -// while (!m_canceled && -// (ApplicationManager::instance()->applicationRunState(m_app->id()) != Am::NotRunning)) { -// QThread::msleep(30); -// } - // there's a small race condition here, but not doing a planned cancellation isn't harmful - m_canBeCanceled = false; - if (m_canceled) - throw Exception(Error::Canceled, "canceled"); - - ScopedRenamer docDirRename; - ScopedRenamer appDirRename; - - if (!m_keepDocuments) { - if (!docDirRename.rename(QDir(m_documentPath).absoluteFilePath(m_package->id()), - ScopedRenamer::NameToNameMinus)) { - throw Exception(Error::IO, "could not rename %1 to %1-").arg(docDirRename.baseName()); - } - } - - if (!appDirRename.rename(QDir(m_installationPath).absoluteFilePath(m_package->id()), - ScopedRenamer::NameToNameMinus)) { - throw Exception(Error::IO, "could not rename %1 to %1-").arg(appDirRename.baseName()); - } - - docDirRename.take(); - appDirRename.take(); - - // point of no return - - for (ScopedRenamer *toDelete : { &docDirRename, &appDirRename }) { - if (toDelete->isRenamed()) { - if (!removeRecursiveHelper(toDelete->baseName() + qL1C('-'))) - qCCritical(LogInstaller) << "ERROR: could not remove" << (toDelete->baseName() + qL1C('-')); - } - } - - // 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_package->id()); }, - Qt::BlockingQueuedConnection); - - if (!finishOk) - qCWarning(LogInstaller) << "PackageManager did not approve deinstallation of " << m_packageId; - - } catch (const Exception &e) { - // we need to call those ApplicationManager methods in the correct thread - if (managerApproval) { - bool cancelOk = false; - QMetaObject::invokeMethod(PackageManager::instance(), [this, &cancelOk]() - { cancelOk = PackageManager::instance()->canceledPackageInstall(m_package->id()); }, - Qt::BlockingQueuedConnection); - - if (!cancelOk) - qCWarning(LogInstaller) << "PackageManager could not re-enable package" << m_packageId << "after a failed removal"; - } - - setError(e.errorCode(), e.errorString()); - } -} - -QT_END_NAMESPACE_AM diff --git a/src/installer-lib/deinstallationtask.h b/src/installer-lib/deinstallationtask.h deleted file mode 100644 index 895f75c3..00000000 --- a/src/installer-lib/deinstallationtask.h +++ /dev/null @@ -1,75 +0,0 @@ -/**************************************************************************** -** -** 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 -** -****************************************************************************/ - -#pragma once - -#include - -QT_BEGIN_NAMESPACE_AM - -class PackageInfo; -class InstallationLocation; - -class DeinstallationTask : public AsynchronousTask -{ - Q_OBJECT - -public: - DeinstallationTask(PackageInfo *package, const QString &installationPath, const QString &documentPath, - bool forceDeinstallation, bool keepDocuments, QObject *parent = nullptr); - - bool cancel() override; - -protected: - void execute() override; - -private: - PackageInfo *m_package; - QString m_installationPath; - QString m_documentPath; - bool m_forceDeinstallation; - bool m_keepDocuments; - bool m_canBeCanceled = true; - bool m_canceled = false; -}; - -QT_END_NAMESPACE_AM diff --git a/src/installer-lib/installationtask.cpp b/src/installer-lib/installationtask.cpp deleted file mode 100644 index ad017286..00000000 --- a/src/installer-lib/installationtask.cpp +++ /dev/null @@ -1,484 +0,0 @@ -/**************************************************************************** -** -** 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 -#include - -#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 to : - - Step 1 -- startInstallation() - ============================= - - delete /+ - - create dir /+ - set to /+ - - - Step 2 -- unpack files - ====================== - - PackageExtractor does its job - - - Step 3 -- finishInstallation() - ================================ - - if (exists /) - set to - - create installation report at /.installation-report.yaml - - if (not ) - create document directory - - if (optional uid separation) - chown/chmod recursively in and document directory - - - Step 3.1 -- final rename in finishInstallation() - ================================================== - - if () - rename / to /- - rename /+ to / -*/ - -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 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 diff --git a/src/installer-lib/installationtask.h b/src/installer-lib/installationtask.h deleted file mode 100644 index 6fc43f16..00000000 --- a/src/installer-lib/installationtask.h +++ /dev/null @@ -1,111 +0,0 @@ -/**************************************************************************** -** -** 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 -** -****************************************************************************/ - -#pragma once - -#include -#include -#include -#include - -#include -#include -#include - -QT_BEGIN_NAMESPACE_AM - -class PackageInfo; -class PackageManager; -class PackageExtractor; - - -class InstallationTask : public AsynchronousTask -{ - Q_OBJECT -public: - InstallationTask(const QString &installationPath, const QString &documentPath, - const QUrl &sourceUrl, QObject *parent = nullptr); - ~InstallationTask() override; - - void acknowledge(); - bool cancel() override; - -signals: - void finishedPackageExtraction(); - -protected: - void execute() override; - -private: - void startInstallation() Q_DECL_NOEXCEPT_EXPR(false); - void finishInstallation() Q_DECL_NOEXCEPT_EXPR(false); - void checkExtractedFile(const QString &file) Q_DECL_NOEXCEPT_EXPR(false); - -private: - PackageManager *m_pm; - QString m_installationPath; - QString m_documentPath; - QUrl m_sourceUrl; - bool m_foundInfo = false; - bool m_foundIcon = false; - QString m_iconFileName; - bool m_locked = false; - uint m_extractedFileCount = 0; - bool m_managerApproval = false; - QScopedPointer m_package; - uint m_applicationUid = uint(-1); - - // changes to these 4 member variables are protected by m_mutex - PackageExtractor *m_extractor = nullptr; - bool m_canceled = false; - bool m_installationAcknowledged = false; - QWaitCondition m_installationAcknowledgeWaitCondition; - - static QMutex s_serializeFinishInstallation; - - QDir m_applicationDir; - QDir m_extractionDir; - - ScopedDirectoryCreator m_installationDirCreator; -}; - -QT_END_NAMESPACE_AM diff --git a/src/installer-lib/installer-lib.pro b/src/installer-lib/installer-lib.pro deleted file mode 100644 index 0cf2695a..00000000 --- a/src/installer-lib/installer-lib.pro +++ /dev/null @@ -1,39 +0,0 @@ -TEMPLATE = lib -TARGET = QtAppManInstaller -MODULE = appman_installer - -load(am-config) - -QT = core network qml -QT_FOR_PRIVATE *= \ - appman_common-private \ - appman_crypto-private \ - appman_application-private \ - appman_package-private \ - -CONFIG *= static internal_module -CONFIG -= create_cmake - -include($$SOURCE_DIR/3rdparty/libarchive.pri) -include($$SOURCE_DIR/3rdparty/libz.pri) - -HEADERS += \ - asynchronoustask.h \ - deinstallationtask.h \ - installationtask.h \ - scopeutilities.h \ - package.h \ - packagemanager.h \ - packagemanager_p.h \ - sudo.h \ - -SOURCES += \ - asynchronoustask.cpp \ - installationtask.cpp \ - deinstallationtask.cpp \ - scopeutilities.cpp \ - packagemanager.cpp \ - package.cpp \ - sudo.cpp \ - -load(qt_module) diff --git a/src/installer-lib/package.cpp b/src/installer-lib/package.cpp deleted file mode 100644 index f9693daa..00000000 --- a/src/installer-lib/package.cpp +++ /dev/null @@ -1,193 +0,0 @@ -/**************************************************************************** -** -** 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 - -#include "package.h" -#include "packageinfo.h" - -QT_BEGIN_NAMESPACE_AM - -Package::Package(PackageInfo *packageInfo, State initialState) - : m_info(packageInfo) - , m_state(initialState) -{ } - -QString Package::id() const -{ - return info()->id(); -} - -bool Package::isBuiltIn() const -{ - return info()->isBuiltIn(); -} - -QString Package::version() const -{ - return info()->version(); -} - -QString Package::name() const -{ - QString name; - if (!info()->names().isEmpty()) { - name = info()->name(QLocale::system().name()); //TODO: language changes - if (name.isEmpty()) - name = info()->name(qSL("en")); - if (name.isEmpty()) - name = info()->name(qSL("en_US")); - if (name.isEmpty()) - name = *info()->names().constBegin(); - } else { - name = id(); - } - return name; -} - -QVariantMap Package::names() const -{ - QVariantMap names; - for (auto it = info()->names().cbegin(); it != info()->names().cend(); ++it) - names.insert(it.key(), it.value()); - return names; -} - -QString Package::description() const -{ - QString description; - if (!info()->descriptions().isEmpty()) { - description = info()->description(QLocale::system().name()); //TODO: language changes - if (description.isEmpty()) - description = info()->description(qSL("en")); - if (description.isEmpty()) - description = info()->description(qSL("en_US")); - if (description.isEmpty()) - description = *info()->descriptions().constBegin(); - } - return description; -} - -QVariantMap Package::descriptions() const -{ - QVariantMap descriptions; - for (auto it = info()->descriptions().cbegin(); it != info()->descriptions().cend(); ++it) - descriptions.insert(it.key(), it.value()); - return descriptions; -} - -QStringList Package::categories() const -{ - return info()->categories(); -} - -QUrl Package::icon() const -{ - if (info()->icon().isEmpty()) - return QUrl(); - - QDir dir; - switch (state()) { - default: - case Installed: - dir = info()->baseDir(); - break; - case BeingInstalled: - case BeingUpdated: - dir = QDir(info()->baseDir().absolutePath() + QLatin1Char('+')); - break; - case BeingRemoved: - dir = QDir(info()->baseDir().absolutePath() + QLatin1Char('-')); - break; - } - return QUrl::fromLocalFile(dir.absoluteFilePath(info()->icon())); -} - -void Package::setState(State state) -{ - if (m_state != state) { - m_state = state; - emit stateChanged(m_state); - } -} - -void Package::setProgress(qreal progress) -{ - m_progress = progress; -} - - -void Package::setBaseInfo(PackageInfo *info) -{ - m_info.reset(info); - emit bulkChange(); -} - -void Package::setUpdatedInfo(PackageInfo *info) -{ - Q_ASSERT(!info || (m_info && info->id() == m_info->id())); - - m_updatedInfo.reset(info); - emit bulkChange(); -} - -PackageInfo *Package::info() const -{ - return m_updatedInfo ? m_updatedInfo.data() : m_info.data(); -} - -PackageInfo *Package::updatedInfo() const -{ - return m_updatedInfo.data(); -} - -PackageInfo *Package::takeBaseInfo() -{ - return m_info.take(); -} - -bool Package::canBeRevertedToBuiltIn() const -{ - return m_info && m_updatedInfo; -} - -QT_END_NAMESPACE_AM diff --git a/src/installer-lib/package.h b/src/installer-lib/package.h deleted file mode 100644 index 2c134c44..00000000 --- a/src/installer-lib/package.h +++ /dev/null @@ -1,138 +0,0 @@ -/**************************************************************************** -** -** 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 -** -****************************************************************************/ - -#pragma once - -#include -#include -#include -#include - -#include - -QT_BEGIN_NAMESPACE_AM - - -class Package : public QObject -{ - Q_OBJECT - Q_CLASSINFO("AM-QmlType", "QtApplicationManager.SystemUI/PackageObject 2.0 UNCREATABLE") - Q_PROPERTY(QString id READ id CONSTANT) - Q_PROPERTY(bool builtIn READ isBuiltIn NOTIFY bulkChange) - Q_PROPERTY(QUrl icon READ icon NOTIFY bulkChange) - Q_PROPERTY(QString version READ version NOTIFY bulkChange) - Q_PROPERTY(QString name READ name NOTIFY bulkChange) - Q_PROPERTY(QVariantMap names READ names NOTIFY bulkChange) - Q_PROPERTY(QString description READ version NOTIFY bulkChange) - Q_PROPERTY(QVariantMap descriptions READ descriptions NOTIFY bulkChange) - Q_PROPERTY(QStringList categories READ categories NOTIFY bulkChange) - Q_PROPERTY(State state READ state NOTIFY stateChanged) - -public: - enum State { - Installed, - BeingInstalled, - BeingUpdated, - BeingDowngraded, - BeingRemoved - }; - Q_ENUM(State) - - Package(PackageInfo *packageInfo, State initialState = Installed); - - QString id() const; - bool isBuiltIn() const; - QUrl icon() const; - QString version() const; - QString name() const; - QVariantMap names() const; - QString description() const; - QVariantMap descriptions() const; - QStringList categories() const; - - State state() const { return m_state; } - qreal progress() const { return m_progress; } - - void setState(State state); - void setProgress(qreal progress); - - // Creates a list of Applications from a list of ApplicationInfo objects. - // Ownership of the given ApplicationInfo objects is passed to the returned Applications. - //static QVector fromApplicationInfoVector(QVector &); - - /* - All packages have a base info. - - Built-in packages, when updated, also get an updated info. - The updated info then overlays the base one. Subsequent updates - just replace the updated info. When requested to be removed, a - built-in packages only loses its updated info, returning to - expose the base one. - - Regular packages (ie, non-built-in) only have a base info. When - updated, their base info gets replaced and thus there's no way to go - back to a previous version. Regular packages get completely - removed when requested. - */ - void setBaseInfo(PackageInfo *info); - void setUpdatedInfo(PackageInfo *info); - - // Returns the updated info, if there's one. Otherwise returns the base info. - PackageInfo *info() const; - PackageInfo *updatedInfo() const; - PackageInfo *takeBaseInfo(); - - bool canBeRevertedToBuiltIn() const; - -signals: - void bulkChange(); - void stateChanged(State state); - -private: - QScopedPointer m_info; - QScopedPointer m_updatedInfo; - - State m_state = Installed; - qreal m_progress = 0; -}; - -QT_END_NAMESPACE_AM diff --git a/src/installer-lib/packagemanager.cpp b/src/installer-lib/packagemanager.cpp deleted file mode 100644 index c84fe833..00000000 --- a/src/installer-lib/packagemanager.cpp +++ /dev/null @@ -1,1193 +0,0 @@ -/**************************************************************************** -** -** 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 -#include -#include -#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" - -#if defined(Q_OS_WIN) -# include -#else -# include -# include -# if defined(Q_OS_ANDROID) -# include -# define statvfs statfs -# else -# include -# endif -#endif - - -QT_BEGIN_NAMESPACE_AM - -enum Roles -{ - Id = Qt::UserRole, - Name, - Description, - Icon, - - IsBlocked, - IsUpdating, - IsRemovable, - - UpdateProgress, - - Version, - PackageItem, -}; - -PackageManager *PackageManager::s_instance = nullptr; -QHash PackageManager::s_roleNames; - -PackageManager *PackageManager::createInstance(PackageDatabase *packageDatabase, - const QString &documentPath) -{ - if (Q_UNLIKELY(s_instance)) - qFatal("PackageManager::createInstance() was called a second time."); - - Q_ASSERT(packageDatabase); - - QScopedPointer pm(new PackageManager(packageDatabase, documentPath)); - 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 PackageManager::packages() const -{ - return d->packages; -} - -void PackageManager::registerQmlTypes() -{ - qmlRegisterSingletonType("QtApplicationManager.SystemUI", 2, 0, "PackageManager", - &PackageManager::instanceForQml); - qmlRegisterUncreatableType("QtApplicationManager.SystemUI", 2, 0, "PackageObject", - qSL("Cannot create objects of type PackageObject")); - qRegisterMetaType("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 QString &documentPath) - : QAbstractListModel() - , d(new PackageManagerPrivate()) -{ - d->database = packageDatabase; - d->installationPath = packageDatabase->installedPackagesDir(); - d->documentPath = documentPath; -} - -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 &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 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 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 PackageManager::caCertificates() const -{ - return d->chainOfTrust; -} - -void PackageManager::setCACertificates(const QList &chainOfTrust) -{ - d->chainOfTrust = chainOfTrust; -} - -static QVariantMap locationMap(const QString &path) -{ - QString cpath = QFileInfo(path).canonicalPath(); - quint64 bytesTotal = 0; - quint64 bytesFree = 0; - -#if defined(Q_OS_WIN) - GetDiskFreeSpaceExW((LPCWSTR) cpath.utf16(), (ULARGE_INTEGER *) &bytesFree, - (ULARGE_INTEGER *) &bytesTotal, nullptr); - -#else // Q_OS_UNIX - int result; - struct ::statvfs svfs; - - do { - result = ::statvfs(cpath.toLocal8Bit(), &svfs); - if (result == -1 && errno == EINTR) - continue; - } while (false); - - if (result == 0) { - bytesTotal = quint64(svfs.f_frsize) * svfs.f_blocks; - bytesFree = quint64(svfs.f_frsize) * svfs.f_bavail; - } -#endif // Q_OS_WIN - - - return QVariantMap { - { qSL("path"), path }, - { qSL("deviceSize"), bytesTotal }, - { qSL("deviceFree"), bytesFree } - }; -} - -/*! - \qmlproperty object PackageManager::installationLocation - - Returns an object describing the location under which applications are installed in detail. - - The returned object has the following members: - - \table - \header - \li \c Name - \li \c Type - \li Description - \row - \li \c path - \li \c string - \li The absolute file-system path to the base directory. - \row - \li \c deviceSize - \li \c int - \li The size of the device holding \c path in bytes. - \row - \li \c deviceFree - \li \c int - \li The amount of bytes available on the device holding \c path. - \endtable - - Returns an empty object in case the installer component is disabled. -*/ -QVariantMap PackageManager::installationLocation() const -{ - return locationMap(d->installationPath); -} - -/*! - \qmlproperty object PackageManager::documentLocation - - Returns an object describing the location under which per-user document - directories are created in detail. - - The returned object has the same members as described in PackageManager::installationLocation. -*/ -QVariantMap PackageManager::documentLocation() const -{ - return locationMap(d->documentPath); -} - -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 validPaths; - if (!d->documentPath.isEmpty()) - validPaths.insert(d->documentPath, QString()); - if (!d->installationPath.isEmpty()) - validPaths.insert(d->installationPath, QString()); - - for (Package *pkg : d->packages) { // we want to detach here! - const InstallationReport *ir = pkg->info()->installationReport(); - if (ir) { - bool valid = true; - - QString pkgDir = d->installationPath + pkg->id(); - QStringList checkDirs; - QStringList checkFiles; - - checkFiles << pkgDir + qSL("/info.yaml"); - checkFiles << pkgDir + qSL("/.installation-report.yaml"); - checkDirs << pkgDir; - checkDirs << d->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(d->installationPath, pkg->id() + qL1C('/')); - validPaths.insertMulti(d->documentPath, pkg->id() + qL1C('/')); - } else { - 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 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()); - } - } - } - } - } -} - -/*! - \qmlmethod list 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 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(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 QUrl &sourceUrl) -{ - AM_TRACE(LogInstaller, sourceUrl); - - return enqueueTask(new InstallationTask(d->installationPath, d->documentPath, sourceUrl)); -} - -/*! - \qmlmethod string PackageManager::startPackageInstallation(string sourceUrl) - - Downloads an application package from \a sourceUrl and installs it. - - 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 &sourceUrl) -{ - QUrl url(sourceUrl); - if (url.scheme().isEmpty()) - url = QUrl::fromLocalFile(sourceUrl); - return startPackageInstallation(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(task) && (task->id() == taskId)) { - static_cast(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 (package->info()->installationReport()) { - return enqueueTask(new DeinstallationTask(package->info(), d->installationPath, - d->documentPath, 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 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 { 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(task)) { - connect(static_cast(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 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 { 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 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 { 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 diff --git a/src/installer-lib/packagemanager.h b/src/installer-lib/packagemanager.h deleted file mode 100644 index 24cd0f63..00000000 --- a/src/installer-lib/packagemanager.h +++ /dev/null @@ -1,210 +0,0 @@ -/**************************************************************************** -** -** 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 -** -****************************************************************************/ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include - - -QT_FORWARD_DECLARE_CLASS(QQmlEngine) -QT_FORWARD_DECLARE_CLASS(QJSEngine) - -QT_BEGIN_NAMESPACE_AM - -class PackageDatabase; -class Package; -class PackageManagerPrivate; - -class PackageManager : public QAbstractListModel -{ - Q_OBJECT - Q_PROPERTY(int count READ count NOTIFY countChanged) - Q_CLASSINFO("D-Bus Interface", "io.qt.PackageManager") - Q_CLASSINFO("AM-QmlType", "QtApplicationManager.SystemUI/PackageManager 2.0 SINGLETON") - - // these are const on purpose - these should never change in a running system - Q_PROPERTY(bool allowInstallationOfUnsignedPackages READ allowInstallationOfUnsignedPackages CONSTANT) - Q_PROPERTY(bool developmentMode READ developmentMode CONSTANT) - Q_PROPERTY(QString hardwareId READ hardwareId CONSTANT) - - Q_PROPERTY(QVariantMap installationLocation READ installationLocation CONSTANT) - Q_PROPERTY(QVariantMap documentLocation READ documentLocation CONSTANT) - - Q_PROPERTY(bool applicationUserIdSeparation READ isApplicationUserIdSeparationEnabled) - Q_PROPERTY(uint commonApplicationGroupId READ commonApplicationGroupId) - -public: - enum CacheMode { - NoCache, - UseCache, - RecreateCache - }; - - ~PackageManager() override; - static PackageManager *createInstance(PackageDatabase *packageDatabase, - const QString &documentPath); - static PackageManager *instance(); - static QObject *instanceForQml(QQmlEngine *qmlEngine, QJSEngine *); - - QVector packages() const; - - Package *fromId(const QString &id) const; - - // the item model part - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role) const override; - QHash roleNames() const override; - - int count() const; - Q_INVOKABLE QVariantMap get(int index) const; - Q_INVOKABLE Package *package(int index) const; - Q_INVOKABLE Package *package(const QString &id) const; - Q_INVOKABLE int indexOfPackage(const QString &id) const; - - bool developmentMode() const; - void setDevelopmentMode(bool enable); - bool allowInstallationOfUnsignedPackages() const; - void setAllowInstallationOfUnsignedPackages(bool enable); - QString hardwareId() const; - void setHardwareId(const QString &hwId); -// bool securityChecksEnabled() const; -// void setSecurityChecksEnabled(bool enabled); - - bool isApplicationUserIdSeparationEnabled() const; - uint commonApplicationGroupId() const; - - bool enableApplicationUserIdSeparation(uint minUserId, uint maxUserId, uint commonGroupId); - - void setCACertificates(const QList &chainOfTrust); - - void cleanupBrokenInstallations() Q_DECL_NOEXCEPT_EXPR(false); - - QVariantMap installationLocation() const; - QVariantMap documentLocation() const; - - // Q_SCRIPTABLEs are available via both QML and D-Bus - Q_SCRIPTABLE QStringList packageIds() const; - Q_SCRIPTABLE QVariantMap get(const QString &id) const; - - Q_SCRIPTABLE qint64 installedPackageSize(const QString &packageId) const; - Q_SCRIPTABLE QVariantMap installedPackageExtraMetaData(const QString &packageId) const; - Q_SCRIPTABLE QVariantMap installedPackageExtraSignedMetaData(const QString &packageId) const; - - // all QString return values are task-ids - QString startPackageInstallation(const QUrl &sourceUrl); - Q_SCRIPTABLE QString startPackageInstallation(const QString &sourceUrl); - Q_SCRIPTABLE void acknowledgePackageInstallation(const QString &taskId); - Q_SCRIPTABLE QString removePackage(const QString &id, bool keepDocuments, bool force = false); - - Q_SCRIPTABLE AsynchronousTask::TaskState taskState(const QString &taskId) const; - Q_SCRIPTABLE QString taskPackageId(const QString &taskId) const; - Q_SCRIPTABLE QStringList activeTaskIds() const; - Q_SCRIPTABLE bool cancelTask(const QString &taskId); - - // convenience function for app-store implementations - Q_SCRIPTABLE int compareVersions(const QString &version1, const QString &version2); - Q_SCRIPTABLE bool validateDnsName(const QString &name, int minimumParts = 1); - - -signals: - Q_SCRIPTABLE void countChanged(); - - Q_SCRIPTABLE void packageAdded(const QString &id); - Q_SCRIPTABLE void packageAboutToBeRemoved(const QString &id); - Q_SCRIPTABLE void packageChanged(const QString &id, const QStringList &changedRoles); - - Q_SCRIPTABLE void taskStarted(const QString &taskId); - Q_SCRIPTABLE void taskProgressChanged(const QString &taskId, qreal progress); - Q_SCRIPTABLE void taskFinished(const QString &taskId); - Q_SCRIPTABLE void taskFailed(const QString &taskId, int errorCode, const QString &errorString); - Q_SCRIPTABLE void taskStateChanged(const QString &taskId, - QT_PREPEND_NAMESPACE_AM(AsynchronousTask::TaskState) newState); - - // installation only - Q_SCRIPTABLE void taskRequestingInstallationAcknowledge(const QString &taskId, - const QVariantMap &packageAsVariantMap, - const QVariantMap &packageExtraMetaData, - const QVariantMap &packageExtraSignedMetaData); - Q_SCRIPTABLE void taskBlockingUntilInstallationAcknowledge(const QString &taskId); - -private slots: - void executeNextTask(); - -protected: - bool startingPackageInstallation(PackageInfo *info); - bool startingPackageRemoval(const QString &id); - bool finishedPackageInstall(const QString &id); - bool canceledPackageInstall(const QString &id); - -private: - void emitDataChanged(Package *package, const QVector &roles = QVector()); - static void registerQmlTypes(); - - void triggerExecuteNextTask(); - QString enqueueTask(AsynchronousTask *task); - void handleFailure(AsynchronousTask *task); - - QList caCertificates() const; - -private: - uint findUnusedUserId() const Q_DECL_NOEXCEPT_EXPR(false); - - explicit PackageManager(PackageDatabase *packageDatabase, - const QString &documentPath); - PackageManager(const PackageManager &); - PackageManager &operator=(const PackageManager &); - static PackageManager *s_instance; - static QHash s_roleNames; - - PackageManagerPrivate *d; - - friend class InstallationTask; - friend class DeinstallationTask; -}; - -QT_END_NAMESPACE_AM diff --git a/src/installer-lib/packagemanager_p.h b/src/installer-lib/packagemanager_p.h deleted file mode 100644 index 89d5c287..00000000 --- a/src/installer-lib/packagemanager_p.h +++ /dev/null @@ -1,97 +0,0 @@ -/**************************************************************************** -** -** 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 -** -****************************************************************************/ - -#pragma once - -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -QT_BEGIN_NAMESPACE_AM - -bool removeRecursiveHelper(const QString &path); - -class PackageManagerPrivate -{ -public: - PackageDatabase *database = nullptr; - QVector packages; - - bool developmentMode = false; - bool allowInstallationOfUnsignedPackages = false; - bool userIdSeparation = false; - uint minUserId = uint(-1); - uint maxUserId = uint(-1); - uint commonGroupId = uint(-1); - - QString installationPath; - QString documentPath; - - QString error; - - QString hardwareId; - QList chainOfTrust; - - QList incomingTaskList; // incoming queue - QList installationTaskList; // installation jobs in state >= AwaitingAcknowledge - AsynchronousTask *activeTask = nullptr; // currently active - - QList allTasks() const - { - QList all = incomingTaskList; - if (!installationTaskList.isEmpty()) - all += installationTaskList; - if (activeTask) - all += activeTask; - return all; - } -}; - -QT_END_NAMESPACE_AM -// We mean it. Dummy comment since syncqt needs this also for completely private Qt modules. diff --git a/src/installer-lib/scopeutilities.cpp b/src/installer-lib/scopeutilities.cpp deleted file mode 100644 index eff1d1bc..00000000 --- a/src/installer-lib/scopeutilities.cpp +++ /dev/null @@ -1,215 +0,0 @@ -/**************************************************************************** -** -** 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 "logging.h" -#include "scopeutilities.h" -#include "packagemanager_p.h" -#include "utilities.h" - -QT_BEGIN_NAMESPACE_AM - -ScopedDirectoryCreator::ScopedDirectoryCreator() -{ } - -bool ScopedDirectoryCreator::create(const QString &path, bool removeExisting) -{ - m_path = path; - QFileInfo fi(m_path); - - if (fi.exists() && fi.isDir()) { - if (!removeExisting) - return m_created = true; - else if (!removeRecursiveHelper(m_path)) - return false; - } - //qWarning() << "CREATE" << path << fi.absolutePath() << fi.fileName(); - return m_created = QDir::root().mkpath(path); -} - -bool ScopedDirectoryCreator::take() -{ - if (m_created && !m_taken) - m_taken = true; - return m_taken; -} - -bool ScopedDirectoryCreator::destroy() -{ - if (!m_taken) { - if (m_created ) - removeRecursiveHelper(m_path); - m_taken = true; - } - return m_taken; -} - -ScopedDirectoryCreator::~ScopedDirectoryCreator() -{ - destroy(); -} - -QDir ScopedDirectoryCreator::dir() -{ - return QDir(m_path); -} - -ScopedRenamer::ScopedRenamer() -{ } - -bool ScopedRenamer::internalRename(const QDir &dir, const QString &from, const QString &to) -{ - QFileInfo fromInfo(dir.absoluteFilePath(from)); - QFileInfo toInfo(dir.absoluteFilePath(to)); - - // POSIX cannot atomically rename directories, if the destination directory exists - // and is non-empty. We need to delete it first. - // Windows on the other hand cannot do anything atomically. -#ifdef Q_OS_UNIX - if (fromInfo.isDir()) { -#else - Q_UNUSED(fromInfo) - - if (true) { -#endif - if (toInfo.exists() && !recursiveOperation(toInfo.absoluteFilePath(), safeRemove)) - return false; - } -#ifdef Q_OS_UNIX - return (::rename(fromInfo.absoluteFilePath().toLocal8Bit(), toInfo.absoluteFilePath().toLocal8Bit()) == 0); -#else - return QDir(dir).rename(from, to); -#endif -} - -bool ScopedRenamer::rename(const QString &baseName, ScopedRenamer::Modes modes) -{ - QFileInfo fi(baseName); - m_basePath.setPath(fi.absolutePath()); - m_name = fi.fileName(); - m_requested = modes; - - // convenience - bool backupRequired = (modes & NameToNameMinus); - bool backupDone = false; - - if (m_requested & NameToNameMinus) { - backupDone = internalRename(m_basePath, m_name, m_name + qL1C('-')); - if (backupDone) - m_done |= NameToNameMinus; - } - if (m_requested & NamePlusToName) { - // only try if no backup required, or it worked - if (!backupRequired || backupDone) { - if (internalRename(m_basePath, m_name + qL1C('+'), m_name)) { - m_done |= NamePlusToName; - } - else if (backupDone && !undoRename()) { - qCCritical(LogSystem) << QString::fromLatin1("failed to rename '%1+' to '%1', but also failed to rename backup '%1-' back to '%1' (in directory %2)") - .arg(m_name, m_basePath.absolutePath()); - } - } - } - return m_requested == m_done; -} - -bool ScopedRenamer::rename(const QDir &baseName, ScopedRenamer::Modes modes) -{ - return rename(baseName.absolutePath(), modes); -} - - -bool ScopedRenamer::take() -{ - if (!m_taken) - m_taken = true; - return m_taken; -} - -bool ScopedRenamer::undoRename() -{ - if (!m_taken) { - if (interalUndoRename()) { - m_taken = true; - } else { - if (m_done & NamePlusToName) { - qCCritical(LogSystem) << QString::fromLatin1("failed to undo rename from '%1+' to '%1' (in directory %2)") - .arg(m_name, m_basePath.absolutePath()); - } - if (m_done & NameToNameMinus) { - qCCritical(LogSystem) << QString::fromLatin1("failed to undo rename from '%1' to '%1-' (in directory %2)") - .arg(m_name, m_basePath.absolutePath()); - } - } - } - return m_taken; -} - -bool ScopedRenamer::interalUndoRename() -{ - if (m_done & NamePlusToName) { - if (internalRename(m_basePath, m_name, m_name + qL1C('+'))) - m_done &= ~NamePlusToName; - } - if (m_done & NameToNameMinus) { - if (internalRename(m_basePath, m_name + qL1C('-'), m_name)) - m_done &= ~NameToNameMinus; - } - - return (m_done == 0); -} - -ScopedRenamer::~ScopedRenamer() -{ - undoRename(); -} - -bool ScopedRenamer::isRenamed() const -{ - return m_requested && (m_requested == m_done); -} - -QString ScopedRenamer::baseName() const -{ - return m_basePath.absoluteFilePath(m_name); -} - -QT_END_NAMESPACE_AM diff --git a/src/installer-lib/scopeutilities.h b/src/installer-lib/scopeutilities.h deleted file mode 100644 index f3d30424..00000000 --- a/src/installer-lib/scopeutilities.h +++ /dev/null @@ -1,105 +0,0 @@ -/**************************************************************************** -** -** 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 -** -****************************************************************************/ - -#pragma once - -#include -#include -#include - -#include - -QT_BEGIN_NAMESPACE_AM - -class ScopedDirectoryCreator -{ -public: - ScopedDirectoryCreator(); - bool create(const QString &path, bool removeExisting = true); - bool take(); - bool destroy(); - ~ScopedDirectoryCreator(); - - QDir dir(); - -private: - Q_DISABLE_COPY(ScopedDirectoryCreator) - - QString m_path; - bool m_created = false; - bool m_taken = false; -}; - -class ScopedRenamer -{ -public: - enum Mode { - NameToNameMinus = 1, // create backup : foo -> foo- - NamePlusToName = 2 // replace with new: foo+ -> foo - }; - - Q_DECLARE_FLAGS(Modes, Mode) - - ScopedRenamer(); - bool rename(const QString &baseName, ScopedRenamer::Modes modes); - bool rename(const QDir &baseName, ScopedRenamer::Modes modes); - bool take(); - bool undoRename(); - ~ScopedRenamer(); - - bool isRenamed() const; - QString baseName() const; - -private: - bool interalUndoRename(); - static bool internalRename(const QDir &dir, const QString &from, const QString &to); - Q_DISABLE_COPY(ScopedRenamer) - QDir m_basePath; - QString m_name; - Modes m_requested; - Modes m_done; - bool m_taken = false; -}; - -QT_END_NAMESPACE_AM - -Q_DECLARE_OPERATORS_FOR_FLAGS(QT_PREPEND_NAMESPACE_AM(ScopedRenamer::Modes)) diff --git a/src/installer-lib/sudo.cpp b/src/installer-lib/sudo.cpp deleted file mode 100644 index a9d661fe..00000000 --- a/src/installer-lib/sudo.cpp +++ /dev/null @@ -1,495 +0,0 @@ -/**************************************************************************** -** -** 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 -#include -#include -#include -#include -#include -#include - -#include "logging.h" -#include "sudo.h" -#include "utilities.h" -#include "exception.h" -#include "global.h" - -#include - -#if defined(Q_OS_LINUX) -# include "processtitle.h" - -# include -# include -# include -# include -# include -# include -# include -# include - -// These two functions are implemented in glibc, but the header file is -// in the separate libcap-dev package. Since we want to avoid unnecessary -// dependencies, we just declare them here -extern "C" int capset(cap_user_header_t header, cap_user_data_t data); -extern "C" int capget(cap_user_header_t header, const cap_user_data_t data); - -// Support for old/broken C libraries -# if defined(_LINUX_CAPABILITY_VERSION) && !defined(_LINUX_CAPABILITY_VERSION_1) -# define _LINUX_CAPABILITY_VERSION_1 _LINUX_CAPABILITY_VERSION -# define _LINUX_CAPABILITY_U32S_1 1 -# if !defined(CAP_TO_INDEX) -# define CAP_TO_INDEX(x) ((x) >> 5) -# endif -# if !defined(CAP_TO_MASK) -# define CAP_TO_MASK(x) (1 << ((x) & 31)) -# endif -# endif -# if defined(_LINUX_CAPABILITY_VERSION_3) // use 64-bit support, if available -# define AM_CAP_VERSION _LINUX_CAPABILITY_VERSION_3 -# define AM_CAP_SIZE _LINUX_CAPABILITY_U32S_3 -# else // fallback to 32-bit support -# define AM_CAP_VERSION _LINUX_CAPABILITY_VERSION_1 -# define AM_CAP_SIZE _LINUX_CAPABILITY_U32S_1 -# endif - -// Convenient way to ignore EINTR on any system call -# define EINTR_LOOP(cmd) __extension__ ({__typeof__(cmd) res = 0; do { res = cmd; } while (res == -1 && errno == EINTR); res; }) - - -// Declared as weak symbol here, so we can check at runtime if we were compiled against libgcov -extern "C" void __gcov_init() __attribute__((weak)); - - -QT_BEGIN_NAMESPACE_AM - -static void sigHupHandler(int sig) -{ - if (sig == SIGHUP) - _exit(0); -} - -QT_END_NAMESPACE_AM - -#endif // Q_OS_LINUX - - -QT_BEGIN_NAMESPACE_AM - -void Sudo::forkServer(DropPrivileges dropPrivileges, QStringList *warnings) -{ - bool canSudo = false; - -#if defined(Q_OS_LINUX) - uid_t realUid = getuid(); - uid_t effectiveUid = geteuid(); - canSudo = (realUid == 0) || (effectiveUid == 0); -#else - Q_UNUSED(warnings) - Q_UNUSED(dropPrivileges) -#endif - - if (!canSudo) { - SudoServer::createInstance(-1); - SudoClient::createInstance(-1, SudoServer::instance()); - if (warnings) { - *warnings << qSL("For the installer to work correctly, the executable needs to be run either as root via sudo or SUID (preferred)"); - *warnings << qSL("(using fallback implementation - you might experience permission errors on installer operations)"); - } - return; - } - -#if defined(Q_OS_LINUX) - gid_t realGid = getgid(); - uid_t sudoUid = static_cast(qEnvironmentVariableIntValue("SUDO_UID")); - - // run as normal user (e.g. 1000): uid == 1000 euid == 1000 - // run with binary suid-root: uid == 1000 euid == 0 - // run with sudo (no suid-root): uid == 0 euid == 0 $SUDO_UID == 1000 - - // treat sudo as special variant of a SUID executable - if (realUid == 0 && effectiveUid == 0 && sudoUid != 0) { - realUid = sudoUid; - realGid = static_cast(qEnvironmentVariableIntValue("SUDO_GID")); - - if (setresgid(realGid, 0, 0) || setresuid(realUid, 0, 0)) - throw Exception(errno, "Could not set real user or group ID"); - } - - int socketFds[2]; - if (EINTR_LOOP(socketpair(AF_UNIX, SOCK_DGRAM, 0, socketFds)) != 0) - throw Exception(errno, "Could not create a pair of sockets"); - - // We need to make the gcda files generated by the root process writable by the normal user. - // There is no way to detect a compilation with -ftest-coverage, but we can check for gcov - // symbols at runtime. GCov will open all gcda files at fork() time, so we can get away with - // switching umasks around the fork() call. - - mode_t realUmask = 0; - if (__gcov_init) - realUmask = umask(0); - - pid_t pid = fork(); - if (pid < 0) { - throw Exception(errno, "Could not fork process"); - } else if (pid == 0) { - // child - close(0); - setsid(); - - // reset umask - if (realUmask) - umask(realUmask); - - // This call is Linux only, but it makes it so easy to detect a dying parent process. - // We would have a big problem otherwise, since the main process drops its privileges, - // which prevents it from sending SIGHUP to the child process, which still runs with - // root privileges. - prctl(PR_SET_PDEATHSIG, SIGHUP); - signal(SIGHUP, sigHupHandler); - - // Drop as many capabilities as possible, just to be on the safe side - static const quint32 neededCapabilities[] = { - CAP_SYS_ADMIN, - CAP_CHOWN, - CAP_FOWNER, - CAP_DAC_OVERRIDE - }; - - bool capSetOk = false; - __user_cap_header_struct capHeader { AM_CAP_VERSION, getpid() }; - __user_cap_data_struct capData[AM_CAP_SIZE]; - if (capget(&capHeader, capData) == 0) { - quint32 capNeeded[AM_CAP_SIZE]; - memset(&capNeeded, 0, sizeof(capNeeded)); - for (quint32 cap : neededCapabilities) { - int idx = CAP_TO_INDEX(cap); - Q_ASSERT(idx < AM_CAP_SIZE); - capNeeded[idx] |= CAP_TO_MASK(cap); - } - for (int i = 0; i < AM_CAP_SIZE; ++i) - capData[i].effective = capData[i].permitted = capData[i].inheritable = capNeeded[i]; - if (capset(&capHeader, capData) == 0) - capSetOk = true; - } - if (!capSetOk) - qCCritical(LogSystem) << "could not drop privileges in the SudoServer process -- continuing with full root privileges"; - - SudoServer::createInstance(socketFds[0]); - ProcessTitle::setTitle("%s", "sudo helper"); - SudoServer::instance()->run(); - } - // parent - - // reset umask - if (realUmask) - umask(realUmask); - - SudoClient::createInstance(socketFds[1]); - - if (realUid != effectiveUid) { - // drop all root privileges - if (dropPrivileges == DropPrivilegesPermanently) { - if (setresgid(realGid, realGid, realGid) || setresuid(realUid, realUid, realUid)) { - kill(pid, SIGKILL); - throw Exception(errno, "Could not set real user or group ID"); - } - } else { - qCCritical(LogSystem) << "\nSudo was instructed to NOT drop root privileges permanently.\nThis is dangerous and should only be used in auto-tests!\n"; - if (setresgid(realGid, realGid, 0) || setresuid(realUid, realUid, 0)) { - kill(pid, 9); - throw Exception(errno, "Could not set real user or group ID"); - } - } - } - ::atexit([]() { SudoClient::instance()->stopServer(); }); -#endif -} - -SudoInterface::SudoInterface() -{ } - -#ifdef Q_OS_LINUX -bool SudoInterface::sendMessage(int socket, const QByteArray &msg, MessageType type, const QString &errorString) -{ - QByteArray packet; - QDataStream ds(&packet, QIODevice::WriteOnly); - ds << errorString << msg; - packet.prepend((type == Request) ? "RQST" : "RPLY"); - - auto bytesWritten = EINTR_LOOP(write(socket, packet.constData(), static_cast(packet.size()))); - return bytesWritten == packet.size(); -} - - -QByteArray SudoInterface::receiveMessage(int socket, MessageType type, QString *errorString) -{ - const int headerSize = 4; - char recvBuffer[8*1024]; - auto bytesReceived = EINTR_LOOP(recv(socket, recvBuffer, sizeof(recvBuffer), 0)); - - if ((bytesReceived < headerSize) || qstrncmp(recvBuffer, (type == Request ? "RQST" : "RPLY"), 4)) { - *errorString = qL1S("failed to receive command from the SudoClient process"); - //qCCritical(LogSystem) << *errorString; - return QByteArray(); - } - - QByteArray packet(recvBuffer + headerSize, int(bytesReceived) - headerSize); - - QDataStream ds(&packet, QIODevice::ReadOnly); - QByteArray msg; - ds >> *errorString >> msg; - return msg; -} -#endif // Q_OS_LINUX - - -SudoClient *SudoClient::s_instance = nullptr; - -SudoClient *SudoClient::instance() -{ - return s_instance; -} - -bool SudoClient::isFallbackImplementation() const -{ - return m_socket < 0; -} - -SudoClient::SudoClient(int socketFd) - : m_socket(socketFd) -{ } - -SudoClient *SudoClient::createInstance(int socketFd, SudoServer *shortCircuit) -{ - if (!s_instance) { - s_instance = new SudoClient(socketFd); - s_instance->m_shortCircuit = shortCircuit; - } - return s_instance; -} - - -// this is not nice, but it prevents a lot of copy/paste errors. (the C++ variadic template version -// would be equally ugly, since it needs a friend declaration in the public header) -template R returnType(R (C::*)(Ps...)); - -#define CALL(FUNC_NAME, PARAM) \ - QByteArray msg; \ - QDataStream(&msg, QIODevice::WriteOnly) << #FUNC_NAME << PARAM; \ - QByteArray reply = call(msg); \ - QDataStream result(&reply, QIODevice::ReadOnly); \ - decltype(returnType(&SudoClient::FUNC_NAME)) r; \ - result >> r; \ - return r - -bool SudoClient::removeRecursive(const QString &fileOrDir) -{ - CALL(removeRecursive, fileOrDir); -} - -bool SudoClient::setOwnerAndPermissionsRecursive(const QString &fileOrDir, uid_t user, gid_t group, mode_t permissions) -{ - CALL(setOwnerAndPermissionsRecursive, fileOrDir << user << group << permissions); -} - -void SudoClient::stopServer() -{ -#ifdef Q_OS_LINUX - if (!m_shortCircuit && m_socket >= 0) { - QByteArray msg; - QDataStream(&msg, QIODevice::WriteOnly) << "stopServer"; - sendMessage(m_socket, msg, Request); - } -#endif -} - -QByteArray SudoClient::call(const QByteArray &msg) -{ - QMutexLocker locker(&m_mutex); - - if (m_shortCircuit) - return m_shortCircuit->receive(msg); - -#ifdef Q_OS_LINUX - if (m_socket >= 0) { - if (sendMessage(m_socket, msg, Request)) - return receiveMessage(m_socket, Reply, &m_errorString); - } -#else - Q_UNUSED(m_socket) -#endif - - //qCCritical(LogSystem) << "failed to send command to the SudoServer process"; - m_errorString = qL1S("failed to send command to the SudoServer process"); - return QByteArray(); -} - - - -SudoServer *SudoServer::s_instance = nullptr; - -SudoServer *SudoServer::instance() -{ - return s_instance; -} - -SudoServer::SudoServer(int socketFd) - : m_socket(socketFd) -{ } - -SudoServer *SudoServer::createInstance(int socketFd) -{ - if (!s_instance) - s_instance = new SudoServer(socketFd); - return s_instance; -} - -void SudoServer::run() -{ -#ifdef Q_OS_LINUX - QString dummy; - - forever { - QByteArray msg = receiveMessage(m_socket, Request, &dummy); - QByteArray reply = receive(msg); - - if (m_stop) - exit(0); - - sendMessage(m_socket, reply, Reply, m_errorString); - } -#else - Q_UNUSED(m_socket) - Q_ASSERT(false); - exit(0); -#endif -} - -QByteArray SudoServer::receive(const QByteArray &msg) -{ - QDataStream params(msg); - char *functionArray; - params >> functionArray; - QByteArray function(functionArray); - delete [] functionArray; - QByteArray reply; - QDataStream result(&reply, QIODevice::WriteOnly); - m_errorString.clear(); - - if (function == "removeRecursive") { - QString fileOrDir; - params >> fileOrDir; - result << removeRecursive(fileOrDir); - } else if (function == "setOwnerAndPermissionsRecursive") { - QString fileOrDir; - uid_t user; - gid_t group; - mode_t permissions; - params >> fileOrDir >> user >> group >> permissions; - result << setOwnerAndPermissionsRecursive(fileOrDir, user, group, permissions); - } else if (function == "stopServer") { - m_stop = true; - } else { - reply.truncate(0); - m_errorString = QString::fromLatin1("unknown function '%1' called in SudoServer").arg(qL1S(function)); - } - return reply; -} - -bool SudoServer::removeRecursive(const QString &fileOrDir) -{ - try { - if (!recursiveOperation(fileOrDir, safeRemove)) - throw Exception(errno, "could not recursively remove %1").arg(fileOrDir); - return true; - } catch (const Exception &e) { - m_errorString = e.errorString(); - return false; - } -} - -bool SudoServer::setOwnerAndPermissionsRecursive(const QString &fileOrDir, uid_t user, gid_t group, mode_t permissions) -{ -#if defined(Q_OS_LINUX) - static auto setOwnerAndPermissions = - [user, group, permissions](const QString &path, RecursiveOperationType type) -> bool { - if (type == RecursiveOperationType::EnterDirectory) - return true; - - const QByteArray localPath = path.toLocal8Bit(); - mode_t mode = permissions; - - if (type == RecursiveOperationType::LeaveDirectory) { - // set the x bit for directories, but only where it makes sense - if (mode & 06) - mode |= 01; - if (mode & 060) - mode |= 010; - if (mode & 0600) - mode |= 0100; - } - - return ((chmod(localPath, mode) == 0) && (chown(localPath, user, group) == 0)); - }; - - try { - if (!recursiveOperation(fileOrDir, setOwnerAndPermissions)) { - throw Exception(errno, "could not recursively set owner and permission on %1 to %2:%3 / %4") - .arg(fileOrDir).arg(user).arg(group).arg(permissions, 4, 8, QLatin1Char('0')); - } - } catch (const Exception &e) { - m_errorString = e.errorString(); - return false; - } -#else - Q_UNUSED(fileOrDir) - Q_UNUSED(user) - Q_UNUSED(group) - Q_UNUSED(permissions) -#endif // Q_OS_LINUX - return false; -} - -QT_END_NAMESPACE_AM diff --git a/src/installer-lib/sudo.h b/src/installer-lib/sudo.h deleted file mode 100644 index 357261e6..00000000 --- a/src/installer-lib/sudo.h +++ /dev/null @@ -1,154 +0,0 @@ -/**************************************************************************** -** -** 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 -** -****************************************************************************/ - -#pragma once - -#include -#include -#include -#include - -#ifdef Q_OS_UNIX -# include -#else -typedef uint uid_t; -typedef uint gid_t; -//typedef uint mode_t; // already typedef'ed in qplatformdefs.h -#endif - -#include - -QT_BEGIN_NAMESPACE_AM - -class Sudo -{ -public: - enum DropPrivileges { - DropPrivilegesPermanently, - DropPrivilegesRegainable, // only use this for auto-tests - }; - - static void forkServer(DropPrivileges dropPrivileges, QStringList *warnings = nullptr) Q_DECL_NOEXCEPT_EXPR(false); -}; - -class SudoInterface -{ -public: - virtual ~SudoInterface() = default; - - virtual bool removeRecursive(const QString &fileOrDir) = 0; - virtual bool setOwnerAndPermissionsRecursive(const QString &fileOrDir, uid_t user, gid_t group, mode_t permissions) = 0; - -protected: - enum MessageType { Request, Reply }; - -#ifdef Q_OS_LINUX - QByteArray receiveMessage(int socket, MessageType type, QString *errorString); - bool sendMessage(int socket, const QByteArray &msg, MessageType type, const QString &errorString = QString()); -#endif - QByteArray receive(const QByteArray &packet); - -protected: - SudoInterface(); -private: - Q_DISABLE_COPY(SudoInterface) -}; - -class SudoServer; - -class SudoClient : public SudoInterface -{ -public: - static SudoClient *createInstance(int socketFd, SudoServer *shortCircuit = 0); - - static SudoClient *instance(); - - bool isFallbackImplementation() const; - - bool removeRecursive(const QString &fileOrDir) override; - bool setOwnerAndPermissionsRecursive(const QString &fileOrDir, uid_t user, gid_t group, mode_t permissions) override; - - void stopServer(); - - QString lastError() const { return m_errorString; } - -private: - SudoClient(int socketFd); - - QByteArray call(const QByteArray &msg); - - int m_socket; - QString m_errorString; - QMutex m_mutex; - SudoServer *m_shortCircuit; - - static SudoClient *s_instance; -}; - -class SudoServer : public SudoInterface -{ -public: - static SudoServer *createInstance(int socketFd); - - static SudoServer *instance(); - - bool removeRecursive(const QString &fileOrDir) override; - bool setOwnerAndPermissionsRecursive(const QString &fileOrDir, uid_t user, gid_t group, mode_t permissions) override; - - QString lastError() const { return m_errorString; } - - Q_NORETURN void run(); - -private: - SudoServer(int socketFd); - - QByteArray receive(const QByteArray &msg); - friend class SudoClient; - - int m_socket; - QString m_errorString; - bool m_stop = false; - - static SudoServer *s_instance; -}; - -QT_END_NAMESPACE_AM diff --git a/src/main-lib/applicationinstaller.h b/src/main-lib/applicationinstaller.h index 70dcece8..e902fd21 100644 --- a/src/main-lib/applicationinstaller.h +++ b/src/main-lib/applicationinstaller.h @@ -51,9 +51,9 @@ #include #include #include -#include -#include -#include +#include +#include +#include QT_FORWARD_DECLARE_CLASS(QQmlEngine) QT_FORWARD_DECLARE_CLASS(QJSEngine) diff --git a/src/main-lib/main-lib.pro b/src/main-lib/main-lib.pro index d2933c14..721a54ce 100644 --- a/src/main-lib/main-lib.pro +++ b/src/main-lib/main-lib.pro @@ -18,7 +18,6 @@ QT *= \ appman_monitor-private \ appman_shared_main-private \ appman_intent_server-private \ - appman_installer-private \ !headless:QT *= appman_window-private !disable-external-dbus-interfaces:qtHaveModule(dbus):QT *= dbus appman_dbus-private diff --git a/src/manager-lib/application.cpp b/src/manager-lib/application.cpp index 2e489e7f..722ad753 100644 --- a/src/manager-lib/application.cpp +++ b/src/manager-lib/application.cpp @@ -284,6 +284,20 @@ Application::Application(ApplicationInfo *info, Package *package) { Q_ASSERT(info); Q_ASSERT(package); + + // handle package blocking: all apps have to be stopped and the stop state has to be reported + // back to the package + connect(package, &Package::blockedChanged, this, [this](bool blocked) { + emit blockedChanged(blocked); + if (blocked && (runState() == Am::NotRunning)) + this->package()->applicationStoppedDueToBlock(id()); + else if (blocked) + stop(true); + }); + connect(this, &Application::runStateChanged, this, [this](Am::RunState runState) { + if (isBlocked() && (runState == Am::NotRunning)) + this->package()->applicationStoppedDueToBlock(id()); + }); } bool Application::start(const QString &documentUrl) @@ -373,6 +387,11 @@ QString Application::name(const QString &language) const return package()->names().value(language).toString(); } +bool Application::isBlocked() const +{ + return package()->isBlocked(); +} + QVariantMap Application::applicationProperties() const { return info()->applicationProperties(); @@ -412,21 +431,6 @@ qreal Application::progress() const return package()->progress(); } -bool Application::isBlocked() const -{ - return m_blocked.load() == 1; -} - -bool Application::block() -{ - return m_blocked.testAndSetOrdered(0, 1); -} - -bool Application::unblock() -{ - return m_blocked.testAndSetOrdered(1, 0); -} - void Application::setRunState(Am::RunState runState) { if (runState != m_runState) { diff --git a/src/manager-lib/application.h b/src/manager-lib/application.h index 9e3bcc16..0909ce00 100644 --- a/src/manager-lib/application.h +++ b/src/manager-lib/application.h @@ -92,6 +92,7 @@ class Application : public QObject Q_PROPERTY(QString codeDir READ codeDir NOTIFY bulkChange) Q_PROPERTY(State state READ state NOTIFY stateChanged) Q_PROPERTY(QT_PREPEND_NAMESPACE_AM(Am::RunState) runState READ runState NOTIFY runStateChanged) + Q_PROPERTY(bool blocked READ isBlocked NOTIFY blockedChanged) public: enum State { // kept for compatibility ... in reality moved to class Package @@ -122,6 +123,7 @@ public: QStringList supportedMimeTypes() const; QString name() const; Q_INVOKABLE QString name(const QString &language) const; + bool isBlocked() const; // Properties that mainly forward content from ApplicationInfo QString id() const; @@ -140,9 +142,6 @@ public: State state() const; qreal progress() const; Am::RunState runState() const { return m_runState; } - bool isBlocked() const; - bool block(); - bool unblock(); int lastExitCode() const { return m_lastExitCode; } Am::ExitStatus lastExitStatus() const { return m_lastExitStatus; } @@ -159,6 +158,7 @@ signals: void activated(); void stateChanged(State state); void runStateChanged(Am::RunState state); + void blockedChanged(bool blocked); private: void setLastExitCodeAndStatus(int exitCode, Am::ExitStatus exitStatus); @@ -166,8 +166,6 @@ private: QScopedPointer m_info; Package *m_package = nullptr; AbstractRuntime *m_runtime = nullptr; - QAtomicInt m_blocked; - QAtomicInt m_mounted; Am::RunState m_runState = Am::NotRunning; diff --git a/src/manager-lib/applicationmanager.cpp b/src/manager-lib/applicationmanager.cpp index eaa8e205..0bcc9585 100644 --- a/src/manager-lib/applicationmanager.cpp +++ b/src/manager-lib/applicationmanager.cpp @@ -1126,30 +1126,6 @@ QString ApplicationManager::identifyApplication(qint64 pid) const return app ? app->id() : QString(); } -bool ApplicationManager::blockApplication(const QString &id) -{ - Application *app = fromId(id); - if (!app) - return false; - if (!app->block()) - return false; - emitDataChanged(app, QVector { IsBlocked }); - stopApplicationInternal(app, true); - emitDataChanged(app, QVector { IsRunning }); - return true; -} - -bool ApplicationManager::unblockApplication(const QString &id) -{ - Application *app = fromId(id); - if (!app) - return false; - if (!app->unblock()) - return false; - emitDataChanged(app, QVector { IsBlocked }); - return true; -} - void ApplicationManager::shutDown() { d->shuttingDown = true; @@ -1416,6 +1392,11 @@ void ApplicationManager::addApplication(Application *app) stopApplication(app->id(), forceKill); }; + connect(app, &Application::blockedChanged, + this, [this, app]() { + emitDataChanged(app, QVector { IsBlocked }); + }); + d->apps << app; } diff --git a/src/manager-lib/applicationmanager.h b/src/manager-lib/applicationmanager.h index 3e98038c..f221a91b 100644 --- a/src/manager-lib/applicationmanager.h +++ b/src/manager-lib/applicationmanager.h @@ -184,9 +184,6 @@ private slots: void openUrlRelay(const QUrl &url); void addApplication(Application *app); - bool blockApplication(const QString &id); - bool unblockApplication(const QString &id); - private: void emitDataChanged(Application *app, const QVector &roles = QVector()); void emitActivated(Application *app); diff --git a/src/manager-lib/asynchronoustask.cpp b/src/manager-lib/asynchronoustask.cpp new file mode 100644 index 00000000..168dc72f --- /dev/null +++ b/src/manager-lib/asynchronoustask.cpp @@ -0,0 +1,133 @@ +/**************************************************************************** +** +** 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 + +#include "global.h" +#include "asynchronoustask.h" + +QT_BEGIN_NAMESPACE_AM + +AsynchronousTask::AsynchronousTask(QObject *parent) + : QThread(parent) + , m_id(QUuid::createUuid().toString()) +{ + static int once = qRegisterMetaType(); + Q_UNUSED(once) +} + +QString AsynchronousTask::id() const +{ + return m_id; +} + +AsynchronousTask::TaskState AsynchronousTask::state() const +{ + return m_state; +} + +void AsynchronousTask::setState(AsynchronousTask::TaskState state) +{ + if (m_state != state) { + m_state = state; + emit stateChanged(m_state); + } +} + +bool AsynchronousTask::hasFailed() const +{ + return (m_state == Failed); +} + +Error AsynchronousTask::errorCode() const +{ + return m_errorCode; +} + +QString AsynchronousTask::errorString() const +{ + return m_errorString; +} + + +bool AsynchronousTask::cancel() +{ + return false; +} + +bool AsynchronousTask::forceCancel() +{ + if (m_state == Queued) { + setError(Error::Canceled, qSL("canceled")); + return true; + } + return cancel(); +} + +QString AsynchronousTask::packageId() const +{ + return m_packageId; +} + +bool AsynchronousTask::preExecute() +{ + return true; +} + +bool AsynchronousTask::postExecute() +{ + return true; +} + +void AsynchronousTask::setError(Error errorCode, const QString &errorString) +{ + m_errorCode = errorCode; + m_errorString = errorString; + setState(Failed); +} + +void AsynchronousTask::run() +{ + execute(); +} + +QT_END_NAMESPACE_AM diff --git a/src/manager-lib/asynchronoustask.h b/src/manager-lib/asynchronoustask.h new file mode 100644 index 00000000..977140df --- /dev/null +++ b/src/manager-lib/asynchronoustask.h @@ -0,0 +1,111 @@ +/**************************************************************************** +** +** 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 +** +****************************************************************************/ + +#pragma once + +#include +#include + +#include + +QT_BEGIN_NAMESPACE_AM + +class AsynchronousTask : public QThread +{ + Q_OBJECT + +public: + enum TaskState + { + Invalid, + Queued, + Executing, + Failed, + Finished, + + // installation task only + AwaitingAcknowledge, + Installing, + CleaningUp + }; + Q_ENUM(TaskState) + + AsynchronousTask(QObject *parent = nullptr); + + QString id() const; + + TaskState state() const; + void setState(TaskState state); + + bool hasFailed() const; + Error errorCode() const; + QString errorString() const; + + virtual bool cancel(); + bool forceCancel(); // will always work in Queued state + + QString packageId() const; // convenience + + virtual bool preExecute(); + virtual bool postExecute(); + +signals: + void stateChanged(QT_PREPEND_NAMESPACE_AM(AsynchronousTask::TaskState) newState); + void progress(qreal p); + +protected: + void setError(Error errorCode, const QString &errorString); + virtual void execute() = 0; + void run() override final; + +protected: + QMutex m_mutex; + + QString m_id; + QString m_packageId; + TaskState m_state = Queued; + Error m_errorCode = Error::None; + QString m_errorString; +}; + + +QT_END_NAMESPACE_AM diff --git a/src/manager-lib/deinstallationtask.cpp b/src/manager-lib/deinstallationtask.cpp new file mode 100644 index 00000000..09483022 --- /dev/null +++ b/src/manager-lib/deinstallationtask.cpp @@ -0,0 +1,155 @@ +/**************************************************************************** +** +** 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 "logging.h" +#include "packagemanager.h" +#include "packagemanager_p.h" +#include "installationreport.h" +#include "package.h" +#include "exception.h" +#include "scopeutilities.h" +#include "deinstallationtask.h" + +QT_BEGIN_NAMESPACE_AM + +DeinstallationTask::DeinstallationTask(Package *package, const QString &installationPath, + const QString &documentPath, bool forceDeinstallation, + bool keepDocuments, QObject *parent) + : AsynchronousTask(parent) + , m_package(package) + , m_installationPath(installationPath) + , m_documentPath(documentPath) + , m_forceDeinstallation(forceDeinstallation) + , m_keepDocuments(keepDocuments) +{ + m_packageId = m_package->id(); // in base class +} + +bool DeinstallationTask::cancel() +{ + if (m_canBeCanceled) + m_canceled = true; + return m_canceled; +} + +void DeinstallationTask::execute() +{ + // these have been checked in PackageManager::removePackage() already + Q_ASSERT(m_package); + Q_ASSERT(m_package->info()); + Q_ASSERT(m_package->info()->installationReport()); + + bool managerApproval = false; + + try { + // we need to call those PackageManager methods in the correct thread + // this will also exclusively lock the package for us + QMetaObject::invokeMethod(PackageManager::instance(), [this, &managerApproval]() + { managerApproval = PackageManager::instance()->startingPackageRemoval(m_package->id()); }, + Qt::BlockingQueuedConnection); + + if (!managerApproval) + throw Exception("PackageManager rejected the removal of package %1").arg(m_package->id()); + + // if any of the apps in the package were running before, we now need to wait until all of + // them have actually stopped + while (!m_canceled && !m_package->areAllApplicationsStoppedDueToBlock()) + QThread::msleep(30); + + // there's a small race condition here, but not doing a planned cancellation isn't harmful + m_canBeCanceled = false; + if (m_canceled) + throw Exception(Error::Canceled, "canceled"); + + ScopedRenamer docDirRename; + ScopedRenamer appDirRename; + + if (!m_keepDocuments) { + if (!docDirRename.rename(QDir(m_documentPath).absoluteFilePath(m_package->id()), + ScopedRenamer::NameToNameMinus)) { + throw Exception(Error::IO, "could not rename %1 to %1-").arg(docDirRename.baseName()); + } + } + + if (!appDirRename.rename(QDir(m_installationPath).absoluteFilePath(m_package->id()), + ScopedRenamer::NameToNameMinus)) { + throw Exception(Error::IO, "could not rename %1 to %1-").arg(appDirRename.baseName()); + } + + docDirRename.take(); + appDirRename.take(); + + // point of no return + + for (ScopedRenamer *toDelete : { &docDirRename, &appDirRename }) { + if (toDelete->isRenamed()) { + if (!removeRecursiveHelper(toDelete->baseName() + qL1C('-'))) + qCCritical(LogInstaller) << "ERROR: could not remove" << (toDelete->baseName() + qL1C('-')); + } + } + + // 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_package->id()); }, + Qt::BlockingQueuedConnection); + + if (!finishOk) + qCWarning(LogInstaller) << "PackageManager did not approve deinstallation of " << m_packageId; + + } catch (const Exception &e) { + // we need to call those ApplicationManager methods in the correct thread + if (managerApproval) { + bool cancelOk = false; + QMetaObject::invokeMethod(PackageManager::instance(), [this, &cancelOk]() + { cancelOk = PackageManager::instance()->canceledPackageInstall(m_package->id()); }, + Qt::BlockingQueuedConnection); + + if (!cancelOk) + qCWarning(LogInstaller) << "PackageManager could not re-enable package" << m_packageId << "after a failed removal"; + } + + setError(e.errorCode(), e.errorString()); + } +} + +QT_END_NAMESPACE_AM diff --git a/src/manager-lib/deinstallationtask.h b/src/manager-lib/deinstallationtask.h new file mode 100644 index 00000000..9161d1ba --- /dev/null +++ b/src/manager-lib/deinstallationtask.h @@ -0,0 +1,75 @@ +/**************************************************************************** +** +** 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 +** +****************************************************************************/ + +#pragma once + +#include + +QT_BEGIN_NAMESPACE_AM + +class Package; +class InstallationLocation; + +class DeinstallationTask : public AsynchronousTask +{ + Q_OBJECT + +public: + DeinstallationTask(Package *package, const QString &installationPath, const QString &documentPath, + bool forceDeinstallation, bool keepDocuments, QObject *parent = nullptr); + + bool cancel() override; + +protected: + void execute() override; + +private: + Package *m_package; + QString m_installationPath; + QString m_documentPath; + bool m_forceDeinstallation; + bool m_keepDocuments; + bool m_canBeCanceled = true; + bool m_canceled = false; +}; + +QT_END_NAMESPACE_AM 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 +#include + +#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 to : + + Step 1 -- startInstallation() + ============================= + + delete /+ + + create dir /+ + set to /+ + + + Step 2 -- unpack files + ====================== + + PackageExtractor does its job + + + Step 3 -- finishInstallation() + ================================ + + if (exists /) + set to + + create installation report at /.installation-report.yaml + + if (not ) + create document directory + + if (optional uid separation) + chown/chmod recursively in and document directory + + + Step 3.1 -- final rename in finishInstallation() + ================================================== + + if () + rename / to /- + rename /+ to / +*/ + +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 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 diff --git a/src/manager-lib/installationtask.h b/src/manager-lib/installationtask.h new file mode 100644 index 00000000..5ab947fe --- /dev/null +++ b/src/manager-lib/installationtask.h @@ -0,0 +1,111 @@ +/**************************************************************************** +** +** 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 +** +****************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +QT_BEGIN_NAMESPACE_AM + +class PackageInfo; +class PackageManager; +class PackageExtractor; + + +class InstallationTask : public AsynchronousTask +{ + Q_OBJECT +public: + InstallationTask(const QString &installationPath, const QString &documentPath, + const QUrl &sourceUrl, QObject *parent = nullptr); + ~InstallationTask() override; + + void acknowledge(); + bool cancel() override; + +signals: + void finishedPackageExtraction(); + +protected: + void execute() override; + +private: + void startInstallation() Q_DECL_NOEXCEPT_EXPR(false); + void finishInstallation() Q_DECL_NOEXCEPT_EXPR(false); + void checkExtractedFile(const QString &file) Q_DECL_NOEXCEPT_EXPR(false); + +private: + PackageManager *m_pm; + QString m_installationPath; + QString m_documentPath; + QUrl m_sourceUrl; + bool m_foundInfo = false; + bool m_foundIcon = false; + QString m_iconFileName; + bool m_locked = false; + uint m_extractedFileCount = 0; + bool m_managerApproval = false; + QScopedPointer m_package; + uint m_applicationUid = uint(-1); + + // changes to these 4 member variables are protected by m_mutex + PackageExtractor *m_extractor = nullptr; + bool m_canceled = false; + bool m_installationAcknowledged = false; + QWaitCondition m_installationAcknowledgeWaitCondition; + + static QMutex s_serializeFinishInstallation; + + QDir m_applicationDir; + QDir m_extractionDir; + + ScopedDirectoryCreator m_installationDirCreator; +}; + +QT_END_NAMESPACE_AM diff --git a/src/manager-lib/manager-lib.pro b/src/manager-lib/manager-lib.pro index 9ae1ee90..71fad983 100644 --- a/src/manager-lib/manager-lib.pro +++ b/src/manager-lib/manager-lib.pro @@ -13,7 +13,6 @@ QT_FOR_PRIVATE *= \ appman_plugininterfaces-private \ appman_intent_server-private \ appman_intent_client-private \ - appman_installer-private \ appman_monitor-private \ CONFIG *= static internal_module @@ -57,6 +56,9 @@ HEADERS += \ amnamespace.h \ intentaminterface.h \ processstatus.h \ + package.h \ + packagemanager.h \ + packagemanager_p.h \ !headless:HEADERS += \ qmlinprocessapplicationmanagerwindow.h \ @@ -82,6 +84,8 @@ SOURCES += \ debugwrapper.cpp \ intentaminterface.cpp \ processstatus.cpp \ + packagemanager.cpp \ + package.cpp \ !headless:SOURCES += \ qmlinprocessapplicationmanagerwindow.cpp \ @@ -95,4 +99,26 @@ qtHaveModule(qml):SOURCES += \ # compile the moc-data into the exporting binary (appman itself) HEADERS += ../plugin-interfaces/containerinterface.h + +!disable-installer { + + QT_FOR_PRIVATE *= \ + appman_package-private \ + appman_crypto-private \ + + HEADERS += \ + asynchronoustask.h \ + deinstallationtask.h \ + installationtask.h \ + scopeutilities.h \ + sudo.h \ + + SOURCES += \ + asynchronoustask.cpp \ + installationtask.cpp \ + deinstallationtask.cpp \ + scopeutilities.cpp \ + sudo.cpp \ +} + load(qt_module) diff --git a/src/manager-lib/package.cpp b/src/manager-lib/package.cpp new file mode 100644 index 00000000..5f414488 --- /dev/null +++ b/src/manager-lib/package.cpp @@ -0,0 +1,237 @@ +/**************************************************************************** +** +** 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 + +#include "package.h" +#include "packageinfo.h" +#include "applicationinfo.h" + +QT_BEGIN_NAMESPACE_AM + +Package::Package(PackageInfo *packageInfo, State initialState) + : m_info(packageInfo) + , m_state(initialState) +{ } + +QString Package::id() const +{ + return info()->id(); +} + +bool Package::isBuiltIn() const +{ + return info()->isBuiltIn(); +} + +QString Package::version() const +{ + return info()->version(); +} + +QString Package::name() const +{ + QString name; + if (!info()->names().isEmpty()) { + name = info()->name(QLocale::system().name()); //TODO: language changes + if (name.isEmpty()) + name = info()->name(qSL("en")); + if (name.isEmpty()) + name = info()->name(qSL("en_US")); + if (name.isEmpty()) + name = *info()->names().constBegin(); + } else { + name = id(); + } + return name; +} + +QVariantMap Package::names() const +{ + QVariantMap names; + for (auto it = info()->names().cbegin(); it != info()->names().cend(); ++it) + names.insert(it.key(), it.value()); + return names; +} + +QString Package::description() const +{ + QString description; + if (!info()->descriptions().isEmpty()) { + description = info()->description(QLocale::system().name()); //TODO: language changes + if (description.isEmpty()) + description = info()->description(qSL("en")); + if (description.isEmpty()) + description = info()->description(qSL("en_US")); + if (description.isEmpty()) + description = *info()->descriptions().constBegin(); + } + return description; +} + +QVariantMap Package::descriptions() const +{ + QVariantMap descriptions; + for (auto it = info()->descriptions().cbegin(); it != info()->descriptions().cend(); ++it) + descriptions.insert(it.key(), it.value()); + return descriptions; +} + +QStringList Package::categories() const +{ + return info()->categories(); +} + +QUrl Package::icon() const +{ + if (info()->icon().isEmpty()) + return QUrl(); + + QDir dir; + switch (state()) { + default: + case Installed: + dir = info()->baseDir(); + break; + case BeingInstalled: + case BeingUpdated: + dir = QDir(info()->baseDir().absolutePath() + QLatin1Char('+')); + break; + case BeingRemoved: + dir = QDir(info()->baseDir().absolutePath() + QLatin1Char('-')); + break; + } + return QUrl::fromLocalFile(dir.absoluteFilePath(info()->icon())); +} + +void Package::setState(State state) +{ + if (m_state != state) { + m_state = state; + emit stateChanged(m_state); + } +} + +void Package::setProgress(qreal progress) +{ + m_progress = progress; +} + + +void Package::setBaseInfo(PackageInfo *info) +{ + m_info.reset(info); + emit bulkChange(); +} + +void Package::setUpdatedInfo(PackageInfo *info) +{ + Q_ASSERT(!info || (m_info && info->id() == m_info->id())); + + m_updatedInfo.reset(info); + emit bulkChange(); +} + +PackageInfo *Package::info() const +{ + return m_updatedInfo ? m_updatedInfo.data() : m_info.data(); +} + +PackageInfo *Package::updatedInfo() const +{ + return m_updatedInfo.data(); +} + +PackageInfo *Package::takeBaseInfo() +{ + return m_info.take(); +} + +bool Package::canBeRevertedToBuiltIn() const +{ + return m_info && m_updatedInfo; +} + +bool Package::isBlocked() const +{ + return m_blocked > 0; +} + +bool Package::block() +{ + bool blockedNow = (m_blocked.fetchAndAddOrdered(1) == 0); + if (blockedNow) { + emit blockedChanged(true); + m_blockedApps = info()->applications(); + } + return blockedNow; +} + +bool Package::unblock() +{ + bool unblockedNow = (m_blocked.fetchAndSubOrdered(1) == 1); + if (unblockedNow) { + m_blockedApps.clear(); + emit blockedChanged(false); + } + return unblockedNow; + +} + +void Package::applicationStoppedDueToBlock(const QString &appId) +{ + if (!isBlocked()) + return; + + auto it = std::find_if(m_blockedApps.cbegin(), m_blockedApps.cend(), [appId](const ApplicationInfo *appInfo) { + return appInfo->id() == appId; + }); + if (it != m_blockedApps.cend()) + m_blockedApps.removeOne(*it); +} + +bool Package::areAllApplicationsStoppedDueToBlock() const +{ + return isBlocked() && m_blockedApps.isEmpty(); +} + +QT_END_NAMESPACE_AM diff --git a/src/manager-lib/package.h b/src/manager-lib/package.h new file mode 100644 index 00000000..7932f426 --- /dev/null +++ b/src/manager-lib/package.h @@ -0,0 +1,151 @@ +/**************************************************************************** +** +** 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 +** +****************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE_AM + + +class Package : public QObject +{ + Q_OBJECT + Q_CLASSINFO("AM-QmlType", "QtApplicationManager.SystemUI/PackageObject 2.0 UNCREATABLE") + Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(bool builtIn READ isBuiltIn NOTIFY bulkChange) + Q_PROPERTY(QUrl icon READ icon NOTIFY bulkChange) + Q_PROPERTY(QString version READ version NOTIFY bulkChange) + Q_PROPERTY(QString name READ name NOTIFY bulkChange) + Q_PROPERTY(QVariantMap names READ names NOTIFY bulkChange) + Q_PROPERTY(QString description READ version NOTIFY bulkChange) + Q_PROPERTY(QVariantMap descriptions READ descriptions NOTIFY bulkChange) + Q_PROPERTY(QStringList categories READ categories NOTIFY bulkChange) + Q_PROPERTY(State state READ state NOTIFY stateChanged) + Q_PROPERTY(bool blocked READ isBlocked NOTIFY blockedChanged) + +public: + enum State { + Installed, + BeingInstalled, + BeingUpdated, + BeingDowngraded, + BeingRemoved + }; + Q_ENUM(State) + + Package(PackageInfo *packageInfo, State initialState = Installed); + + QString id() const; + bool isBuiltIn() const; + QUrl icon() const; + QString version() const; + QString name() const; + QVariantMap names() const; + QString description() const; + QVariantMap descriptions() const; + QStringList categories() const; + + State state() const { return m_state; } + qreal progress() const { return m_progress; } + + void setState(State state); + void setProgress(qreal progress); + + // Creates a list of Applications from a list of ApplicationInfo objects. + // Ownership of the given ApplicationInfo objects is passed to the returned Applications. + //static QVector fromApplicationInfoVector(QVector &); + + /* + All packages have a base info. + + Built-in packages, when updated, also get an updated info. + The updated info then overlays the base one. Subsequent updates + just replace the updated info. When requested to be removed, a + built-in packages only loses its updated info, returning to + expose the base one. + + Regular packages (ie, non-built-in) only have a base info. When + updated, their base info gets replaced and thus there's no way to go + back to a previous version. Regular packages get completely + removed when requested. + */ + void setBaseInfo(PackageInfo *info); + void setUpdatedInfo(PackageInfo *info); + + // Returns the updated info, if there's one. Otherwise returns the base info. + PackageInfo *info() const; + PackageInfo *updatedInfo() const; + PackageInfo *takeBaseInfo(); + + bool canBeRevertedToBuiltIn() const; + + bool isBlocked() const; + bool block(); + bool unblock(); + + // function for Application to report it has stopped after getting a block request + void applicationStoppedDueToBlock(const QString &appId); + // query function for the installer to verify that it is safe to manipulate binaries + bool areAllApplicationsStoppedDueToBlock() const; + +signals: + void bulkChange(); + void stateChanged(State state); + void blockedChanged(bool blocked); + +private: + QScopedPointer m_info; + QScopedPointer m_updatedInfo; + + State m_state = Installed; + qreal m_progress = 0; + QAtomicInt m_blocked; + QVector m_blockedApps; +}; + +QT_END_NAMESPACE_AM diff --git a/src/manager-lib/packagemanager.cpp b/src/manager-lib/packagemanager.cpp new file mode 100644 index 00000000..36cd5d07 --- /dev/null +++ b/src/manager-lib/packagemanager.cpp @@ -0,0 +1,1192 @@ +/**************************************************************************** +** +** 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 +#include +#include +#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" + +#if defined(Q_OS_WIN) +# include +#else +# include +# include +# if defined(Q_OS_ANDROID) +# include +# define statvfs statfs +# else +# include +# endif +#endif + + +QT_BEGIN_NAMESPACE_AM + +enum Roles +{ + Id = Qt::UserRole, + Name, + Description, + Icon, + + IsBlocked, + IsUpdating, + IsRemovable, + + UpdateProgress, + + Version, + PackageItem, +}; + +PackageManager *PackageManager::s_instance = nullptr; +QHash PackageManager::s_roleNames; + +PackageManager *PackageManager::createInstance(PackageDatabase *packageDatabase, + const QString &documentPath) +{ + if (Q_UNLIKELY(s_instance)) + qFatal("PackageManager::createInstance() was called a second time."); + + Q_ASSERT(packageDatabase); + + QScopedPointer pm(new PackageManager(packageDatabase, documentPath)); + 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 PackageManager::packages() const +{ + return d->packages; +} + +void PackageManager::registerQmlTypes() +{ + qmlRegisterSingletonType("QtApplicationManager.SystemUI", 2, 0, "PackageManager", + &PackageManager::instanceForQml); + qmlRegisterUncreatableType("QtApplicationManager.SystemUI", 2, 0, "PackageObject", + qSL("Cannot create objects of type PackageObject")); + qRegisterMetaType("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 QString &documentPath) + : QAbstractListModel() + , d(new PackageManagerPrivate()) +{ + d->database = packageDatabase; + d->installationPath = packageDatabase->installedPackagesDir(); + d->documentPath = documentPath; +} + +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 &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 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 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 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 PackageManager::caCertificates() const +{ + return d->chainOfTrust; +} + +void PackageManager::setCACertificates(const QList &chainOfTrust) +{ + d->chainOfTrust = chainOfTrust; +} + +static QVariantMap locationMap(const QString &path) +{ + QString cpath = QFileInfo(path).canonicalPath(); + quint64 bytesTotal = 0; + quint64 bytesFree = 0; + +#if defined(Q_OS_WIN) + GetDiskFreeSpaceExW((LPCWSTR) cpath.utf16(), (ULARGE_INTEGER *) &bytesFree, + (ULARGE_INTEGER *) &bytesTotal, nullptr); + +#else // Q_OS_UNIX + int result; + struct ::statvfs svfs; + + do { + result = ::statvfs(cpath.toLocal8Bit(), &svfs); + if (result == -1 && errno == EINTR) + continue; + } while (false); + + if (result == 0) { + bytesTotal = quint64(svfs.f_frsize) * svfs.f_blocks; + bytesFree = quint64(svfs.f_frsize) * svfs.f_bavail; + } +#endif // Q_OS_WIN + + + return QVariantMap { + { qSL("path"), path }, + { qSL("deviceSize"), bytesTotal }, + { qSL("deviceFree"), bytesFree } + }; +} + +/*! + \qmlproperty object PackageManager::installationLocation + + Returns an object describing the location under which applications are installed in detail. + + The returned object has the following members: + + \table + \header + \li \c Name + \li \c Type + \li Description + \row + \li \c path + \li \c string + \li The absolute file-system path to the base directory. + \row + \li \c deviceSize + \li \c int + \li The size of the device holding \c path in bytes. + \row + \li \c deviceFree + \li \c int + \li The amount of bytes available on the device holding \c path. + \endtable + + Returns an empty object in case the installer component is disabled. +*/ +QVariantMap PackageManager::installationLocation() const +{ + return locationMap(d->installationPath); +} + +/*! + \qmlproperty object PackageManager::documentLocation + + Returns an object describing the location under which per-user document + directories are created in detail. + + The returned object has the same members as described in PackageManager::installationLocation. +*/ +QVariantMap PackageManager::documentLocation() const +{ + return locationMap(d->documentPath); +} + +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 validPaths; + if (!d->documentPath.isEmpty()) + validPaths.insert(d->documentPath, QString()); + if (!d->installationPath.isEmpty()) + validPaths.insert(d->installationPath, QString()); + + for (Package *pkg : d->packages) { // we want to detach here! + const InstallationReport *ir = pkg->info()->installationReport(); + if (ir) { + bool valid = true; + + QString pkgDir = d->installationPath + pkg->id(); + QStringList checkDirs; + QStringList checkFiles; + + checkFiles << pkgDir + qSL("/info.yaml"); + checkFiles << pkgDir + qSL("/.installation-report.yaml"); + checkDirs << pkgDir; + checkDirs << d->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(d->installationPath, pkg->id() + qL1C('/')); + validPaths.insertMulti(d->documentPath, pkg->id() + qL1C('/')); + } else { + 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 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()); + } + } + } + } + } +} + +/*! + \qmlmethod list 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 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(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::installedPackageExtraSignedMetaData(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 QUrl &sourceUrl) +{ + AM_TRACE(LogInstaller, sourceUrl); + + return enqueueTask(new InstallationTask(d->installationPath, d->documentPath, sourceUrl)); +} + +/*! + \qmlmethod string PackageManager::startPackageInstallation(string sourceUrl) + + Downloads an application package from \a sourceUrl and installs it. + + 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 &sourceUrl) +{ + QUrl url(sourceUrl); + if (url.scheme().isEmpty()) + url = QUrl::fromLocalFile(sourceUrl); + return startPackageInstallation(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(task) && (task->id() == taskId)) { + static_cast(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 (package->info()->installationReport()) { + return enqueueTask(new DeinstallationTask(package, d->installationPath, + d->documentPath, 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 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 { 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(task)) { + connect(static_cast(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 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 (!package->block()) + 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); + + Q_ASSERT(package->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() || (package->state() != Package::Installed)) + return false; + + if (package->isBuiltIn() && !package->canBeRevertedToBuiltIn()) + return false; + + if (!package->block()) // this will implicitly stop all apps in this package (asynchronously) + return false; + + package->setState(package->canBeRevertedToBuiltIn() ? Package::BeingDowngraded + : Package::BeingRemoved); + + package->setProgress(0); + emitDataChanged(package, QVector { 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 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); + + package->unblock(); + 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 { IsUpdating }); + + package->unblock(); + 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 diff --git a/src/manager-lib/packagemanager.h b/src/manager-lib/packagemanager.h new file mode 100644 index 00000000..9d4419de --- /dev/null +++ b/src/manager-lib/packagemanager.h @@ -0,0 +1,210 @@ +/**************************************************************************** +** +** 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 +** +****************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + + +QT_FORWARD_DECLARE_CLASS(QQmlEngine) +QT_FORWARD_DECLARE_CLASS(QJSEngine) + +QT_BEGIN_NAMESPACE_AM + +class PackageDatabase; +class Package; +class PackageManagerPrivate; + +class PackageManager : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_CLASSINFO("D-Bus Interface", "io.qt.PackageManager") + Q_CLASSINFO("AM-QmlType", "QtApplicationManager.SystemUI/PackageManager 2.0 SINGLETON") + + // these are const on purpose - these should never change in a running system + Q_PROPERTY(bool allowInstallationOfUnsignedPackages READ allowInstallationOfUnsignedPackages CONSTANT) + Q_PROPERTY(bool developmentMode READ developmentMode CONSTANT) + Q_PROPERTY(QString hardwareId READ hardwareId CONSTANT) + + Q_PROPERTY(QVariantMap installationLocation READ installationLocation CONSTANT) + Q_PROPERTY(QVariantMap documentLocation READ documentLocation CONSTANT) + + Q_PROPERTY(bool applicationUserIdSeparation READ isApplicationUserIdSeparationEnabled) + Q_PROPERTY(uint commonApplicationGroupId READ commonApplicationGroupId) + +public: + enum CacheMode { + NoCache, + UseCache, + RecreateCache + }; + + ~PackageManager() override; + static PackageManager *createInstance(PackageDatabase *packageDatabase, + const QString &documentPath); + static PackageManager *instance(); + static QObject *instanceForQml(QQmlEngine *qmlEngine, QJSEngine *); + + QVector packages() const; + + Package *fromId(const QString &id) const; + + // the item model part + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + int count() const; + Q_INVOKABLE QVariantMap get(int index) const; + Q_INVOKABLE Package *package(int index) const; + Q_INVOKABLE Package *package(const QString &id) const; + Q_INVOKABLE int indexOfPackage(const QString &id) const; + + bool developmentMode() const; + void setDevelopmentMode(bool enable); + bool allowInstallationOfUnsignedPackages() const; + void setAllowInstallationOfUnsignedPackages(bool enable); + QString hardwareId() const; + void setHardwareId(const QString &hwId); +// bool securityChecksEnabled() const; +// void setSecurityChecksEnabled(bool enabled); + + bool isApplicationUserIdSeparationEnabled() const; + uint commonApplicationGroupId() const; + + bool enableApplicationUserIdSeparation(uint minUserId, uint maxUserId, uint commonGroupId); + + void setCACertificates(const QList &chainOfTrust); + + void cleanupBrokenInstallations() Q_DECL_NOEXCEPT_EXPR(false); + + QVariantMap installationLocation() const; + QVariantMap documentLocation() const; + + // Q_SCRIPTABLEs are available via both QML and D-Bus + Q_SCRIPTABLE QStringList packageIds() const; + Q_SCRIPTABLE QVariantMap get(const QString &id) const; + + Q_SCRIPTABLE qint64 installedPackageSize(const QString &packageId) const; + Q_SCRIPTABLE QVariantMap installedPackageExtraMetaData(const QString &packageId) const; + Q_SCRIPTABLE QVariantMap installedPackageExtraSignedMetaData(const QString &packageId) const; + + // all QString return values are task-ids + QString startPackageInstallation(const QUrl &sourceUrl); + Q_SCRIPTABLE QString startPackageInstallation(const QString &sourceUrl); + Q_SCRIPTABLE void acknowledgePackageInstallation(const QString &taskId); + Q_SCRIPTABLE QString removePackage(const QString &id, bool keepDocuments, bool force = false); + + Q_SCRIPTABLE AsynchronousTask::TaskState taskState(const QString &taskId) const; + Q_SCRIPTABLE QString taskPackageId(const QString &taskId) const; + Q_SCRIPTABLE QStringList activeTaskIds() const; + Q_SCRIPTABLE bool cancelTask(const QString &taskId); + + // convenience function for app-store implementations + Q_SCRIPTABLE int compareVersions(const QString &version1, const QString &version2); + Q_SCRIPTABLE bool validateDnsName(const QString &name, int minimumParts = 1); + + +signals: + Q_SCRIPTABLE void countChanged(); + + Q_SCRIPTABLE void packageAdded(const QString &id); + Q_SCRIPTABLE void packageAboutToBeRemoved(const QString &id); + Q_SCRIPTABLE void packageChanged(const QString &id, const QStringList &changedRoles); + + Q_SCRIPTABLE void taskStarted(const QString &taskId); + Q_SCRIPTABLE void taskProgressChanged(const QString &taskId, qreal progress); + Q_SCRIPTABLE void taskFinished(const QString &taskId); + Q_SCRIPTABLE void taskFailed(const QString &taskId, int errorCode, const QString &errorString); + Q_SCRIPTABLE void taskStateChanged(const QString &taskId, + QT_PREPEND_NAMESPACE_AM(AsynchronousTask::TaskState) newState); + + // installation only + Q_SCRIPTABLE void taskRequestingInstallationAcknowledge(const QString &taskId, + const QVariantMap &packageAsVariantMap, + const QVariantMap &packageExtraMetaData, + const QVariantMap &packageExtraSignedMetaData); + Q_SCRIPTABLE void taskBlockingUntilInstallationAcknowledge(const QString &taskId); + +private slots: + void executeNextTask(); + +protected: + bool startingPackageInstallation(PackageInfo *info); + bool startingPackageRemoval(const QString &id); + bool finishedPackageInstall(const QString &id); + bool canceledPackageInstall(const QString &id); + +private: + void emitDataChanged(Package *package, const QVector &roles = QVector()); + static void registerQmlTypes(); + + void triggerExecuteNextTask(); + QString enqueueTask(AsynchronousTask *task); + void handleFailure(AsynchronousTask *task); + + QList caCertificates() const; + +private: + uint findUnusedUserId() const Q_DECL_NOEXCEPT_EXPR(false); + + explicit PackageManager(PackageDatabase *packageDatabase, + const QString &documentPath); + PackageManager(const PackageManager &); + PackageManager &operator=(const PackageManager &); + static PackageManager *s_instance; + static QHash s_roleNames; + + PackageManagerPrivate *d; + + friend class InstallationTask; + friend class DeinstallationTask; +}; + +QT_END_NAMESPACE_AM diff --git a/src/manager-lib/packagemanager_p.h b/src/manager-lib/packagemanager_p.h new file mode 100644 index 00000000..8c916db2 --- /dev/null +++ b/src/manager-lib/packagemanager_p.h @@ -0,0 +1,97 @@ +/**************************************************************************** +** +** 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 +** +****************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE_AM + +bool removeRecursiveHelper(const QString &path); + +class PackageManagerPrivate +{ +public: + PackageDatabase *database = nullptr; + QVector packages; + + bool developmentMode = false; + bool allowInstallationOfUnsignedPackages = false; + bool userIdSeparation = false; + uint minUserId = uint(-1); + uint maxUserId = uint(-1); + uint commonGroupId = uint(-1); + + QString installationPath; + QString documentPath; + + QString error; + + QString hardwareId; + QList chainOfTrust; + + QList incomingTaskList; // incoming queue + QList installationTaskList; // installation jobs in state >= AwaitingAcknowledge + AsynchronousTask *activeTask = nullptr; // currently active + + QList allTasks() const + { + QList all = incomingTaskList; + if (!installationTaskList.isEmpty()) + all += installationTaskList; + if (activeTask) + all += activeTask; + return all; + } +}; + +QT_END_NAMESPACE_AM +// We mean it. Dummy comment since syncqt needs this also for completely private Qt modules. diff --git a/src/manager-lib/scopeutilities.cpp b/src/manager-lib/scopeutilities.cpp new file mode 100644 index 00000000..eff1d1bc --- /dev/null +++ b/src/manager-lib/scopeutilities.cpp @@ -0,0 +1,215 @@ +/**************************************************************************** +** +** 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 "logging.h" +#include "scopeutilities.h" +#include "packagemanager_p.h" +#include "utilities.h" + +QT_BEGIN_NAMESPACE_AM + +ScopedDirectoryCreator::ScopedDirectoryCreator() +{ } + +bool ScopedDirectoryCreator::create(const QString &path, bool removeExisting) +{ + m_path = path; + QFileInfo fi(m_path); + + if (fi.exists() && fi.isDir()) { + if (!removeExisting) + return m_created = true; + else if (!removeRecursiveHelper(m_path)) + return false; + } + //qWarning() << "CREATE" << path << fi.absolutePath() << fi.fileName(); + return m_created = QDir::root().mkpath(path); +} + +bool ScopedDirectoryCreator::take() +{ + if (m_created && !m_taken) + m_taken = true; + return m_taken; +} + +bool ScopedDirectoryCreator::destroy() +{ + if (!m_taken) { + if (m_created ) + removeRecursiveHelper(m_path); + m_taken = true; + } + return m_taken; +} + +ScopedDirectoryCreator::~ScopedDirectoryCreator() +{ + destroy(); +} + +QDir ScopedDirectoryCreator::dir() +{ + return QDir(m_path); +} + +ScopedRenamer::ScopedRenamer() +{ } + +bool ScopedRenamer::internalRename(const QDir &dir, const QString &from, const QString &to) +{ + QFileInfo fromInfo(dir.absoluteFilePath(from)); + QFileInfo toInfo(dir.absoluteFilePath(to)); + + // POSIX cannot atomically rename directories, if the destination directory exists + // and is non-empty. We need to delete it first. + // Windows on the other hand cannot do anything atomically. +#ifdef Q_OS_UNIX + if (fromInfo.isDir()) { +#else + Q_UNUSED(fromInfo) + + if (true) { +#endif + if (toInfo.exists() && !recursiveOperation(toInfo.absoluteFilePath(), safeRemove)) + return false; + } +#ifdef Q_OS_UNIX + return (::rename(fromInfo.absoluteFilePath().toLocal8Bit(), toInfo.absoluteFilePath().toLocal8Bit()) == 0); +#else + return QDir(dir).rename(from, to); +#endif +} + +bool ScopedRenamer::rename(const QString &baseName, ScopedRenamer::Modes modes) +{ + QFileInfo fi(baseName); + m_basePath.setPath(fi.absolutePath()); + m_name = fi.fileName(); + m_requested = modes; + + // convenience + bool backupRequired = (modes & NameToNameMinus); + bool backupDone = false; + + if (m_requested & NameToNameMinus) { + backupDone = internalRename(m_basePath, m_name, m_name + qL1C('-')); + if (backupDone) + m_done |= NameToNameMinus; + } + if (m_requested & NamePlusToName) { + // only try if no backup required, or it worked + if (!backupRequired || backupDone) { + if (internalRename(m_basePath, m_name + qL1C('+'), m_name)) { + m_done |= NamePlusToName; + } + else if (backupDone && !undoRename()) { + qCCritical(LogSystem) << QString::fromLatin1("failed to rename '%1+' to '%1', but also failed to rename backup '%1-' back to '%1' (in directory %2)") + .arg(m_name, m_basePath.absolutePath()); + } + } + } + return m_requested == m_done; +} + +bool ScopedRenamer::rename(const QDir &baseName, ScopedRenamer::Modes modes) +{ + return rename(baseName.absolutePath(), modes); +} + + +bool ScopedRenamer::take() +{ + if (!m_taken) + m_taken = true; + return m_taken; +} + +bool ScopedRenamer::undoRename() +{ + if (!m_taken) { + if (interalUndoRename()) { + m_taken = true; + } else { + if (m_done & NamePlusToName) { + qCCritical(LogSystem) << QString::fromLatin1("failed to undo rename from '%1+' to '%1' (in directory %2)") + .arg(m_name, m_basePath.absolutePath()); + } + if (m_done & NameToNameMinus) { + qCCritical(LogSystem) << QString::fromLatin1("failed to undo rename from '%1' to '%1-' (in directory %2)") + .arg(m_name, m_basePath.absolutePath()); + } + } + } + return m_taken; +} + +bool ScopedRenamer::interalUndoRename() +{ + if (m_done & NamePlusToName) { + if (internalRename(m_basePath, m_name, m_name + qL1C('+'))) + m_done &= ~NamePlusToName; + } + if (m_done & NameToNameMinus) { + if (internalRename(m_basePath, m_name + qL1C('-'), m_name)) + m_done &= ~NameToNameMinus; + } + + return (m_done == 0); +} + +ScopedRenamer::~ScopedRenamer() +{ + undoRename(); +} + +bool ScopedRenamer::isRenamed() const +{ + return m_requested && (m_requested == m_done); +} + +QString ScopedRenamer::baseName() const +{ + return m_basePath.absoluteFilePath(m_name); +} + +QT_END_NAMESPACE_AM diff --git a/src/manager-lib/scopeutilities.h b/src/manager-lib/scopeutilities.h new file mode 100644 index 00000000..f3d30424 --- /dev/null +++ b/src/manager-lib/scopeutilities.h @@ -0,0 +1,105 @@ +/**************************************************************************** +** +** 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 +** +****************************************************************************/ + +#pragma once + +#include +#include +#include + +#include + +QT_BEGIN_NAMESPACE_AM + +class ScopedDirectoryCreator +{ +public: + ScopedDirectoryCreator(); + bool create(const QString &path, bool removeExisting = true); + bool take(); + bool destroy(); + ~ScopedDirectoryCreator(); + + QDir dir(); + +private: + Q_DISABLE_COPY(ScopedDirectoryCreator) + + QString m_path; + bool m_created = false; + bool m_taken = false; +}; + +class ScopedRenamer +{ +public: + enum Mode { + NameToNameMinus = 1, // create backup : foo -> foo- + NamePlusToName = 2 // replace with new: foo+ -> foo + }; + + Q_DECLARE_FLAGS(Modes, Mode) + + ScopedRenamer(); + bool rename(const QString &baseName, ScopedRenamer::Modes modes); + bool rename(const QDir &baseName, ScopedRenamer::Modes modes); + bool take(); + bool undoRename(); + ~ScopedRenamer(); + + bool isRenamed() const; + QString baseName() const; + +private: + bool interalUndoRename(); + static bool internalRename(const QDir &dir, const QString &from, const QString &to); + Q_DISABLE_COPY(ScopedRenamer) + QDir m_basePath; + QString m_name; + Modes m_requested; + Modes m_done; + bool m_taken = false; +}; + +QT_END_NAMESPACE_AM + +Q_DECLARE_OPERATORS_FOR_FLAGS(QT_PREPEND_NAMESPACE_AM(ScopedRenamer::Modes)) diff --git a/src/manager-lib/sudo.cpp b/src/manager-lib/sudo.cpp new file mode 100644 index 00000000..a9d661fe --- /dev/null +++ b/src/manager-lib/sudo.cpp @@ -0,0 +1,495 @@ +/**************************************************************************** +** +** 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 +#include +#include +#include +#include +#include +#include + +#include "logging.h" +#include "sudo.h" +#include "utilities.h" +#include "exception.h" +#include "global.h" + +#include + +#if defined(Q_OS_LINUX) +# include "processtitle.h" + +# include +# include +# include +# include +# include +# include +# include +# include + +// These two functions are implemented in glibc, but the header file is +// in the separate libcap-dev package. Since we want to avoid unnecessary +// dependencies, we just declare them here +extern "C" int capset(cap_user_header_t header, cap_user_data_t data); +extern "C" int capget(cap_user_header_t header, const cap_user_data_t data); + +// Support for old/broken C libraries +# if defined(_LINUX_CAPABILITY_VERSION) && !defined(_LINUX_CAPABILITY_VERSION_1) +# define _LINUX_CAPABILITY_VERSION_1 _LINUX_CAPABILITY_VERSION +# define _LINUX_CAPABILITY_U32S_1 1 +# if !defined(CAP_TO_INDEX) +# define CAP_TO_INDEX(x) ((x) >> 5) +# endif +# if !defined(CAP_TO_MASK) +# define CAP_TO_MASK(x) (1 << ((x) & 31)) +# endif +# endif +# if defined(_LINUX_CAPABILITY_VERSION_3) // use 64-bit support, if available +# define AM_CAP_VERSION _LINUX_CAPABILITY_VERSION_3 +# define AM_CAP_SIZE _LINUX_CAPABILITY_U32S_3 +# else // fallback to 32-bit support +# define AM_CAP_VERSION _LINUX_CAPABILITY_VERSION_1 +# define AM_CAP_SIZE _LINUX_CAPABILITY_U32S_1 +# endif + +// Convenient way to ignore EINTR on any system call +# define EINTR_LOOP(cmd) __extension__ ({__typeof__(cmd) res = 0; do { res = cmd; } while (res == -1 && errno == EINTR); res; }) + + +// Declared as weak symbol here, so we can check at runtime if we were compiled against libgcov +extern "C" void __gcov_init() __attribute__((weak)); + + +QT_BEGIN_NAMESPACE_AM + +static void sigHupHandler(int sig) +{ + if (sig == SIGHUP) + _exit(0); +} + +QT_END_NAMESPACE_AM + +#endif // Q_OS_LINUX + + +QT_BEGIN_NAMESPACE_AM + +void Sudo::forkServer(DropPrivileges dropPrivileges, QStringList *warnings) +{ + bool canSudo = false; + +#if defined(Q_OS_LINUX) + uid_t realUid = getuid(); + uid_t effectiveUid = geteuid(); + canSudo = (realUid == 0) || (effectiveUid == 0); +#else + Q_UNUSED(warnings) + Q_UNUSED(dropPrivileges) +#endif + + if (!canSudo) { + SudoServer::createInstance(-1); + SudoClient::createInstance(-1, SudoServer::instance()); + if (warnings) { + *warnings << qSL("For the installer to work correctly, the executable needs to be run either as root via sudo or SUID (preferred)"); + *warnings << qSL("(using fallback implementation - you might experience permission errors on installer operations)"); + } + return; + } + +#if defined(Q_OS_LINUX) + gid_t realGid = getgid(); + uid_t sudoUid = static_cast(qEnvironmentVariableIntValue("SUDO_UID")); + + // run as normal user (e.g. 1000): uid == 1000 euid == 1000 + // run with binary suid-root: uid == 1000 euid == 0 + // run with sudo (no suid-root): uid == 0 euid == 0 $SUDO_UID == 1000 + + // treat sudo as special variant of a SUID executable + if (realUid == 0 && effectiveUid == 0 && sudoUid != 0) { + realUid = sudoUid; + realGid = static_cast(qEnvironmentVariableIntValue("SUDO_GID")); + + if (setresgid(realGid, 0, 0) || setresuid(realUid, 0, 0)) + throw Exception(errno, "Could not set real user or group ID"); + } + + int socketFds[2]; + if (EINTR_LOOP(socketpair(AF_UNIX, SOCK_DGRAM, 0, socketFds)) != 0) + throw Exception(errno, "Could not create a pair of sockets"); + + // We need to make the gcda files generated by the root process writable by the normal user. + // There is no way to detect a compilation with -ftest-coverage, but we can check for gcov + // symbols at runtime. GCov will open all gcda files at fork() time, so we can get away with + // switching umasks around the fork() call. + + mode_t realUmask = 0; + if (__gcov_init) + realUmask = umask(0); + + pid_t pid = fork(); + if (pid < 0) { + throw Exception(errno, "Could not fork process"); + } else if (pid == 0) { + // child + close(0); + setsid(); + + // reset umask + if (realUmask) + umask(realUmask); + + // This call is Linux only, but it makes it so easy to detect a dying parent process. + // We would have a big problem otherwise, since the main process drops its privileges, + // which prevents it from sending SIGHUP to the child process, which still runs with + // root privileges. + prctl(PR_SET_PDEATHSIG, SIGHUP); + signal(SIGHUP, sigHupHandler); + + // Drop as many capabilities as possible, just to be on the safe side + static const quint32 neededCapabilities[] = { + CAP_SYS_ADMIN, + CAP_CHOWN, + CAP_FOWNER, + CAP_DAC_OVERRIDE + }; + + bool capSetOk = false; + __user_cap_header_struct capHeader { AM_CAP_VERSION, getpid() }; + __user_cap_data_struct capData[AM_CAP_SIZE]; + if (capget(&capHeader, capData) == 0) { + quint32 capNeeded[AM_CAP_SIZE]; + memset(&capNeeded, 0, sizeof(capNeeded)); + for (quint32 cap : neededCapabilities) { + int idx = CAP_TO_INDEX(cap); + Q_ASSERT(idx < AM_CAP_SIZE); + capNeeded[idx] |= CAP_TO_MASK(cap); + } + for (int i = 0; i < AM_CAP_SIZE; ++i) + capData[i].effective = capData[i].permitted = capData[i].inheritable = capNeeded[i]; + if (capset(&capHeader, capData) == 0) + capSetOk = true; + } + if (!capSetOk) + qCCritical(LogSystem) << "could not drop privileges in the SudoServer process -- continuing with full root privileges"; + + SudoServer::createInstance(socketFds[0]); + ProcessTitle::setTitle("%s", "sudo helper"); + SudoServer::instance()->run(); + } + // parent + + // reset umask + if (realUmask) + umask(realUmask); + + SudoClient::createInstance(socketFds[1]); + + if (realUid != effectiveUid) { + // drop all root privileges + if (dropPrivileges == DropPrivilegesPermanently) { + if (setresgid(realGid, realGid, realGid) || setresuid(realUid, realUid, realUid)) { + kill(pid, SIGKILL); + throw Exception(errno, "Could not set real user or group ID"); + } + } else { + qCCritical(LogSystem) << "\nSudo was instructed to NOT drop root privileges permanently.\nThis is dangerous and should only be used in auto-tests!\n"; + if (setresgid(realGid, realGid, 0) || setresuid(realUid, realUid, 0)) { + kill(pid, 9); + throw Exception(errno, "Could not set real user or group ID"); + } + } + } + ::atexit([]() { SudoClient::instance()->stopServer(); }); +#endif +} + +SudoInterface::SudoInterface() +{ } + +#ifdef Q_OS_LINUX +bool SudoInterface::sendMessage(int socket, const QByteArray &msg, MessageType type, const QString &errorString) +{ + QByteArray packet; + QDataStream ds(&packet, QIODevice::WriteOnly); + ds << errorString << msg; + packet.prepend((type == Request) ? "RQST" : "RPLY"); + + auto bytesWritten = EINTR_LOOP(write(socket, packet.constData(), static_cast(packet.size()))); + return bytesWritten == packet.size(); +} + + +QByteArray SudoInterface::receiveMessage(int socket, MessageType type, QString *errorString) +{ + const int headerSize = 4; + char recvBuffer[8*1024]; + auto bytesReceived = EINTR_LOOP(recv(socket, recvBuffer, sizeof(recvBuffer), 0)); + + if ((bytesReceived < headerSize) || qstrncmp(recvBuffer, (type == Request ? "RQST" : "RPLY"), 4)) { + *errorString = qL1S("failed to receive command from the SudoClient process"); + //qCCritical(LogSystem) << *errorString; + return QByteArray(); + } + + QByteArray packet(recvBuffer + headerSize, int(bytesReceived) - headerSize); + + QDataStream ds(&packet, QIODevice::ReadOnly); + QByteArray msg; + ds >> *errorString >> msg; + return msg; +} +#endif // Q_OS_LINUX + + +SudoClient *SudoClient::s_instance = nullptr; + +SudoClient *SudoClient::instance() +{ + return s_instance; +} + +bool SudoClient::isFallbackImplementation() const +{ + return m_socket < 0; +} + +SudoClient::SudoClient(int socketFd) + : m_socket(socketFd) +{ } + +SudoClient *SudoClient::createInstance(int socketFd, SudoServer *shortCircuit) +{ + if (!s_instance) { + s_instance = new SudoClient(socketFd); + s_instance->m_shortCircuit = shortCircuit; + } + return s_instance; +} + + +// this is not nice, but it prevents a lot of copy/paste errors. (the C++ variadic template version +// would be equally ugly, since it needs a friend declaration in the public header) +template R returnType(R (C::*)(Ps...)); + +#define CALL(FUNC_NAME, PARAM) \ + QByteArray msg; \ + QDataStream(&msg, QIODevice::WriteOnly) << #FUNC_NAME << PARAM; \ + QByteArray reply = call(msg); \ + QDataStream result(&reply, QIODevice::ReadOnly); \ + decltype(returnType(&SudoClient::FUNC_NAME)) r; \ + result >> r; \ + return r + +bool SudoClient::removeRecursive(const QString &fileOrDir) +{ + CALL(removeRecursive, fileOrDir); +} + +bool SudoClient::setOwnerAndPermissionsRecursive(const QString &fileOrDir, uid_t user, gid_t group, mode_t permissions) +{ + CALL(setOwnerAndPermissionsRecursive, fileOrDir << user << group << permissions); +} + +void SudoClient::stopServer() +{ +#ifdef Q_OS_LINUX + if (!m_shortCircuit && m_socket >= 0) { + QByteArray msg; + QDataStream(&msg, QIODevice::WriteOnly) << "stopServer"; + sendMessage(m_socket, msg, Request); + } +#endif +} + +QByteArray SudoClient::call(const QByteArray &msg) +{ + QMutexLocker locker(&m_mutex); + + if (m_shortCircuit) + return m_shortCircuit->receive(msg); + +#ifdef Q_OS_LINUX + if (m_socket >= 0) { + if (sendMessage(m_socket, msg, Request)) + return receiveMessage(m_socket, Reply, &m_errorString); + } +#else + Q_UNUSED(m_socket) +#endif + + //qCCritical(LogSystem) << "failed to send command to the SudoServer process"; + m_errorString = qL1S("failed to send command to the SudoServer process"); + return QByteArray(); +} + + + +SudoServer *SudoServer::s_instance = nullptr; + +SudoServer *SudoServer::instance() +{ + return s_instance; +} + +SudoServer::SudoServer(int socketFd) + : m_socket(socketFd) +{ } + +SudoServer *SudoServer::createInstance(int socketFd) +{ + if (!s_instance) + s_instance = new SudoServer(socketFd); + return s_instance; +} + +void SudoServer::run() +{ +#ifdef Q_OS_LINUX + QString dummy; + + forever { + QByteArray msg = receiveMessage(m_socket, Request, &dummy); + QByteArray reply = receive(msg); + + if (m_stop) + exit(0); + + sendMessage(m_socket, reply, Reply, m_errorString); + } +#else + Q_UNUSED(m_socket) + Q_ASSERT(false); + exit(0); +#endif +} + +QByteArray SudoServer::receive(const QByteArray &msg) +{ + QDataStream params(msg); + char *functionArray; + params >> functionArray; + QByteArray function(functionArray); + delete [] functionArray; + QByteArray reply; + QDataStream result(&reply, QIODevice::WriteOnly); + m_errorString.clear(); + + if (function == "removeRecursive") { + QString fileOrDir; + params >> fileOrDir; + result << removeRecursive(fileOrDir); + } else if (function == "setOwnerAndPermissionsRecursive") { + QString fileOrDir; + uid_t user; + gid_t group; + mode_t permissions; + params >> fileOrDir >> user >> group >> permissions; + result << setOwnerAndPermissionsRecursive(fileOrDir, user, group, permissions); + } else if (function == "stopServer") { + m_stop = true; + } else { + reply.truncate(0); + m_errorString = QString::fromLatin1("unknown function '%1' called in SudoServer").arg(qL1S(function)); + } + return reply; +} + +bool SudoServer::removeRecursive(const QString &fileOrDir) +{ + try { + if (!recursiveOperation(fileOrDir, safeRemove)) + throw Exception(errno, "could not recursively remove %1").arg(fileOrDir); + return true; + } catch (const Exception &e) { + m_errorString = e.errorString(); + return false; + } +} + +bool SudoServer::setOwnerAndPermissionsRecursive(const QString &fileOrDir, uid_t user, gid_t group, mode_t permissions) +{ +#if defined(Q_OS_LINUX) + static auto setOwnerAndPermissions = + [user, group, permissions](const QString &path, RecursiveOperationType type) -> bool { + if (type == RecursiveOperationType::EnterDirectory) + return true; + + const QByteArray localPath = path.toLocal8Bit(); + mode_t mode = permissions; + + if (type == RecursiveOperationType::LeaveDirectory) { + // set the x bit for directories, but only where it makes sense + if (mode & 06) + mode |= 01; + if (mode & 060) + mode |= 010; + if (mode & 0600) + mode |= 0100; + } + + return ((chmod(localPath, mode) == 0) && (chown(localPath, user, group) == 0)); + }; + + try { + if (!recursiveOperation(fileOrDir, setOwnerAndPermissions)) { + throw Exception(errno, "could not recursively set owner and permission on %1 to %2:%3 / %4") + .arg(fileOrDir).arg(user).arg(group).arg(permissions, 4, 8, QLatin1Char('0')); + } + } catch (const Exception &e) { + m_errorString = e.errorString(); + return false; + } +#else + Q_UNUSED(fileOrDir) + Q_UNUSED(user) + Q_UNUSED(group) + Q_UNUSED(permissions) +#endif // Q_OS_LINUX + return false; +} + +QT_END_NAMESPACE_AM diff --git a/src/manager-lib/sudo.h b/src/manager-lib/sudo.h new file mode 100644 index 00000000..357261e6 --- /dev/null +++ b/src/manager-lib/sudo.h @@ -0,0 +1,154 @@ +/**************************************************************************** +** +** 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 +** +****************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +#ifdef Q_OS_UNIX +# include +#else +typedef uint uid_t; +typedef uint gid_t; +//typedef uint mode_t; // already typedef'ed in qplatformdefs.h +#endif + +#include + +QT_BEGIN_NAMESPACE_AM + +class Sudo +{ +public: + enum DropPrivileges { + DropPrivilegesPermanently, + DropPrivilegesRegainable, // only use this for auto-tests + }; + + static void forkServer(DropPrivileges dropPrivileges, QStringList *warnings = nullptr) Q_DECL_NOEXCEPT_EXPR(false); +}; + +class SudoInterface +{ +public: + virtual ~SudoInterface() = default; + + virtual bool removeRecursive(const QString &fileOrDir) = 0; + virtual bool setOwnerAndPermissionsRecursive(const QString &fileOrDir, uid_t user, gid_t group, mode_t permissions) = 0; + +protected: + enum MessageType { Request, Reply }; + +#ifdef Q_OS_LINUX + QByteArray receiveMessage(int socket, MessageType type, QString *errorString); + bool sendMessage(int socket, const QByteArray &msg, MessageType type, const QString &errorString = QString()); +#endif + QByteArray receive(const QByteArray &packet); + +protected: + SudoInterface(); +private: + Q_DISABLE_COPY(SudoInterface) +}; + +class SudoServer; + +class SudoClient : public SudoInterface +{ +public: + static SudoClient *createInstance(int socketFd, SudoServer *shortCircuit = 0); + + static SudoClient *instance(); + + bool isFallbackImplementation() const; + + bool removeRecursive(const QString &fileOrDir) override; + bool setOwnerAndPermissionsRecursive(const QString &fileOrDir, uid_t user, gid_t group, mode_t permissions) override; + + void stopServer(); + + QString lastError() const { return m_errorString; } + +private: + SudoClient(int socketFd); + + QByteArray call(const QByteArray &msg); + + int m_socket; + QString m_errorString; + QMutex m_mutex; + SudoServer *m_shortCircuit; + + static SudoClient *s_instance; +}; + +class SudoServer : public SudoInterface +{ +public: + static SudoServer *createInstance(int socketFd); + + static SudoServer *instance(); + + bool removeRecursive(const QString &fileOrDir) override; + bool setOwnerAndPermissionsRecursive(const QString &fileOrDir, uid_t user, gid_t group, mode_t permissions) override; + + QString lastError() const { return m_errorString; } + + Q_NORETURN void run(); + +private: + SudoServer(int socketFd); + + QByteArray receive(const QByteArray &msg); + friend class SudoClient; + + int m_socket; + QString m_errorString; + bool m_stop = false; + + static SudoServer *s_instance; +}; + +QT_END_NAMESPACE_AM diff --git a/src/src.pro b/src/src.pro index 3bd37a3c..b5e66223 100644 --- a/src/src.pro +++ b/src/src.pro @@ -19,11 +19,9 @@ notification_lib.depends = common_lib package_lib.subdir = package-lib package_lib.depends = crypto_lib application_lib -installer_lib.subdir = installer-lib -installer_lib.depends = package_lib - manager_lib.subdir = manager-lib -manager_lib.depends = application_lib notification_lib intent_server_lib intent_client_lib installer_lib monitor_lib plugin_interfaces +manager_lib.depends = application_lib notification_lib intent_server_lib intent_client_lib monitor_lib plugin_interfaces +!disable-installer:manager_lib.depends += package_lib crypto_lib window_lib.subdir = window-lib window_lib.depends = manager_lib @@ -49,7 +47,6 @@ main_lib.depends = shared_main_lib manager_lib window_lib monitor_lib !disable-external-dbus-interfaces:qtHaveModule(dbus) { dbus_lib.subdir = dbus-lib dbus_lib.depends = manager_lib window_lib - !disable-installer:dbus_lib.depends += installer_lib main_lib.depends += dbus_lib } @@ -64,10 +61,10 @@ tools_testrunner.subdir = tools/testrunner tools_testrunner.depends = main_lib tools_dumpqmltypes.subdir = tools/dumpqmltypes -tools_dumpqmltypes.depends = manager_lib installer_lib window_lib shared_main_lib main_lib launcher_lib +tools_dumpqmltypes.depends = manager_lib window_lib shared_main_lib main_lib launcher_lib tools_packager.subdir = tools/packager -tools_packager.depends = package_lib +tools_packager.depends = package_lib application_lib crypto_lib tools_uploader.subdir = tools/uploader tools_uploader.depends = common_lib @@ -91,7 +88,6 @@ SUBDIRS = \ qtHaveModule(qml):SUBDIRS += \ notification_lib \ manager_lib \ - installer_lib \ window_lib \ monitor_lib \ shared_main_lib \ diff --git a/src/tools/dumpqmltypes/dumpqmltypes.cpp b/src/tools/dumpqmltypes/dumpqmltypes.cpp index d5ce1ab6..3690dc89 100644 --- a/src/tools/dumpqmltypes/dumpqmltypes.cpp +++ b/src/tools/dumpqmltypes/dumpqmltypes.cpp @@ -27,7 +27,7 @@ ** ****************************************************************************/ -#include +#include #include #include #include diff --git a/src/tools/dumpqmltypes/dumpqmltypes.pro b/src/tools/dumpqmltypes/dumpqmltypes.pro index ff13f5a7..acde4c97 100644 --- a/src/tools/dumpqmltypes/dumpqmltypes.pro +++ b/src/tools/dumpqmltypes/dumpqmltypes.pro @@ -10,7 +10,6 @@ QT *= \ appman_common-private \ appman_application-private \ appman_manager-private \ - appman_installer-private \ appman_notification-private \ appman_window-private \ appman_launcher-private \ diff --git a/tests/application/application.pro b/tests/application/application.pro index b2a75f71..cc13702e 100644 --- a/tests/application/application.pro +++ b/tests/application/application.pro @@ -6,6 +6,5 @@ QT *= \ appman_common-private \ appman_application-private \ appman_manager-private \ - appman_installer-private \ SOURCES += tst_application.cpp diff --git a/tests/applicationinstaller/applicationinstaller.pro b/tests/applicationinstaller/applicationinstaller.pro index 37532e10..b84ffac4 100644 --- a/tests/applicationinstaller/applicationinstaller.pro +++ b/tests/applicationinstaller/applicationinstaller.pro @@ -9,6 +9,5 @@ QT *= \ appman_application-private \ appman_package-private \ appman_manager-private \ - appman_installer-private SOURCES += tst_applicationinstaller.cpp diff --git a/tests/packager-tool/packager-tool.pro b/tests/packager-tool/packager-tool.pro index 7e69306b..08876ac6 100644 --- a/tests/packager-tool/packager-tool.pro +++ b/tests/packager-tool/packager-tool.pro @@ -8,7 +8,6 @@ QT *= \ appman_application-private \ appman_package-private \ appman_manager-private \ - appman_installer-private \ INCLUDEPATH += $$PWD/../../src/tools/packager SOURCES += $$PWD/../../src/tools/packager/packagingjob.cpp diff --git a/tests/runtime/runtime.pro b/tests/runtime/runtime.pro index 16e6ed0a..64c550cc 100644 --- a/tests/runtime/runtime.pro +++ b/tests/runtime/runtime.pro @@ -7,6 +7,5 @@ QT *= \ appman_common-private \ appman_application-private \ appman_manager-private \ - appman_installer-private \ SOURCES += tst_runtime.cpp diff --git a/tests/sudo/sudo.pro b/tests/sudo/sudo.pro index 9745e2fc..95be319b 100644 --- a/tests/sudo/sudo.pro +++ b/tests/sudo/sudo.pro @@ -6,6 +6,6 @@ include($$PWD/../tests.pri) QT *= \ appman_common-private \ - appman_installer-private \ + appman_manager-private \ SOURCES += tst_sudo.cpp -- cgit v1.2.3