summaryrefslogtreecommitdiffstats
path: root/src/libs/kdtools/updatefinder.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/libs/kdtools/updatefinder.cpp')
-rw-r--r--src/libs/kdtools/updatefinder.cpp639
1 files changed, 639 insertions, 0 deletions
diff --git a/src/libs/kdtools/updatefinder.cpp b/src/libs/kdtools/updatefinder.cpp
new file mode 100644
index 000000000..25e587b17
--- /dev/null
+++ b/src/libs/kdtools/updatefinder.cpp
@@ -0,0 +1,639 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Klaralvdalens Datakonsult AB (KDAB)
+** Copyright (C) 2015 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the Qt Installer Framework.
+**
+** $QT_BEGIN_LICENSE:LGPL21$
+** Commercial License Usage
+** Licensees holding valid commercial Qt 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 http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://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 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** As a special exception, The Qt Company gives you certain additional
+** rights. These rights are described in The Qt Company LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "updatefinder.h"
+#include "update.h"
+#include "filedownloader.h"
+#include "filedownloaderfactory.h"
+#include "updatesinfo_p.h"
+#include "localpackagehub.h"
+
+#include "fileutils.h"
+#include "globals.h"
+
+#include <QCoreApplication>
+#include <QFileInfo>
+
+using namespace KDUpdater;
+using namespace QInstaller;
+
+/*!
+ \inmodule kdupdater
+ \class KDUpdater::UpdateFinder
+ \brief The UpdaterFinder class finds updates applicable for installed packages.
+
+ The KDUpdater::UpdateFinder class helps in searching for updates and installing them on the
+ application. The class basically processes the application's KDUpdater::PackagesInfo and the
+ UpdateXMLs it aggregates from all the update sources and populates a list of KDUpdater::Update
+ objects.
+*/
+
+//
+// Private
+//
+class UpdateFinder::Private
+{
+public:
+ enum struct Resolution {
+ AddPackage,
+ KeepExisting,
+ RemoveExisting
+ };
+
+ Private(UpdateFinder *qq)
+ : q(qq)
+ , downloadCompleteCount(0)
+ , m_downloadsToComplete(0)
+ {}
+
+ ~Private()
+ {
+ clear();
+ }
+
+ struct Data {
+ Data()
+ : downloader(0) {}
+ Data(const PackageSource &i, FileDownloader *d = 0)
+ : info(i), downloader(d) {}
+
+ PackageSource info;
+ FileDownloader *downloader;
+ };
+ UpdateFinder *q;
+ QHash<QString, Update *> updates;
+
+ // Temporary structure that notes down information about updates.
+ bool cancel;
+ int downloadCompleteCount;
+ int m_downloadsToComplete;
+ QHash<UpdatesInfo *, Data> m_updatesInfoList;
+
+ void clear();
+ void computeUpdates();
+ void cancelComputeUpdates();
+ bool downloadUpdateXMLFiles();
+ bool computeApplicableUpdates();
+
+ QList<UpdateInfo> applicableUpdates(UpdatesInfo *updatesInfo);
+ void createUpdateObjects(const PackageSource &source, const QList<UpdateInfo> &updateInfoList);
+ Resolution checkPriorityAndVersion(const PackageSource &source, const QVariantHash &data) const;
+ void slotDownloadDone();
+
+ QSet<PackageSource> packageSources;
+ std::weak_ptr<LocalPackageHub> m_localPackageHub;
+};
+
+
+static int computeProgressPercentage(int min, int max, int percent)
+{
+ return min + qint64(max-min) * percent / 100;
+}
+
+static int computePercent(int done, int total)
+{
+ return total ? done * Q_INT64_C(100) / total : 0 ;
+}
+
+/*!
+ \internal
+
+ Releases all internal resources consumed while downloading and computing updates.
+*/
+void UpdateFinder::Private::clear()
+{
+ qDeleteAll(updates);
+ updates.clear();
+
+ const QList<Data> values = m_updatesInfoList.values();
+ foreach (const Data &data, values)
+ delete data.downloader;
+
+ qDeleteAll(m_updatesInfoList.keys());
+ m_updatesInfoList.clear();
+
+ downloadCompleteCount = 0;
+ m_downloadsToComplete = 0;
+}
+
+/*!
+ \internal
+
+ This method computes the updates that can be applied on the application by
+ studying the application's KDUpdater::PackagesInfo object and the UpdateXML files
+ from each of the update sources described in QInstaller::PackageSource.
+
+ This function can take a long time to complete. The following signals are emitted
+ during the execution of this function
+
+ The function creates KDUpdater::Update objects on the stack. All KDUpdater::Update objects
+ are made children of the application associated with this finder.
+
+ The update sources are fetched from the QInstaller::PackageSource object associated with
+ the application. Package information is extracted from the KDUpdater::PackagesInfo object
+ associated with the application.
+
+ \note Each time this function is called, all the previously computed updates are discarded
+ and its resources are freed.
+*/
+void UpdateFinder::Private::computeUpdates()
+{
+ // Computing updates is done in two stages
+ // 1. Downloading Update XML files from all the update sources
+ // 2. Matching updates with Package XML and figuring out available updates
+
+ clear();
+ cancel = false;
+
+ // First do some quick sanity checks on the packages info
+ std::shared_ptr<LocalPackageHub> packages = m_localPackageHub.lock();
+ if (!packages) {
+ q->reportError(tr("Cannot access the package information of this application."));
+ return;
+ }
+
+ if (!packages->isValid()) {
+ q->reportError(packages->errorString());
+ return;
+ }
+
+ // Now do some quick sanity checks on the package sources.
+ if (packageSources.count() <= 0) {
+ q->reportError(tr("No package sources set for this application."));
+ return;
+ }
+
+ // Now we can start...
+
+ // Step 1: 0 - 49 percent
+ if (!downloadUpdateXMLFiles() || cancel) {
+ clear();
+ return;
+ }
+
+ // Step 2: 50 - 100 percent
+ if (!computeApplicableUpdates() || cancel) {
+ clear();
+ return;
+ }
+
+ // All done
+ q->reportProgress(100, tr("%n update(s) found.", "", updates.count()));
+ q->reportDone();
+}
+
+/*!
+ \internal
+
+ Cancels the computation of updates.
+
+ \sa computeUpdates()
+*/
+void UpdateFinder::Private::cancelComputeUpdates()
+{
+ cancel = true;
+}
+
+/*!
+ \internal
+
+ This function downloads Updates.xml from all the update sources except local files.
+ A single application can potentially have several update sources, hence we need to be
+ asynchronous in downloading updates from different sources.
+
+ The function basically does this for each update source:
+ a) Create a KDUpdater::FileDownloader and KDUpdater::UpdatesInfo for each update
+ b) Triggers the download of Updates.xml from each file downloader.
+ c) The downloadCompleted(), downloadCanceled() and downloadAborted() signals are connected
+ in each of the downloaders. Once all the downloads are complete and/or aborted, the next stage
+ would be done.
+
+ The function gets into an event loop until all the downloads are complete.
+*/
+bool UpdateFinder::Private::downloadUpdateXMLFiles()
+{
+ // create UpdatesInfo for each update source
+ foreach (const PackageSource &info, packageSources) {
+ const QUrl url = QString::fromLatin1("%1/Updates.xml").arg(info.url.toString());
+ if (url.scheme() != QLatin1String("resource") && url.scheme() != QLatin1String("file")) {
+ // create FileDownloader (except for local files and resources)
+ FileDownloader *downloader = FileDownloaderFactory::instance().create(url.scheme(), q);
+ if (!downloader)
+ break;
+
+ downloader->setUrl(url);
+ downloader->setAutoRemoveDownloadedFile(true);
+ connect(downloader, SIGNAL(downloadCanceled()), q, SLOT(slotDownloadDone()));
+ connect(downloader, SIGNAL(downloadCompleted()), q, SLOT(slotDownloadDone()));
+ connect(downloader, SIGNAL(downloadAborted(QString)), q, SLOT(slotDownloadDone()));
+ m_updatesInfoList.insert(new UpdatesInfo, Data(info, downloader));
+ } else {
+ UpdatesInfo *updatesInfo = new UpdatesInfo;
+ updatesInfo->setFileName(QInstaller::pathFromUrl(url));
+ m_updatesInfoList.insert(updatesInfo, Data(info));
+ }
+ }
+
+ // Trigger download of Updates.xml file
+ downloadCompleteCount = 0;
+ m_downloadsToComplete = 0;
+ foreach (const Data &data, m_updatesInfoList) {
+ if (data.downloader) {
+ m_downloadsToComplete++;
+ data.downloader->download();
+ }
+ }
+
+ // Wait until all downloaders have completed their downloads.
+ while (true) {
+ QCoreApplication::processEvents();
+ if (cancel)
+ return false;
+
+ if (downloadCompleteCount == m_downloadsToComplete)
+ break;
+
+ q->reportProgress(computePercent(downloadCompleteCount, m_downloadsToComplete),
+ tr("Downloading Updates.xml from update sources."));
+ }
+
+ // Setup the update info objects with the files from download.
+ foreach (UpdatesInfo *updatesInfo, m_updatesInfoList.keys()) {
+ const Data data = m_updatesInfoList.value(updatesInfo);
+ if (data.downloader) {
+ if (!data.downloader->isDownloaded()) {
+ q->reportError(tr("Cannot download package source %1 from \"%2\".").arg(data
+ .downloader->url().fileName(), data.info.url.toString()));
+ } else {
+ updatesInfo->setFileName(data.downloader->downloadedFileName());
+ }
+ }
+ }
+
+ // Remove all invalid update info objects.
+ QMutableHashIterator<UpdatesInfo *, Data> it(m_updatesInfoList);
+ while (it.hasNext()) {
+ UpdatesInfo *info = it.next().key();
+ if (info->isValid())
+ continue;
+
+ q->reportError(info->errorString());
+ delete info;
+ it.remove();
+ }
+
+ if (m_updatesInfoList.isEmpty())
+ return false;
+
+ q->reportProgress(49, tr("Updates.xml file(s) downloaded from update sources."));
+ return true;
+}
+
+/*!
+ \internal
+
+ This function runs through all the KDUpdater::UpdatesInfo objects created during
+ the downloadUpdateXMLFiles() method and compares it with the data contained in
+ KDUpdater::PackagesInfo. Thereby figures out whether an update is applicable for
+ this application or not.
+*/
+bool UpdateFinder::Private::computeApplicableUpdates()
+{
+ int i = 0;
+ foreach (UpdatesInfo *updatesInfo, m_updatesInfoList.keys()) {
+ // Fetch updates applicable to this application.
+ QList<UpdateInfo> updates = applicableUpdates(updatesInfo);
+ if (!updates.count())
+ continue;
+
+ if (cancel)
+ return false;
+ const PackageSource updateSource = m_updatesInfoList.value(updatesInfo).info;
+
+ // Create Update objects for updates that have a valid
+ // UpdateFile
+ createUpdateObjects(updateSource, updates);
+ if (cancel)
+ return false;
+
+ // Report progress
+ q->reportProgress(computeProgressPercentage(51, 100, computePercent(i,
+ m_updatesInfoList.count())), tr("Computing applicable updates."));
+ ++i;
+ }
+
+ q->reportProgress(99, tr("Application updates computed."));
+ return true;
+}
+
+QList<UpdateInfo> UpdateFinder::Private::applicableUpdates(UpdatesInfo *updatesInfo)
+{
+ const QList<UpdateInfo> dummy;
+ if (!updatesInfo || updatesInfo->updateInfoCount() == 0)
+ return dummy;
+
+ std::shared_ptr<LocalPackageHub> packages = m_localPackageHub.lock();
+ if (!packages)
+ return dummy;
+
+ // Check to see if the updates info contains updates for any application
+ if (updatesInfo->applicationName() != QLatin1String("{AnyApplication}")) {
+ // updatesInfo->applicationName() describes one application or a series of
+ // application names separated by commas.
+ QString appName = updatesInfo->applicationName();
+ appName = appName.replace(QLatin1String( ", " ), QLatin1String( "," ));
+ appName = appName.replace(QLatin1String( " ," ), QLatin1String( "," ));
+
+ // Catch hold of app names contained updatesInfo->applicationName()
+ // If the application appName isn't one of the app names, then the updates are not applicable.
+ const QStringList apps = appName.split(QInstaller::commaRegExp(), QString::SkipEmptyParts);
+ if (apps.indexOf([&packages] { return packages->isValid() ? packages->applicationName()
+ : QCoreApplication::applicationName(); } ()) < 0) {
+ return dummy;
+ }
+ }
+ return updatesInfo->updatesInfo();
+}
+
+void UpdateFinder::Private::createUpdateObjects(const PackageSource &source,
+ const QList<UpdateInfo> &updateInfoList)
+{
+ foreach (const UpdateInfo &info, updateInfoList) {
+ const Resolution value = checkPriorityAndVersion(source, info.data);
+ if (value == Resolution::KeepExisting)
+ continue;
+
+ const QString name = info.data.value(QLatin1String("Name")).toString();
+ if (value == Resolution::RemoveExisting)
+ delete updates.take(name);
+
+ // Create and register the update
+ updates.insert(name, new Update(source, info));
+ }
+}
+
+/*
+ If a package of the same name exists, always use the one with the higher
+ version. If the new package has the same version but a higher
+ priority, use the new new package, otherwise keep the already existing package.
+*/
+UpdateFinder::Private::Resolution UpdateFinder::Private::checkPriorityAndVersion(
+ const PackageSource &source, const QVariantHash &newPackage) const
+{
+ const QString name = newPackage.value(QLatin1String("Name")).toString();
+ if (Update *existingPackage = updates.value(name)) {
+ // Bingo, package was previously found elsewhere.
+
+ const int match = compareVersion(newPackage.value(QLatin1String("Version")).toString(),
+ existingPackage->data(QLatin1String("Version")).toString());
+
+ if (match > 0) {
+ // new package has higher version, use
+ qDebug().nospace() << "Remove Package 'Name: " << name << ", Version: "
+ << existingPackage->data(QLatin1String("Version")).toString()
+ << ", Source: " << QFileInfo(existingPackage->packageSource().url.toLocalFile()).fileName()
+ << "' found a package with higher version 'Name: "
+ << name << ", Version: " << newPackage.value(QLatin1String("Version")).toString()
+ << ", Source: " << QFileInfo(source.url.toLocalFile()).fileName() << "'";
+ return Resolution::RemoveExisting;
+ }
+
+ if ((match == 0) && (source.priority > existingPackage->packageSource().priority)) {
+ // new package version equals but priority is higher, use
+ qDebug().nospace() << "Remove Package 'Name: " << name << ", Priority: "
+ << existingPackage->packageSource().priority
+ << ", Source: " << QFileInfo(existingPackage->packageSource().url.toLocalFile()).fileName()
+ << "' found a package with higher priority 'Name: "
+ << name << ", Priority: " << source.priority
+ << ", Source: " << QFileInfo(source.url.toLocalFile()).fileName() << "'";
+ return Resolution::RemoveExisting;
+ }
+ return Resolution::KeepExisting; // otherwise keep existing
+ }
+ return Resolution::AddPackage;
+}
+
+//
+// UpdateFinder
+//
+
+/*!
+ Constructs an update finder.
+*/
+UpdateFinder::UpdateFinder()
+ : Task(QLatin1String("UpdateFinder"), Stoppable),
+ d(new Private(this))
+{
+}
+
+/*!
+ Destructor
+*/
+UpdateFinder::~UpdateFinder()
+{
+ delete d;
+}
+
+/*!
+ Returns a list of KDUpdater::Update objects.
+*/
+QList<Update *> UpdateFinder::updates() const
+{
+ return d->updates.values();
+}
+
+void UpdateFinder::setLocalPackageHub(std::weak_ptr<LocalPackageHub> hub)
+{
+ d->m_localPackageHub = std::move(hub);
+}
+
+/*!
+ Sets the package sources information to use when searching for applicable packages.
+*/
+void UpdateFinder::setPackageSources(const QSet<PackageSource> &sources)
+{
+ d->packageSources = sources;
+}
+
+/*!
+ \internal
+
+ Implemented from KDUpdater::Task::doRun().
+*/
+void UpdateFinder::doRun()
+{
+ d->computeUpdates();
+}
+
+/*!
+ \internal
+
+ Implemented from KDUpdater::Task::doStop().
+*/
+bool UpdateFinder::doStop()
+{
+ d->cancelComputeUpdates();
+
+ // Wait until the cancel has actually happened, and then return.
+ // Thinking of using QMutex for this. Frank/Till any suggestions?
+
+ return true;
+}
+
+/*!
+ \internal
+
+ Implemented from KDUpdater::Task::doStop().
+*/
+bool UpdateFinder::doPause()
+{
+ // Not a pausable task
+ return false;
+}
+
+/*!
+ \internal
+
+ Implemented from KDUpdater::Task::doStop().
+*/
+bool UpdateFinder::doResume()
+{
+ // Not a pausable task, hence it is not resumable as well
+ return false;
+}
+
+/*!
+ \internal
+*/
+void UpdateFinder::Private::slotDownloadDone()
+{
+ ++downloadCompleteCount;
+
+ int pc = computePercent(downloadCompleteCount, m_downloadsToComplete);
+ pc = computeProgressPercentage(0, 45, pc);
+ q->reportProgress( pc, tr("Downloading Updates.xml from update sources.") );
+}
+
+
+/*!
+ \inmodule kdupdater
+
+ This function compares two version strings \c v1 and \c v2 and returns
+ -1, 0 or +1 based on the following rule
+
+ \list
+ \li Returns 0 if v1 == v2
+ \li Returns -1 if v1 < v2
+ \li Returns +1 if v1 > v2
+ \endlist
+
+ The function is very similar to \c strcmp(), except that it works on version strings.
+
+ Example:
+ \code
+
+ KDUpdater::compareVersion("2.0", "2.1"); // Returns -1
+ KDUpdater::compareVersion("2.1", "2.0"); // Returns +1
+ KDUpdater::compareVersion("2.0", "2.0"); // Returns 0
+ KDUpdater::compareVersion("2.1", "2.1"); // Returns 0
+
+ KDUpdater::compareVersion("2.0", "2.x"); // Returns 0
+ KDUpdater::compareVersion("2.x", "2.0"); // Returns 0
+
+ KDUpdater::compareVersion("2.0.12.4", "2.1.10.4"); // Returns -1
+ KDUpdater::compareVersion("2.0.12.x", "2.0.x"); // Returns 0
+ KDUpdater::compareVersion("2.1.12.x", "2.0.x"); // Returns +1
+ KDUpdater::compareVersion("2.1.12.x", "2.x"); // Returns 0
+ KDUpdater::compareVersion("2.x", "2.1.12.x"); // Returns 0
+
+ \endcode
+*/
+int KDUpdater::compareVersion(const QString &v1, const QString &v2)
+{
+ // For tests refer VersionCompareFnTest testcase.
+
+ // Check for equality
+ if (v1 == v2)
+ return 0;
+
+ // Split version numbers across "."
+ const QStringList v1_comps = v1.split(QRegExp(QLatin1String( "\\.|-")));
+ const QStringList v2_comps = v2.split(QRegExp(QLatin1String( "\\.|-")));
+
+ // Check each component of the version
+ int index = 0;
+ while (true) {
+ if (index == v1_comps.count() && index < v2_comps.count())
+ return -1;
+ if (index < v1_comps.count() && index == v2_comps.count())
+ return +1;
+ if (index >= v1_comps.count() || index >= v2_comps.count())
+ break;
+
+ bool v1_ok, v2_ok;
+ int v1_comp = v1_comps[index].toInt(&v1_ok);
+ int v2_comp = v2_comps[index].toInt(&v2_ok);
+
+ if (!v1_ok) {
+ if (v1_comps[index] == QLatin1String("x"))
+ return 0;
+ }
+ if (!v2_ok) {
+ if (v2_comps[index] == QLatin1String("x"))
+ return 0;
+ }
+ if (!v1_ok && !v2_ok)
+ return v1_comps[index].compare(v2_comps[index]);
+
+ if (v1_comp < v2_comp)
+ return -1;
+
+ if (v1_comp > v2_comp)
+ return +1;
+
+ // v1_comp == v2_comp
+ ++index;
+ }
+
+ if (index < v2_comps.count())
+ return +1;
+
+ if (index < v1_comps.count())
+ return -1;
+
+ // Controversial return. I hope this never happens.
+ return 0;
+}
+
+#include "moc_updatefinder.cpp"