diff options
Diffstat (limited to 'src/manager-lib')
-rw-r--r-- | src/manager-lib/application.cpp | 34 | ||||
-rw-r--r-- | src/manager-lib/application.h | 8 | ||||
-rw-r--r-- | src/manager-lib/applicationmanager.cpp | 29 | ||||
-rw-r--r-- | src/manager-lib/applicationmanager.h | 3 | ||||
-rw-r--r-- | src/manager-lib/asynchronoustask.cpp | 133 | ||||
-rw-r--r-- | src/manager-lib/asynchronoustask.h | 111 | ||||
-rw-r--r-- | src/manager-lib/deinstallationtask.cpp | 155 | ||||
-rw-r--r-- | src/manager-lib/deinstallationtask.h | 75 | ||||
-rw-r--r-- | src/manager-lib/installationtask.cpp | 484 | ||||
-rw-r--r-- | src/manager-lib/installationtask.h | 111 | ||||
-rw-r--r-- | src/manager-lib/manager-lib.pro | 28 | ||||
-rw-r--r-- | src/manager-lib/package.cpp | 237 | ||||
-rw-r--r-- | src/manager-lib/package.h | 151 | ||||
-rw-r--r-- | src/manager-lib/packagemanager.cpp | 1192 | ||||
-rw-r--r-- | src/manager-lib/packagemanager.h | 210 | ||||
-rw-r--r-- | src/manager-lib/packagemanager_p.h | 97 | ||||
-rw-r--r-- | src/manager-lib/scopeutilities.cpp | 215 | ||||
-rw-r--r-- | src/manager-lib/scopeutilities.h | 105 | ||||
-rw-r--r-- | src/manager-lib/sudo.cpp | 495 | ||||
-rw-r--r-- | src/manager-lib/sudo.h | 154 |
20 files changed, 3979 insertions, 48 deletions
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<ApplicationInfo> 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<int> { IsBlocked }); - stopApplicationInternal(app, true); - emitDataChanged(app, QVector<int> { 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<int> { 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<int> { 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<int> &roles = QVector<int>()); 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 <QUuid> + +#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<AsynchronousTask::TaskState>(); + 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 <QThread> +#include <QMutex> + +#include <QtAppManCommon/error.h> + +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 <QtAppManManager/asynchronoustask.h> + +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 <QTemporaryDir> +#include <QMessageAuthenticationCode> + +#include "logging.h" +#include "packagemanager_p.h" +#include "packageinfo.h" +#include "packageextractor.h" +#include "yamlpackagescanner.h" +#include "exception.h" +#include "packagemanager.h" +#include "sudo.h" +#include "utilities.h" +#include "signature.h" +#include "sudo.h" +#include "installationtask.h" + +/* + Overview of what happens on an installation of an app with <id> to <location>: + + Step 1 -- startInstallation() + ============================= + + delete <location>/<id>+ + + create dir <location>/<id>+ + set <extractiondir> to <location>/<id>+ + + + Step 2 -- unpack files + ====================== + + PackageExtractor does its job + + + Step 3 -- finishInstallation() + ================================ + + if (exists <location>/<id>) + set <isupdate> to <true> + + create installation report at <extractiondir>/.installation-report.yaml + + if (not <isupdate>) + create document directory + + if (optional uid separation) + chown/chmod recursively in <extractiondir> and document directory + + + Step 3.1 -- final rename in finishInstallation() + ================================================== + + if (<isupdate>) + rename <location>/<id> to <location>/<id>- + rename <location>/<id>+ to <location>/<id> +*/ + +QT_BEGIN_NAMESPACE_AM + + + +// The standard QTemporaryDir destructor cannot cope with read-only sub-directories. +class TemporaryDir : public QTemporaryDir +{ +public: + TemporaryDir() + : QTemporaryDir() + { } + explicit TemporaryDir(const QString &templateName) + : QTemporaryDir(templateName) + { } + ~TemporaryDir() + { + recursiveOperation(path(), safeRemove); + } +private: + Q_DISABLE_COPY(TemporaryDir) +}; + + +QMutex InstallationTask::s_serializeFinishInstallation { }; + +InstallationTask::InstallationTask(const QString &installationPath, const QString &documentPath, + const QUrl &sourceUrl, QObject *parent) + : AsynchronousTask(parent) + , m_pm(PackageManager::instance()) + , m_installationPath(installationPath) + , m_documentPath(documentPath) + , m_sourceUrl(sourceUrl) +{ } + +InstallationTask::~InstallationTask() +{ } + +bool InstallationTask::cancel() +{ + QMutexLocker locker(&m_mutex); + + // we cannot cancel anymore after finishInstallation() has been called + if (m_installationAcknowledged) + return false; + + m_canceled = true; + if (m_extractor) + m_extractor->cancel(); + m_installationAcknowledgeWaitCondition.wakeAll(); + return true; +} + +void InstallationTask::acknowledge() +{ + QMutexLocker locker(&m_mutex); + + if (m_canceled) + return; + + m_installationAcknowledged = true; + m_installationAcknowledgeWaitCondition.wakeAll(); +} + +void InstallationTask::execute() +{ + try { + if (m_installationPath.isEmpty()) + throw Exception("no installation location was configured"); + + TemporaryDir extractionDir; + if (!extractionDir.isValid()) + throw Exception("could not create a temporary extraction directory"); + + // protect m_canceled and changes to m_extractor + QMutexLocker locker(&m_mutex); + if (m_canceled) + throw Exception(Error::Canceled, "canceled"); + + m_extractor = new PackageExtractor(m_sourceUrl, QDir(extractionDir.path())); + locker.unlock(); + + connect(m_extractor, &PackageExtractor::progress, this, &AsynchronousTask::progress); + + m_extractor->setFileExtractedCallback(std::bind(&InstallationTask::checkExtractedFile, + this, std::placeholders::_1)); + + if (!m_extractor->extract()) + throw Exception(m_extractor->errorCode(), m_extractor->errorString()); + + if (!m_foundInfo || !m_foundIcon) + throw Exception(Error::Package, "package did not contain a valid info.json and icon file"); + + QList<QByteArray> chainOfTrust = m_pm->caCertificates(); + + if (!m_pm->allowInstallationOfUnsignedPackages()) { + if (!m_extractor->installationReport().storeSignature().isEmpty()) { + // normal package from the store + QByteArray sigDigest = m_extractor->installationReport().digest(); + bool sigOk = false; + + if (Signature(sigDigest).verify(m_extractor->installationReport().storeSignature(), chainOfTrust)) { + sigOk = true; + } else if (!m_pm->hardwareId().isEmpty()) { + // did not verify - if we have a hardware-id, try to verify with it + sigDigest = QMessageAuthenticationCode::hash(sigDigest, m_pm->hardwareId().toUtf8(), QCryptographicHash::Sha256); + if (Signature(sigDigest).verify(m_extractor->installationReport().storeSignature(), chainOfTrust)) + sigOk = true; + } + if (!sigOk) + throw Exception(Error::Package, "could not verify the package's store signature"); + } else if (!m_extractor->installationReport().developerSignature().isEmpty()) { + // developer package - needs a device in dev mode + if (!m_pm->developmentMode()) + throw Exception(Error::Package, "cannot install development packages on consumer devices"); + + if (!Signature(m_extractor->installationReport().digest()).verify(m_extractor->installationReport().developerSignature(), chainOfTrust)) + throw Exception(Error::Package, "could not verify the package's developer signature"); + + } else { + throw Exception(Error::Package, "cannot install unsigned packages"); + } + } + + emit finishedPackageExtraction(); + setState(AwaitingAcknowledge); + + // now wait in a wait-condition until we get an acknowledge or we get canceled + locker.relock(); + while (!m_canceled && !m_installationAcknowledged) + m_installationAcknowledgeWaitCondition.wait(&m_mutex); + + // this is the last cancellation point + if (m_canceled) + throw Exception(Error::Canceled, "canceled"); + locker.unlock(); + + setState(Installing); + + // However many downloads are allowed to happen in parallel: we need to serialize those + // tasks here for the finishInstallation() step + QMutexLocker finishLocker(&s_serializeFinishInstallation); + + finishInstallation(); + + // At this point, the installation is done, so we cannot throw anymore. + + // we need to call those PackageManager methods in the correct thread + bool finishOk = false; + QMetaObject::invokeMethod(PackageManager::instance(), [this, &finishOk]() + { finishOk = PackageManager::instance()->finishedPackageInstall(m_packageId); }, + Qt::BlockingQueuedConnection); + + if (!finishOk) + qCWarning(LogInstaller) << "PackageManager rejected the installation of " << m_packageId; + + } catch (const Exception &e) { + setError(e.errorCode(), e.errorString()); + + if (m_managerApproval) { + // we need to call those ApplicationManager methods in the correct thread + bool cancelOk = false; + QMetaObject::invokeMethod(PackageManager::instance(), [this, &cancelOk]() + { cancelOk = PackageManager::instance()->canceledPackageInstall(m_packageId); }, + Qt::BlockingQueuedConnection); + + if (!cancelOk) + qCWarning(LogInstaller) << "PackageManager could not remove package" << m_packageId << "after a failed installation"; + } + } + + + { + QMutexLocker locker(&m_mutex); + delete m_extractor; + m_extractor = nullptr; + } +} + + +void InstallationTask::checkExtractedFile(const QString &file) Q_DECL_NOEXCEPT_EXPR(false) +{ + ++m_extractedFileCount; + + if (m_extractedFileCount == 1) { + if (file != qL1S("info.yaml")) + throw Exception(Error::Package, "info.yaml must be the first file in the package. Got %1") + .arg(file); + + YamlPackageScanner yps; + m_package.reset(yps.scan(m_extractor->destinationDirectory().absoluteFilePath(file))); + if (m_package->id() != m_extractor->installationReport().packageId()) + throw Exception(Error::Package, "the package identifiers in --PACKAGE-HEADER--' and info.yaml do not match"); + + m_iconFileName = m_package->icon(); // store it separately as we will give away ApplicationInfo later on + + if (m_iconFileName.isEmpty()) + throw Exception(Error::Package, "the 'icon' field in info.yaml cannot be empty or absent."); + + m_packageId = m_package->id(); + + m_foundInfo = true; + } else if (m_extractedFileCount == 2) { + // the second file must be the icon + + Q_ASSERT(m_foundInfo); + Q_ASSERT(!m_foundIcon); + + if (file != m_iconFileName) + throw Exception(Error::Package, + "The package icon (as stated in info.yaml) must be the second file in the package." + " Expected '%1', got '%2'").arg(m_iconFileName, file); + + QFile icon(m_extractor->destinationDirectory().absoluteFilePath(file)); + + if (icon.size() > 256*1024) + throw Exception(Error::Package, "the size of %1 is too large (max. 256KB)").arg(file); + + m_foundIcon = true; + } else { + throw Exception(Error::Package, "Could not find info.yaml and the icon file at the beginning of the package."); + } + + if (m_foundIcon && m_foundInfo) { + qCDebug(LogInstaller) << "emit taskRequestingInstallationAcknowledge" << id() << "for package" << m_package->id(); + + QVariantMap nameMap; + auto names = m_package->names(); + for (auto it = names.constBegin(); it != names.constEnd(); ++it) + nameMap.insert(it.key(), it.value()); + + QVariantMap applicationData { + { qSL("id"), m_package->id() }, + { qSL("version"), m_package->version() }, + { qSL("icon"), m_package->icon() }, + { qSL("displayIcon"), m_package->icon() }, // legacy + { qSL("name"), nameMap }, + { qSL("displayName"), nameMap }, // legacy + { qSL("baseDir"), m_package->baseDir().absolutePath() }, + { qSL("codeDir"), m_package->baseDir().absolutePath() }, // 5.12 backward compatibility + { qSL("manifestDir"), m_package->baseDir().absolutePath() }, // 5.12 backward compatibility + { qSL("installationLocationId"), qSL("internal-0") } // 5.13 backward compatibility + }; + emit m_pm->taskRequestingInstallationAcknowledge(id(), applicationData, + m_extractor->installationReport().extraMetaData(), + m_extractor->installationReport().extraSignedMetaData()); + + QDir oldDestinationDirectory = m_extractor->destinationDirectory(); + + startInstallation(); + + QFile::copy(oldDestinationDirectory.filePath(qSL("info.yaml")), m_extractionDir.filePath(qSL("info.yaml"))); + QFile::copy(oldDestinationDirectory.filePath(m_iconFileName), m_extractionDir.filePath(m_iconFileName)); + + { + QMutexLocker locker(&m_mutex); + m_extractor->setDestinationDirectory(m_extractionDir); + + QString path = m_extractionDir.absolutePath(); + path.chop(1); // remove the '+' + m_package->setBaseDir(QDir(path)); + } + // we need to find a free uid before we call startingApplicationInstallation + m_package->m_uid = m_pm->findUnusedUserId(); + m_applicationUid = m_package->m_uid; + + // we need to call those ApplicationManager methods in the correct thread + // this will also exclusively lock the application for us + // m_package ownership is transferred to the ApplicationManager + QString packageId = m_package->id(); // m_package is gone after the invoke + QMetaObject::invokeMethod(PackageManager::instance(), [this]() + { m_managerApproval = PackageManager::instance()->startingPackageInstallation(m_package.take()); }, + Qt::BlockingQueuedConnection); + + if (!m_managerApproval) + throw Exception("PackageManager declined the installation of %1").arg(packageId); + + // we're not interested in any other files from here on... + m_extractor->setFileExtractedCallback(nullptr); + } +} + +void InstallationTask::startInstallation() Q_DECL_NOEXCEPT_EXPR(false) +{ + // 2. delete old, partial installation + + QDir installationDir = QString(m_installationPath + qL1C('/')); + QString installationTarget = m_packageId + qL1C('+'); + if (installationDir.exists(installationTarget)) { + if (!removeRecursiveHelper(installationDir.absoluteFilePath(installationTarget))) + throw Exception("could not remove old, partial installation %1/%2").arg(installationDir).arg(installationTarget); + } + + // 4. create new installation + if (!m_installationDirCreator.create(installationDir.absoluteFilePath(installationTarget))) + throw Exception("could not create installation directory %1/%2").arg(installationDir).arg(installationTarget); + m_extractionDir = installationDir; + if (!m_extractionDir.cd(installationTarget)) + throw Exception("could not cd into installation directory %1/%2").arg(installationDir).arg(installationTarget); + m_applicationDir.setPath(installationDir.absoluteFilePath(m_packageId)); +} + +void InstallationTask::finishInstallation() Q_DECL_NOEXCEPT_EXPR(false) +{ + QDir documentDirectory(m_documentPath); + ScopedDirectoryCreator documentDirCreator; + + enum { Installation, Update } mode = Installation; + + if (m_applicationDir.exists()) + mode = Update; + + // create the installation report + InstallationReport report = m_extractor->installationReport(); + + QFile reportFile(m_extractionDir.absoluteFilePath(qSL(".installation-report.yaml"))); + if (!reportFile.open(QFile::WriteOnly) || !report.serialize(&reportFile)) + throw Exception(reportFile, "could not write the installation report"); + reportFile.close(); + + // create the document directories when installing (not needed on updates) + if (mode == Installation) { + // this package may have been installed earlier and the document directory may not have been removed + if (!documentDirectory.cd(m_packageId)) { + if (!documentDirCreator.create(documentDirectory.absoluteFilePath(m_packageId))) + throw Exception(Error::IO, "could not create the document directory %1").arg(documentDirectory.filePath(m_packageId)); + } + } +#ifdef Q_OS_UNIX + // update the owner, group and permission bits on both the installation and document directories + SudoClient *root = SudoClient::instance(); + + if (m_pm->isApplicationUserIdSeparationEnabled() && root) { + uid_t uid = m_applicationUid; + gid_t gid = m_pm->commonApplicationGroupId(); + + if (!root->setOwnerAndPermissionsRecursive(documentDirectory.filePath(m_packageId), uid, gid, 02700)) { + throw Exception(Error::IO, "could not recursively change the owner to %1:%2 and the permission bits to %3 in %4") + .arg(uid).arg(gid).arg(02700, 0, 8).arg(documentDirectory.filePath(m_packageId)); + } + + if (!root->setOwnerAndPermissionsRecursive(m_extractionDir.path(), uid, gid, 0440)) { + throw Exception(Error::IO, "could not recursively change the owner to %1:%2 and the permission bits to %3 in %4") + .arg(uid).arg(gid).arg(0440, 0, 8).arg(m_extractionDir.absolutePath()); + } + } +#endif + + // final rename + + // POSIX cannot atomically rename directories, if the destination directory exists + // and is non-empty. We need to do a double-rename in this case, which might fail! + // The image is a file, so this limitation does not apply! + + ScopedRenamer renameApplication; + + if (mode == Update) { + if (!renameApplication.rename(m_applicationDir, ScopedRenamer::NamePlusToName | ScopedRenamer::NameToNameMinus)) + throw Exception(Error::IO, "could not rename application directory %1+ to %1 (including a backup to %1-)").arg(m_applicationDir); + } else { + if (!renameApplication.rename(m_applicationDir, ScopedRenamer::NamePlusToName)) + throw Exception(Error::IO, "could not rename application directory %1+ to %1").arg(m_applicationDir); + } + + // from this point onwards, we are not allowed to throw anymore, since the installation is "done" + + setState(CleaningUp); + + renameApplication.take(); + documentDirCreator.take(); + + m_installationDirCreator.take(); + + // this should not be necessary, but it also won't hurt + if (mode == Update) + removeRecursiveHelper(m_applicationDir.absolutePath() + qL1C('-')); + +#ifdef Q_OS_UNIX + // write files to the filesystem + sync(); +#endif + + m_errorString.clear(); +} + +QT_END_NAMESPACE_AM 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 <QUrl> +#include <QStringList> +#include <QWaitCondition> +#include <QMutex> + +#include <QtAppManApplication/installationreport.h> +#include <QtAppManManager/asynchronoustask.h> +#include <QtAppManManager/scopeutilities.h> + +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<PackageInfo> 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 <QLocale> + +#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 <QtAppManCommon/global.h> +#include <QtAppManApplication/packageinfo.h> +#include <QUrl> +#include <QString> +#include <QAtomicInt> +#include <QObject> + +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<Application *> fromApplicationInfoVector(QVector<ApplicationInfo *> &); + + /* + 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<PackageInfo> m_info; + QScopedPointer<PackageInfo> m_updatedInfo; + + State m_state = Installed; + qreal m_progress = 0; + QAtomicInt m_blocked; + QVector<ApplicationInfo *> 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 <QMetaMethod> +#include <QQmlEngine> +#include <QVersionNumber> +#include "packagemanager.h" +#include "packagedatabase.h" +#include "packagemanager_p.h" +#include "package.h" +#include "logging.h" +#include "installationreport.h" +#include "exception.h" +#include "sudo.h" +#include "utilities.h" + +#if defined(Q_OS_WIN) +# include <Windows.h> +#else +# include <sys/stat.h> +# include <errno.h> +# if defined(Q_OS_ANDROID) +# include <sys/vfs.h> +# define statvfs statfs +# else +# include <sys/statvfs.h> +# 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<int, QByteArray> 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<PackageManager> 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<Package *> PackageManager::packages() const +{ + return d->packages; +} + +void PackageManager::registerQmlTypes() +{ + qmlRegisterSingletonType<PackageManager>("QtApplicationManager.SystemUI", 2, 0, "PackageManager", + &PackageManager::instanceForQml); + qmlRegisterUncreatableType<Package>("QtApplicationManager.SystemUI", 2, 0, "PackageObject", + qSL("Cannot create objects of type PackageObject")); + qRegisterMetaType<Package *>("Package*"); + + s_roleNames.insert(Id, "packageId"); + s_roleNames.insert(Name, "name"); + s_roleNames.insert(Description, "description"); + s_roleNames.insert(Icon, "icon"); + s_roleNames.insert(IsBlocked, "isBlocked"); + s_roleNames.insert(IsUpdating, "isUpdating"); + s_roleNames.insert(IsRemovable, "isRemovable"); + s_roleNames.insert(UpdateProgress, "updateProgress"); + s_roleNames.insert(Version, "version"); + s_roleNames.insert(PackageItem, "package"); +} + +PackageManager::PackageManager(PackageDatabase *packageDatabase, + const 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<int> &roles) +{ + int row = d->packages.indexOf(package); + if (row >= 0) { + emit dataChanged(index(row), index(row), roles); + + static const auto pkgChanged = QMetaMethod::fromSignal(&PackageManager::packageChanged); + if (isSignalConnected(pkgChanged)) { + QStringList stringRoles; + for (auto role : roles) + stringRoles << qL1S(s_roleNames[role]); + emit packageChanged(package->id(), stringRoles); + } + } +} + +// item model part + +int PackageManager::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + return d->packages.count(); +} + +QVariant PackageManager::data(const QModelIndex &index, int role) const +{ + if (index.parent().isValid() || !index.isValid()) + return QVariant(); + + Package *package = d->packages.at(index.row()); + + switch (role) { + case Id: + return package->id(); + case Name: + return package->name(); + case Description: + return package->description(); + case Icon: + return package->icon(); + case IsBlocked: + return package->isBlocked(); + case IsUpdating: + return package->state() != Package::Installed; + case UpdateProgress: + return package->progress(); + case IsRemovable: + return !package->isBuiltIn(); + case Version: + return package->version(); + case PackageItem: + return QVariant::fromValue(package); + } + return QVariant(); +} + +QHash<int, QByteArray> PackageManager::roleNames() const +{ + return s_roleNames; +} + +int PackageManager::count() const +{ + return rowCount(); +} + +/*! + \qmlmethod object PackageManager::get(int index) + + Retrieves the model data at \a index as a JavaScript object. See the + \l {PackageManager Roles}{role names} for the expected object fields. + + Returns an empty object if the specified \a index is invalid. + + \note This is very inefficient if you only want to access a single property from QML; use + package() instead to access the Package object's properties directly. +*/ +QVariantMap PackageManager::get(int index) const +{ + if (index < 0 || index >= count()) { + qCWarning(LogSystem) << "PackageManager::get(index): invalid index:" << index; + return QVariantMap(); + } + + QVariantMap map; + QHash<int, QByteArray> roles = roleNames(); + for (auto it = roles.begin(); it != roles.end(); ++it) + map.insert(qL1S(it.value()), data(this->index(index), it.key())); + return map; +} + +/*! + \qmlmethod PackageObject PackageManager::package(int index) + + Returns the \l{PackageObject}{package} corresponding to the given \a index in the + model, or \c null if the index is invalid. + + \note The object ownership of the returned Package object stays with the application-manager. + If you want to store this pointer, you can use the PackageManager's QAbstractListModel + signals or the packageAboutToBeRemoved signal to get notified if the object is about + to be deleted on the C++ side. +*/ +Package *PackageManager::package(int index) const +{ + if (index < 0 || index >= count()) { + qCWarning(LogSystem) << "PackageManager::application(index): invalid index:" << index; + return nullptr; + } + return d->packages.at(index); +} + +/*! + \qmlmethod PackageObject PackageManager::package(string id) + + Returns the \l{PackageObject}{package} corresponding to the given package \a id, + or \c null if the id does not exist. + + \note The object ownership of the returned Package object stays with the application-manager. + If you want to store this pointer, you can use the PackageManager's QAbstractListModel + signals or the packageAboutToBeRemoved signal to get notified if the object is about + to be deleted on the C++ side. +*/ +Package *PackageManager::package(const QString &id) const +{ + auto index = indexOfPackage(id); + return (index < 0) ? nullptr : package(index); +} + +/*! + \qmlmethod int PackageManager::indexOfPackage(string id) + + Maps the package \a id to its position within the model. + + Returns \c -1 if the specified \a id is invalid. +*/ +int PackageManager::indexOfPackage(const QString &id) const +{ + for (int i = 0; i < d->packages.size(); ++i) { + if (d->packages.at(i)->id() == id) + return i; + } + return -1; +} + +bool PackageManager::developmentMode() const +{ + return d->developmentMode; +} + +void PackageManager::setDevelopmentMode(bool enable) +{ + d->developmentMode = enable; +} + +bool PackageManager::allowInstallationOfUnsignedPackages() const +{ + return d->allowInstallationOfUnsignedPackages; +} + +void PackageManager::setAllowInstallationOfUnsignedPackages(bool enable) +{ + d->allowInstallationOfUnsignedPackages = enable; +} + +QString PackageManager::hardwareId() const +{ + return d->hardwareId; +} + +void PackageManager::setHardwareId(const QString &hwId) +{ + d->hardwareId = hwId; +} + +bool PackageManager::isApplicationUserIdSeparationEnabled() const +{ + return d->userIdSeparation; +} + +uint PackageManager::commonApplicationGroupId() const +{ + return d->commonGroupId; +} + +bool PackageManager::enableApplicationUserIdSeparation(uint minUserId, uint maxUserId, uint commonGroupId) +{ + if (minUserId >= maxUserId || minUserId == uint(-1) || maxUserId == uint(-1)) + return false; + d->userIdSeparation = true; + d->minUserId = minUserId; + d->maxUserId = maxUserId; + d->commonGroupId = commonGroupId; + return true; +} + +uint PackageManager::findUnusedUserId() const Q_DECL_NOEXCEPT_EXPR(false) +{ + if (!isApplicationUserIdSeparationEnabled()) + return uint(-1); + + for (uint uid = d->minUserId; uid <= d->maxUserId; ++uid) { + bool match = false; + for (Package *package : d->packages) { + if (package->info()->uid() == uid) { + match = true; + break; + } + } + if (!match) + return uid; + } + throw Exception("could not find a free user-id for application separation in the range %1 to %2") + .arg(d->minUserId).arg(d->maxUserId); +} + +QList<QByteArray> PackageManager::caCertificates() const +{ + return d->chainOfTrust; +} + +void PackageManager::setCACertificates(const QList<QByteArray> &chainOfTrust) +{ + d->chainOfTrust = chainOfTrust; +} + +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<QString, QString> 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<QString> validNames; + for ( ; it != validPaths.cend() && it.key() == currentDir; ++it) + validNames << it.value(); + + const QFileInfoList &dirEntries = QDir(currentDir).entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot); + + // check if there is anything in the filesystem that is NOT listed in the validNames + for (const QFileInfo &fi : dirEntries) { + QString name = fi.fileName(); + if (fi.isDir()) + name.append(qL1C('/')); + + if ((!fi.isDir() && !fi.isFile()) || !validNames.contains(name)) { + qCDebug(LogInstaller) << "cleanup: removing unreferenced inode" << name; + + if (SudoClient::instance()) { + if (!SudoClient::instance()->removeRecursive(fi.absoluteFilePath())) { + throw Exception(Error::IO, "could not remove broken installation leftover %1: %2") + .arg(fi.absoluteFilePath()).arg(SudoClient::instance()->lastError()); + } + } else { + if (!recursiveOperation(fi.absoluteFilePath(), safeRemove)) { + throw Exception(Error::IO, "could not remove broken installation leftover %1 (maybe due to missing root privileges)") + .arg(fi.absoluteFilePath()); + } + } + } + } + } +} + +/*! + \qmlmethod list<string> PackageManager::packageIds() + + Returns a list of all available package ids. This can be used to further query for specific + information via get(). +*/ +QStringList PackageManager::packageIds() const +{ + QStringList ids; + ids.reserve(d->packages.size()); + for (int i = 0; i < d->packages.size(); ++i) + ids << d->packages.at(i)->id(); + return ids; +} + +/*! + \qmlmethod object PackageManager::get(string id) + + Retrieves the model data for the package identified by \a id as a JavaScript object. + See the \l {PackageManager Roles}{role names} for the expected object fields. + + Returns an empty object if the specified \a id is invalid. +*/ +QVariantMap PackageManager::get(const QString &id) const +{ + int index = indexOfPackage(id); + return (index < 0) ? QVariantMap{} : get(index); +} + +/*! + \qmlmethod int PackageManager::installedPackageSize(string packageId) + + Returns the size in bytes that the package identified by \a packageId is occupying on the storage + device. + + Returns \c -1 in case the package \a packageId is not valid, or the package is not installed. +*/ +qint64 PackageManager::installedPackageSize(const QString &packageId) const +{ + if (Package *package = fromId(packageId)) { + if (const InstallationReport *report = package->info()->installationReport()) + return static_cast<qint64>(report->diskSpaceUsed()); + } + return -1; +} + +/*! + \qmlmethod var PackageManager::installedPackageExtraMetaData(string packageId) + + Returns a map of all extra metadata in the package header of the package identified by \a packageId. + + Returns an empty map in case the package \a packageId is not valid, or the package is not installed. +*/ +QVariantMap PackageManager::installedPackageExtraMetaData(const QString &packageId) const +{ + if (Package *package = fromId(packageId)) { + if (const InstallationReport *report = package->info()->installationReport()) + return report->extraMetaData(); + } + return QVariantMap(); +} + +/*! + \qmlmethod var PackageManager::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<InstallationTask *>(task) && (task->id() == taskId)) { + static_cast<InstallationTask *>(task)->acknowledge(); + break; + } + } +} + +/*! + \qmlmethod string PackageManager::removePackage(string packageId, bool keepDocuments, bool force) + + Uninstalls the package identified by \a id. Normally, the documents directory of the + package is deleted on removal, but this can be prevented by setting \a keepDocuments to \c true. + + The actual removal will happen asynchronously in the background. The PackageManager will + emit the signals \l taskStarted, \l taskProgressChanged, \l taskFinished, \l taskFailed and \l + taskStateChanged for the returned \c taskId when applicable. + + Normally, \a force should only be set to \c true if a previous call to removePackage() failed. + This may be necessary if the installation process was interrupted, or or has file-system issues. + + Returns a unique \c taskId. This can also be an empty string, if the task could not be created + (in this case, no signals will be emitted). +*/ +QString PackageManager::removePackage(const QString &packageId, bool keepDocuments, bool force) +{ + AM_TRACE(LogInstaller, packageId, keepDocuments) + + if (Package *package = fromId(packageId)) { + if (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<string> PackageManager::activeTaskIds() + + Retuns a list of all currently active (as in not yet finished or failed) installation task ids. +*/ +QStringList PackageManager::activeTaskIds() const +{ + const auto allTasks = d->allTasks(); + + QStringList result; + for (const AsynchronousTask *task : allTasks) + result << task->id(); + return result; +} + +/*! + \qmlmethod bool PackageManager::cancelTask(string taskId) + + Tries to cancel the installation task identified by \a taskId. + + Returns \c true if the task was canceled, \c false otherwise. +*/ +bool PackageManager::cancelTask(const QString &taskId) +{ + AM_TRACE(LogInstaller, taskId) + + // incoming tasks can be forcefully cancelled right away + for (AsynchronousTask *task : qAsConst(d->incomingTaskList)) { + if (task->id() == taskId) { + task->forceCancel(); + task->deleteLater(); + + handleFailure(task); + + d->incomingTaskList.removeOne(task); + triggerExecuteNextTask(); + return true; + } + } + + // the active task and async tasks might be in a state where cancellation is not possible, + // so we have to ask them nicely + if (d->activeTask && d->activeTask->id() == taskId) + return d->activeTask->cancel(); + + for (AsynchronousTask *task : qAsConst(d->installationTaskList)) { + if (task->id() == taskId) + return task->cancel(); + } + return false; +} + +/*! + \qmlmethod int PackageManager::compareVersions(string version1, string version2) + + Convenience method for app-store implementations or taskRequestingInstallationAcknowledge() + callbacks for comparing version numbers, as the actual version comparison algorithm is not + trivial. + + Returns \c -1, \c 0 or \c 1 if \a version1 is smaller than, equal to, or greater than \a + version2 (similar to how \c strcmp() works). +*/ +int PackageManager::compareVersions(const QString &version1, const QString &version2) +{ + int vn1Suffix = -1; + int vn2Suffix = -1; + QVersionNumber vn1 = QVersionNumber::fromString(version1, &vn1Suffix); + QVersionNumber vn2 = QVersionNumber::fromString(version2, &vn2Suffix); + + int d = QVersionNumber::compare(vn1, vn2); + return d < 0 ? -1 : (d > 0 ? 1 : version1.mid(vn1Suffix).compare(version2.mid(vn2Suffix))); +} + +/*! + \qmlmethod int PackageManager::validateDnsName(string name, int minimalPartCount) + + Convenience method for app-store implementations or taskRequestingInstallationAcknowledge() + callbacks for checking if the given \a name is a valid DNS (or reverse-DNS) name according to + RFC 1035/1123. If the optional parameter \a minimalPartCount is specified, this function will + also check if \a name contains at least this amount of parts/sub-domains. + + Returns \c true if the name is a valid DNS name or \c false otherwise. +*/ +bool PackageManager::validateDnsName(const QString &name, int minimalPartCount) +{ + try { + // check if we have enough parts: e.g. "tld.company.app" would have 3 parts + QStringList parts = name.split('.'); + if (parts.size() < minimalPartCount) { + throw Exception(Error::Parse, "the minimum amount of parts (subdomains) is %1 (found %2)") + .arg(minimalPartCount).arg(parts.size()); + } + + // standard RFC compliance tests (RFC 1035/1123) + + auto partCheck = [](const QString &part) { + int len = part.length(); + + if (len < 1 || len > 63) + throw Exception(Error::Parse, "domain parts must consist of at least 1 and at most 63 characters (found %2 characters)").arg(len); + + for (int pos = 0; pos < len; ++pos) { + ushort ch = part.at(pos).unicode(); + bool isFirst = (pos == 0); + bool isLast = (pos == (len - 1)); + bool isDash = (ch == '-'); + bool isDigit = (ch >= '0' && ch <= '9'); + bool isLower = (ch >= 'a' && ch <= 'z'); + + if ((isFirst || isLast || !isDash) && !isDigit && !isLower) + throw Exception(Error::Parse, "domain parts must consist of only the characters '0-9', 'a-z', and '-' (which cannot be the first or last character)"); + } + }; + + for (const QString &part : parts) + partCheck(part); + + return true; + } catch (const Exception &e) { + qCDebug(LogInstaller).noquote() << "validateDnsName failed:" << e.errorString(); + return false; + } +} + +QString PackageManager::enqueueTask(AsynchronousTask *task) +{ + d->incomingTaskList.append(task); + triggerExecuteNextTask(); + return task->id(); +} + +void PackageManager::triggerExecuteNextTask() +{ + if (!QMetaObject::invokeMethod(this, "executeNextTask", Qt::QueuedConnection)) + qCCritical(LogSystem) << "ERROR: failed to invoke method checkQueue"; +} + +void PackageManager::executeNextTask() +{ + if (d->activeTask || d->incomingTaskList.isEmpty()) + return; + + AsynchronousTask *task = d->incomingTaskList.takeFirst(); + + if (task->hasFailed()) { + task->setState(AsynchronousTask::Failed); + + handleFailure(task); + + task->deleteLater(); + triggerExecuteNextTask(); + return; + } + + connect(task, &AsynchronousTask::started, this, [this, task]() { + emit taskStarted(task->id()); + }); + + connect(task, &AsynchronousTask::stateChanged, this, [this, task](AsynchronousTask::TaskState newState) { + emit taskStateChanged(task->id(), newState); + }); + + connect(task, &AsynchronousTask::progress, this, [this, task](qreal p) { + emit taskProgressChanged(task->id(), p); + + Package *package = fromId(task->packageId()); + if (package && (package->state() != Package::Installed)) { + package->setProgress(p); + // Icon will be in a "+" suffixed directory during installation. So notify about a change on its + // location as well. + emitDataChanged(package, QVector<int> { Icon, UpdateProgress }); + } + }); + + connect(task, &AsynchronousTask::finished, this, [this, task]() { + task->setState(task->hasFailed() ? AsynchronousTask::Failed : AsynchronousTask::Finished); + + if (task->hasFailed()) { + handleFailure(task); + } else { + qCDebug(LogInstaller) << "emit finished" << task->id(); + emit taskFinished(task->id()); + } + + if (d->activeTask == task) + d->activeTask = nullptr; + d->installationTaskList.removeOne(task); + + delete task; + triggerExecuteNextTask(); + }); + + if (qobject_cast<InstallationTask *>(task)) { + connect(static_cast<InstallationTask *>(task), &InstallationTask::finishedPackageExtraction, this, [this, task]() { + qCDebug(LogInstaller) << "emit blockingUntilInstallationAcknowledge" << task->id(); + emit taskBlockingUntilInstallationAcknowledge(task->id()); + + // we can now start the next download in parallel - the InstallationTask will take care + // of serializing the final installation steps on its own as soon as it gets the + // required acknowledge (or cancel). + if (d->activeTask == task) + d->activeTask = nullptr; + d->installationTaskList.append(task); + triggerExecuteNextTask(); + }); + } + + + d->activeTask = task; + task->setState(AsynchronousTask::Executing); + task->start(); +} + +void PackageManager::handleFailure(AsynchronousTask *task) +{ + qCDebug(LogInstaller) << "emit failed" << task->id() << task->errorCode() << task->errorString(); + emit taskFailed(task->id(), int(task->errorCode()), task->errorString()); +} + +bool PackageManager::startingPackageInstallation(PackageInfo *info) +{ + // ownership of info is transferred to PackageManager + QScopedPointer<PackageInfo> newInfo(info); + + if (!newInfo || newInfo->id().isEmpty()) + return false; + Package *package = fromId(newInfo->id()); +// if (!RuntimeFactory::instance()->manager(newInfo->runtimeName())) +// return false; + + if (package) { // update + if (!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<int> { IsUpdating }); + return true; +} + +bool PackageManager::finishedPackageInstall(const QString &id) +{ + Package *package = fromId(id); + if (!package) + return false; + + switch (package->state()) { + case Package::Installed: + return false; + + case Package::BeingInstalled: + case Package::BeingUpdated: { + // The Package object has been updated right at the start of the installation/update. + // Now's the time to update the InstallationReport that was written by the installer. + QFile irfile(QDir(package->info()->baseDir()).absoluteFilePath(qSL(".installation-report.yaml"))); + QScopedPointer<InstallationReport> ir(new InstallationReport(package->id())); + if (!irfile.open(QFile::ReadOnly) || !ir->deserialize(&irfile)) { + qCCritical(LogInstaller) << "Could not read the new installation-report for package" + << package->id() << "at" << irfile.fileName(); + return false; + } + package->info()->setInstallationReport(ir.take()); + package->setState(Package::Installed); + package->setProgress(0); + + emitDataChanged(package); + + 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<int> { 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 <QObject> +#include <QAbstractListModel> +#include <QtAppManCommon/global.h> +#include <QtAppManApplication/packageinfo.h> +#include <QtAppManManager/asynchronoustask.h> +#include <QtAppManManager/installationtask.h> +#include <QtAppManManager/deinstallationtask.h> + + +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<Package *> 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<int, QByteArray> 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<QByteArray> &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<int> &roles = QVector<int>()); + static void registerQmlTypes(); + + void triggerExecuteNextTask(); + QString enqueueTask(AsynchronousTask *task); + void handleFailure(AsynchronousTask *task); + + QList<QByteArray> 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<int, QByteArray> 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 <QMutex> +#include <QList> +#include <QSet> +#include <QScopedPointer> +#include <QThread> + +#include <QtAppManManager/packagemanager.h> +#include <QtAppManApplication/packagedatabase.h> +#include <QtAppManManager/asynchronoustask.h> +#include <QtAppManCommon/global.h> + +QT_BEGIN_NAMESPACE_AM + +bool removeRecursiveHelper(const QString &path); + +class PackageManagerPrivate +{ +public: + PackageDatabase *database = nullptr; + QVector<Package *> 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<QByteArray> chainOfTrust; + + QList<AsynchronousTask *> incomingTaskList; // incoming queue + QList<AsynchronousTask *> installationTaskList; // installation jobs in state >= AwaitingAcknowledge + AsynchronousTask *activeTask = nullptr; // currently active + + QList<AsynchronousTask *> allTasks() const + { + QList<AsynchronousTask *> 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 <QString> +#include <QDir> +#include <QFile> + +#include <QtAppManCommon/global.h> + +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 <QProcess> +#include <QDir> +#include <QFile> +#include <QtEndian> +#include <QDataStream> +#include <qplatformdefs.h> +#include <QDataStream> + +#include "logging.h" +#include "sudo.h" +#include "utilities.h" +#include "exception.h" +#include "global.h" + +#include <errno.h> + +#if defined(Q_OS_LINUX) +# include "processtitle.h" + +# include <fcntl.h> +# include <unistd.h> +# include <sys/socket.h> +# include <sys/errno.h> +# include <sys/ioctl.h> +# include <sys/stat.h> +# include <sys/prctl.h> +# include <linux/capability.h> + +// 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<uid_t>(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<gid_t>(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<size_t>(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 <typename R, typename C, typename ...Ps> 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 <QString> +#include <QByteArray> +#include <QMutex> +#include <qplatformdefs.h> + +#ifdef Q_OS_UNIX +# include <sys/types.h> +#else +typedef uint uid_t; +typedef uint gid_t; +//typedef uint mode_t; // already typedef'ed in qplatformdefs.h +#endif + +#include <QtAppManCommon/global.h> + +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 |