diff options
Diffstat (limited to 'launcher/updater')
20 files changed, 534 insertions, 595 deletions
diff --git a/launcher/updater/DownloadTask_test.cpp b/launcher/updater/DownloadTask_test.cpp deleted file mode 100644 index deba2632..00000000 --- a/launcher/updater/DownloadTask_test.cpp +++ /dev/null @@ -1,204 +0,0 @@ -#include <QTest> -#include <QSignalSpy> - -#include "updater/GoUpdate.h" -#include "updater/DownloadTask.h" -#include "updater/UpdateChecker.h" -#include <FileSystem.h> - -using namespace GoUpdate; - -FileSourceList encodeBaseFile(const char *suffix) -{ - auto base = QDir::currentPath(); - QUrl localFile = QUrl::fromLocalFile(base + suffix); - QString localUrlString = localFile.toString(QUrl::FullyEncoded); - auto item = FileSource("http", localUrlString); - return FileSourceList({item}); -} - -Q_DECLARE_METATYPE(VersionFileList) -Q_DECLARE_METATYPE(Operation) - -QDebug operator<<(QDebug dbg, const FileSource &f) -{ - dbg.nospace() << "FileSource(type=" << f.type << " url=" << f.url - << " comp=" << f.compressionType << ")"; - return dbg.maybeSpace(); -} - -QDebug operator<<(QDebug dbg, const VersionFileEntry &v) -{ - dbg.nospace() << "VersionFileEntry(path=" << v.path << " mode=" << v.mode - << " md5=" << v.md5 << " sources=" << v.sources << ")"; - return dbg.maybeSpace(); -} - -QDebug operator<<(QDebug dbg, const Operation::Type &t) -{ - switch (t) - { - case Operation::OP_REPLACE: - dbg << "OP_COPY"; - break; - case Operation::OP_DELETE: - dbg << "OP_DELETE"; - break; - } - return dbg.maybeSpace(); -} - -QDebug operator<<(QDebug dbg, const Operation &u) -{ - dbg.nospace() << "Operation(type=" << u.type << " file=" << u.source - << " dest=" << u.destination << " mode=" << u.destinationMode << ")"; - return dbg.maybeSpace(); -} - -class DownloadTaskTest : public QObject -{ - Q_OBJECT -private -slots: - void initTestCase() - { - } - void cleanupTestCase() - { - } - - void test_parseVersionInfo_data() - { - QFile f1(QFINDTESTDATA("testdata/1.json")); - f1.open(QFile::ReadOnly); - QByteArray data1 = f1.readAll(); - f1.close(); - - QFile f2(QFINDTESTDATA("testdata/2.json")); - f2.open(QFile::ReadOnly); - QByteArray data2 = f2.readAll(); - f2.close(); - - QTest::addColumn<QByteArray>("data"); - QTest::addColumn<VersionFileList>("list"); - QTest::addColumn<QString>("error"); - QTest::addColumn<bool>("ret"); - - QTest::newRow("one") - << data1 - << (VersionFileList() - << VersionFileEntry{"fileOne", - 493, - encodeBaseFile("/data/fileOneA"), - "9eb84090956c484e32cb6c08455a667b"} - << VersionFileEntry{"fileTwo", - 644, - encodeBaseFile("/data/fileTwo"), - "38f94f54fa3eb72b0ea836538c10b043"} - << VersionFileEntry{"fileThree", - 750, - encodeBaseFile("/data/fileThree"), - "f12df554b21e320be6471d7154130e70"}) - << QString() << true; - QTest::newRow("two") - << data2 - << (VersionFileList() - << VersionFileEntry{"fileOne", - 493, - encodeBaseFile("/data/fileOneB"), - "42915a71277c9016668cce7b82c6b577"} - << VersionFileEntry{"fileTwo", - 644, - encodeBaseFile("/data/fileTwo"), - "38f94f54fa3eb72b0ea836538c10b043"}) - << QString() << true; - } - void test_parseVersionInfo() - { - QFETCH(QByteArray, data); - QFETCH(VersionFileList, list); - QFETCH(QString, error); - QFETCH(bool, ret); - - VersionFileList outList; - QString outError; - bool outRet = parseVersionInfo(data, outList, outError); - QCOMPARE(outRet, ret); - QCOMPARE(outList, list); - QCOMPARE(outError, error); - } - - void test_processFileLists_data() - { - QTest::addColumn<QString>("tempFolder"); - QTest::addColumn<VersionFileList>("currentVersion"); - QTest::addColumn<VersionFileList>("newVersion"); - QTest::addColumn<OperationList>("expectedOperations"); - - QTemporaryDir tempFolderObj; - QString tempFolder = tempFolderObj.path(); - // update fileOne, keep fileTwo, remove fileThree - QTest::newRow("test 1") - << tempFolder << (VersionFileList() - << VersionFileEntry{ - QFINDTESTDATA("testdata/fileOne"), 493, - FileSourceList() - << FileSource( - "http", "http://host/path/fileOne-1"), - "9eb84090956c484e32cb6c08455a667b"} - << VersionFileEntry{ - QFINDTESTDATA("testdata/fileTwo"), 644, - FileSourceList() - << FileSource( - "http", "http://host/path/fileTwo-1"), - "38f94f54fa3eb72b0ea836538c10b043"} - << VersionFileEntry{ - QFINDTESTDATA("testdata/fileThree"), 420, - FileSourceList() - << FileSource( - "http", "http://host/path/fileThree-1"), - "f12df554b21e320be6471d7154130e70"}) - << (VersionFileList() - << VersionFileEntry{ - QFINDTESTDATA("testdata/fileOne"), 493, - FileSourceList() - << FileSource("http", - "http://host/path/fileOne-2"), - "42915a71277c9016668cce7b82c6b577"} - << VersionFileEntry{ - QFINDTESTDATA("testdata/fileTwo"), 644, - FileSourceList() - << FileSource("http", - "http://host/path/fileTwo-2"), - "38f94f54fa3eb72b0ea836538c10b043"}) - << (OperationList() - << Operation::DeleteOp(QFINDTESTDATA("testdata/fileThree")) - << Operation::CopyOp( - FS::PathCombine(tempFolder, - QFINDTESTDATA("data/fileOne").replace("/", "_")), - QFINDTESTDATA("data/fileOne"), 493)); - } - void test_processFileLists() - { - QFETCH(QString, tempFolder); - QFETCH(VersionFileList, currentVersion); - QFETCH(VersionFileList, newVersion); - QFETCH(OperationList, expectedOperations); - - OperationList operations; - - shared_qobject_ptr<QNetworkAccessManager> network = new QNetworkAccessManager(); - processFileLists(currentVersion, newVersion, QDir::currentPath(), tempFolder, new NetJob("Dummy", network), operations); - qDebug() << (operations == expectedOperations); - qDebug() << operations; - qDebug() << expectedOperations; - QCOMPARE(operations, expectedOperations); - } -}; - -extern "C" -{ - QTEST_GUILESS_MAIN(DownloadTaskTest) -} - -#include "DownloadTask_test.moc" diff --git a/launcher/updater/ExternalUpdater.h b/launcher/updater/ExternalUpdater.h new file mode 100644 index 00000000..a053e081 --- /dev/null +++ b/launcher/updater/ExternalUpdater.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Kenneth Chew <kenneth.c0@protonmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#ifndef LAUNCHER_EXTERNALUPDATER_H +#define LAUNCHER_EXTERNALUPDATER_H + +#include <QObject> + +/*! + * A base class for an updater that uses an external library. + * This class contains basic functions to control the updater. + * + * To implement the updater on a new platform, create a new class that inherits from this class and + * implement the pure virtual functions. + * + * The initializer of the new class should have the side effect of starting the automatic updater. That is, + * once the class is initialized, the program should automatically check for updates if necessary. + */ +class ExternalUpdater : public QObject +{ + Q_OBJECT + +public: + /*! + * Check for updates manually, showing the user a progress bar and an alert if no updates are found. + */ + virtual void checkForUpdates() = 0; + + /*! + * Indicates whether or not to check for updates automatically. + */ + virtual bool getAutomaticallyChecksForUpdates() = 0; + + /*! + * Indicates the current automatic update check interval in seconds. + */ + virtual double getUpdateCheckInterval() = 0; + + /*! + * Indicates whether or not beta updates should be checked for in addition to regular releases. + */ + virtual bool getBetaAllowed() = 0; + + /*! + * Set whether or not to check for updates automatically. + */ + virtual void setAutomaticallyChecksForUpdates(bool check) = 0; + + /*! + * Set the current automatic update check interval in seconds. + */ + virtual void setUpdateCheckInterval(double seconds) = 0; + + /*! + * Set whether or not beta updates should be checked for in addition to regular releases. + */ + virtual void setBetaAllowed(bool allowed) = 0; + +signals: + /*! + * Emits whenever the user's ability to check for updates changes. + * + * As per Sparkle documentation, "An update check can be made by the user when an update session isn’t in progress, + * or when an update or its progress is being shown to the user. A user cannot check for updates when data (such + * as the feed or an update) is still being downloaded automatically in the background. + * + * This property is suitable to use for menu item validation for seeing if checkForUpdates can be invoked." + */ + void canCheckForUpdatesChanged(bool canCheck); +}; + +#endif //LAUNCHER_EXTERNALUPDATER_H diff --git a/launcher/updater/MacSparkleUpdater.h b/launcher/updater/MacSparkleUpdater.h new file mode 100644 index 00000000..d50dbd68 --- /dev/null +++ b/launcher/updater/MacSparkleUpdater.h @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Kenneth Chew <kenneth.c0@protonmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#ifndef LAUNCHER_MACSPARKLEUPDATER_H +#define LAUNCHER_MACSPARKLEUPDATER_H + +#include <QObject> +#include <QSet> +#include "ExternalUpdater.h" + +/*! + * An implementation for the updater on macOS that uses the Sparkle framework. + */ +class MacSparkleUpdater : public ExternalUpdater +{ + Q_OBJECT + +public: + /*! + * Start the Sparkle updater, which automatically checks for updates if necessary. + */ + MacSparkleUpdater(); + ~MacSparkleUpdater() override; + + /*! + * Check for updates manually, showing the user a progress bar and an alert if no updates are found. + */ + void checkForUpdates() override; + + /*! + * Indicates whether or not to check for updates automatically. + */ + bool getAutomaticallyChecksForUpdates() override; + + /*! + * Indicates the current automatic update check interval in seconds. + */ + double getUpdateCheckInterval() override; + + /*! + * Indicates the set of Sparkle channels the updater is allowed to find new updates from. + */ + QSet<QString> getAllowedChannels(); + + /*! + * Indicates whether or not beta updates should be checked for in addition to regular releases. + */ + bool getBetaAllowed() override; + + /*! + * Set whether or not to check for updates automatically. + * + * As per Sparkle documentation, "By default, Sparkle asks users on second launch for permission if they want + * automatic update checks enabled and sets this property based on their response. If SUEnableAutomaticChecks is + * set in the Info.plist, this permission request is not performed however. + * + * Setting this property will persist in the host bundle’s user defaults. Only set this property if you need + * dynamic behavior (e.g. user preferences). + * + * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow + * reverting this property without kicking off a schedule change immediately." + */ + void setAutomaticallyChecksForUpdates(bool check) override; + + /*! + * Set the current automatic update check interval in seconds. + * + * As per Sparkle documentation, "Setting this property will persist in the host bundle’s user defaults. For this + * reason, only set this property if you need dynamic behavior (eg user preferences). Otherwise prefer to set + * SUScheduledCheckInterval directly in your Info.plist. + * + * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow + * reverting this property without kicking off a schedule change immediately." + */ + void setUpdateCheckInterval(double seconds) override; + + /*! + * Clears all allowed Sparkle channels, returning to the default updater channel behavior. + */ + void clearAllowedChannels(); + + /*! + * Set a single Sparkle channel the updater is allowed to find new updates from. + * + * Items in the default channel can always be found, regardless of this setting. If an empty string is passed, + * return to the default behavior. + */ + void setAllowedChannel(const QString& channel); + + /*! + * Set a set of Sparkle channels the updater is allowed to find new updates from. + * + * Items in the default channel can always be found, regardless of this setting. If an empty set is passed, + * return to the default behavior. + */ + void setAllowedChannels(const QSet<QString>& channels); + + /*! + * Set whether or not beta updates should be checked for in addition to regular releases. + */ + void setBetaAllowed(bool allowed) override; + +private: + class Private; + + Private *priv; + + void loadChannelsFromSettings(); +}; + +#endif //LAUNCHER_MACSPARKLEUPDATER_H diff --git a/launcher/updater/MacSparkleUpdater.mm b/launcher/updater/MacSparkleUpdater.mm new file mode 100644 index 00000000..ca6da55a --- /dev/null +++ b/launcher/updater/MacSparkleUpdater.mm @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Kenneth Chew <kenneth.c0@protonmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "MacSparkleUpdater.h" + +#include "Application.h" + +#include <Cocoa/Cocoa.h> +#include <Sparkle/Sparkle.h> + +@interface UpdaterObserver : NSObject + +@property(nonatomic, readonly) SPUUpdater* updater; + +/// A callback to run when the state of `canCheckForUpdates` for the `updater` changes. +@property(nonatomic, copy) void (^callback) (bool); + +- (id)initWithUpdater:(SPUUpdater*)updater; + +@end + +@implementation UpdaterObserver + +- (id)initWithUpdater:(SPUUpdater*)updater +{ + self = [super init]; + _updater = updater; + [self addObserver:self forKeyPath:@"updater.canCheckForUpdates" options:NSKeyValueObservingOptionNew context:nil]; + + return self; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary<NSKeyValueChangeKey, id> *)change + context:(void *)context +{ + if ([keyPath isEqualToString:@"updater.canCheckForUpdates"]) + { + bool canCheck = [change[NSKeyValueChangeNewKey] boolValue]; + self.callback(canCheck); + } +} + +@end + + +@interface UpdaterDelegate : NSObject <SPUUpdaterDelegate> + +@property(nonatomic, copy) NSSet<NSString *> *allowedChannels; + +@end + +@implementation UpdaterDelegate + +- (NSSet<NSString *> *)allowedChannelsForUpdater:(SPUUpdater *)updater +{ + return _allowedChannels; +} + +@end + + +class MacSparkleUpdater::Private +{ +public: + SPUStandardUpdaterController *updaterController; + UpdaterObserver *updaterObserver; + UpdaterDelegate *updaterDelegate; + NSAutoreleasePool *autoReleasePool; +}; + +MacSparkleUpdater::MacSparkleUpdater() +{ + priv = new MacSparkleUpdater::Private(); + + // Enable Cocoa's memory management. + NSApplicationLoad(); + priv->autoReleasePool = [[NSAutoreleasePool alloc] init]; + + // Delegate is used for setting/getting allowed update channels. + priv->updaterDelegate = [[UpdaterDelegate alloc] init]; + + // Controller is the interface for actually doing the updates. + priv->updaterController = [[SPUStandardUpdaterController alloc] initWithStartingUpdater:true + updaterDelegate:priv->updaterDelegate + userDriverDelegate:nil]; + + priv->updaterObserver = [[UpdaterObserver alloc] initWithUpdater:priv->updaterController.updater]; + // Use KVO to run a callback that emits a Qt signal when `canCheckForUpdates` changes, so the UI can respond accordingly. + priv->updaterObserver.callback = ^(bool canCheck) { + emit canCheckForUpdatesChanged(canCheck); + }; + + loadChannelsFromSettings(); +} + +MacSparkleUpdater::~MacSparkleUpdater() +{ + [priv->updaterObserver removeObserver:priv->updaterObserver forKeyPath:@"updater.canCheckForUpdates"]; + + [priv->updaterController release]; + [priv->updaterObserver release]; + [priv->updaterDelegate release]; + [priv->autoReleasePool release]; + delete priv; +} + +void MacSparkleUpdater::checkForUpdates() +{ + [priv->updaterController checkForUpdates:nil]; +} + +bool MacSparkleUpdater::getAutomaticallyChecksForUpdates() +{ + return priv->updaterController.updater.automaticallyChecksForUpdates; +} + +double MacSparkleUpdater::getUpdateCheckInterval() +{ + return priv->updaterController.updater.updateCheckInterval; +} + +QSet<QString> MacSparkleUpdater::getAllowedChannels() +{ + // Convert NSSet<NSString> -> QSet<QString> + __block QSet<QString> channels; + [priv->updaterDelegate.allowedChannels enumerateObjectsUsingBlock:^(NSString *channel, BOOL *stop) + { + channels.insert(QString::fromNSString(channel)); + }]; + return channels; +} + +bool MacSparkleUpdater::getBetaAllowed() +{ + return getAllowedChannels().contains("beta"); +} + +void MacSparkleUpdater::setAutomaticallyChecksForUpdates(bool check) +{ + priv->updaterController.updater.automaticallyChecksForUpdates = check ? YES : NO; // make clang-tidy happy +} + +void MacSparkleUpdater::setUpdateCheckInterval(double seconds) +{ + priv->updaterController.updater.updateCheckInterval = seconds; +} + +void MacSparkleUpdater::clearAllowedChannels() +{ + priv->updaterDelegate.allowedChannels = [NSSet set]; + APPLICATION->settings()->set("UpdateChannel", ""); +} + +void MacSparkleUpdater::setAllowedChannel(const QString &channel) +{ + if (channel.isEmpty()) + { + clearAllowedChannels(); + return; + } + + NSSet<NSString *> *nsChannels = [NSSet setWithObject:channel.toNSString()]; + priv->updaterDelegate.allowedChannels = nsChannels; + APPLICATION->settings()->set("UpdateChannel", channel); +} + +void MacSparkleUpdater::setAllowedChannels(const QSet<QString> &channels) +{ + if (channels.isEmpty()) + { + clearAllowedChannels(); + return; + } + + QString channelsConfig = ""; + // Convert QSet<QString> -> NSSet<NSString> + NSMutableSet<NSString *> *nsChannels = [NSMutableSet setWithCapacity:channels.count()]; + foreach (const QString channel, channels) + { + [nsChannels addObject:channel.toNSString()]; + channelsConfig += channel + " "; + } + + priv->updaterDelegate.allowedChannels = nsChannels; + APPLICATION->settings()->set("UpdateChannel", channelsConfig.trimmed()); +} + +void MacSparkleUpdater::setBetaAllowed(bool allowed) +{ + if (allowed) + { + setAllowedChannel("beta"); + } + else + { + clearAllowedChannels(); + } +} + +void MacSparkleUpdater::loadChannelsFromSettings() +{ + QStringList channelList = APPLICATION->settings()->get("UpdateChannel").toString().split(" "); + QSet<QString> channels(channelList.begin(), channelList.end()); + setAllowedChannels(channels); +} diff --git a/launcher/updater/UpdateChecker.cpp b/launcher/updater/UpdateChecker.cpp index efdb6093..fa6e5a97 100644 --- a/launcher/updater/UpdateChecker.cpp +++ b/launcher/updater/UpdateChecker.cpp @@ -24,7 +24,6 @@ #define CHANLIST_FORMAT 0 #include "BuildConfig.h" -#include "sys.h" UpdateChecker::UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel, int currentBuild) { @@ -32,6 +31,10 @@ UpdateChecker::UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QStr m_channelUrl = channelUrl; m_currentChannel = currentChannel; m_currentBuild = currentBuild; + +#ifdef Q_OS_MAC + m_externalUpdater = new MacSparkleUpdater(); +#endif } QList<UpdateChecker::ChannelListEntry> UpdateChecker::getChannelList() const @@ -44,71 +47,95 @@ bool UpdateChecker::hasChannels() const return !m_channels.isEmpty(); } -void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate) +ExternalUpdater* UpdateChecker::getExternalUpdater() { - qDebug() << "Checking for updates."; - - // If the channel list hasn't loaded yet, load it and defer checking for updates until - // later. - if (!m_chanListLoaded) - { - qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring update check."; - m_checkUpdateWaiting = true; - m_deferredUpdateChannel = updateChannel; - updateChanList(notifyNoUpdate); - return; - } + return m_externalUpdater; +} - if (m_updateChecking) +void UpdateChecker::checkForUpdate(const QString& updateChannel, bool notifyNoUpdate) +{ + if (m_externalUpdater) { - qDebug() << "Ignoring update check request. Already checking for updates."; - return; + m_externalUpdater->setBetaAllowed(updateChannel == "beta"); + if (notifyNoUpdate) + { + qDebug() << "Checking for updates."; + m_externalUpdater->checkForUpdates(); + } else + { + // The updater library already handles automatic update checks. + return; + } } - - // Find the desired channel within the channel list and get its repo URL. If if cannot be - // found, error. - QString stableUrl; - m_newRepoUrl = ""; - for (ChannelListEntry entry : m_channels) + else { - qDebug() << "channelEntry = " << entry.id; - if(entry.id == "stable") { - stableUrl = entry.url; + qDebug() << "Checking for updates."; + // If the channel list hasn't loaded yet, load it and defer checking for updates until + // later. + if (!m_chanListLoaded) + { + qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring update check."; + m_checkUpdateWaiting = true; + m_deferredUpdateChannel = updateChannel; + updateChanList(notifyNoUpdate); + return; } - if (entry.id == updateChannel) { - m_newRepoUrl = entry.url; - qDebug() << "is intended update channel: " << entry.id; + + if (m_updateChecking) + { + qDebug() << "Ignoring update check request. Already checking for updates."; + return; } - if (entry.id == m_currentChannel) { - m_currentRepoUrl = entry.url; - qDebug() << "is current update channel: " << entry.id; + + // Find the desired channel within the channel list and get its repo URL. If if cannot be + // found, error. + QString stableUrl; + m_newRepoUrl = ""; + for (ChannelListEntry entry: m_channels) + { + qDebug() << "channelEntry = " << entry.id; + if (entry.id == "stable") + { + stableUrl = entry.url; + } + if (entry.id == updateChannel) + { + m_newRepoUrl = entry.url; + qDebug() << "is intended update channel: " << entry.id; + } + if (entry.id == m_currentChannel) + { + m_currentRepoUrl = entry.url; + qDebug() << "is current update channel: " << entry.id; + } } - } - qDebug() << "m_repoUrl = " << m_newRepoUrl; + qDebug() << "m_repoUrl = " << m_newRepoUrl; - if (m_newRepoUrl.isEmpty()) { - qWarning() << "m_repoUrl was empty. defaulting to 'stable': " << stableUrl; - m_newRepoUrl = stableUrl; - } + if (m_newRepoUrl.isEmpty()) + { + qWarning() << "m_repoUrl was empty. defaulting to 'stable': " << stableUrl; + m_newRepoUrl = stableUrl; + } - // If nothing applies, error - if (m_newRepoUrl.isEmpty()) - { - qCritical() << "failed to select any update repository for: " << updateChannel; - emit updateCheckFailed(); - return; - } + // If nothing applies, error + if (m_newRepoUrl.isEmpty()) + { + qCritical() << "failed to select any update repository for: " << updateChannel; + emit updateCheckFailed(); + return; + } - m_updateChecking = true; + m_updateChecking = true; - QUrl indexUrl = QUrl(m_newRepoUrl).resolved(QUrl("index.json")); + QUrl indexUrl = QUrl(m_newRepoUrl).resolved(QUrl("index.json")); - indexJob = new NetJob("GoUpdate Repository Index", m_network); - indexJob->addNetAction(Net::Download::makeByteArray(indexUrl, &indexData)); - connect(indexJob.get(), &NetJob::succeeded, [this, notifyNoUpdate](){ updateCheckFinished(notifyNoUpdate); }); - connect(indexJob.get(), &NetJob::failed, this, &UpdateChecker::updateCheckFailed); - indexJob->start(); + indexJob = new NetJob("GoUpdate Repository Index", m_network); + indexJob->addNetAction(Net::Download::makeByteArray(indexUrl, &indexData)); + connect(indexJob.get(), &NetJob::succeeded, [this, notifyNoUpdate]() { updateCheckFinished(notifyNoUpdate); }); + connect(indexJob.get(), &NetJob::failed, this, &UpdateChecker::updateCheckFailed); + indexJob->start(); + } } void UpdateChecker::updateCheckFinished(bool notifyNoUpdate) diff --git a/launcher/updater/UpdateChecker.h b/launcher/updater/UpdateChecker.h index 13ee4efd..94e4312b 100644 --- a/launcher/updater/UpdateChecker.h +++ b/launcher/updater/UpdateChecker.h @@ -17,6 +17,11 @@ #include "net/NetJob.h" #include "GoUpdate.h" +#include "ExternalUpdater.h" + +#ifdef Q_OS_MAC +#include "MacSparkleUpdater.h" +#endif class UpdateChecker : public QObject { @@ -24,7 +29,7 @@ class UpdateChecker : public QObject public: UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel, int currentBuild); - void checkForUpdate(QString updateChannel, bool notifyNoUpdate); + void checkForUpdate(const QString& updateChannel, bool notifyNoUpdate); /*! * Causes the update checker to download the channel list from the URL specified in config.h (generated by CMake). @@ -54,6 +59,11 @@ public: */ bool hasChannels() const; + /*! + * Returns a pointer to an object that controls the external updater, or nullptr if an external updater is not used. + */ + ExternalUpdater *getExternalUpdater(); + signals: //! Signal emitted when an update is available. Passes the URL for the repo and the ID and name for the version. void updateAvailable(GoUpdate::Status status); @@ -117,5 +127,14 @@ private: QString m_currentRepoUrl; QString m_newRepoUrl; + + /*! + * If not a nullptr, then the updater here will be used instead of the old updater that uses GoUpdate when + * checking for updates. + * + * As a result, signals from this class won't be emitted, and most of the functions in this class other + * than checkForUpdate are not useful. Call functions from this external updater object instead. + */ + ExternalUpdater *m_externalUpdater = nullptr; }; diff --git a/launcher/updater/UpdateChecker_test.cpp b/launcher/updater/UpdateChecker_test.cpp deleted file mode 100644 index 70e3381f..00000000 --- a/launcher/updater/UpdateChecker_test.cpp +++ /dev/null @@ -1,148 +0,0 @@ -#include <QTest> -#include <QSignalSpy> - -#include "updater/UpdateChecker.h" - -Q_DECLARE_METATYPE(UpdateChecker::ChannelListEntry) - -bool operator==(const UpdateChecker::ChannelListEntry &e1, const UpdateChecker::ChannelListEntry &e2) -{ - qDebug() << e1.url << "vs" << e2.url; - return e1.id == e2.id && - e1.name == e2.name && - e1.description == e2.description && - e1.url == e2.url; -} - -QDebug operator<<(QDebug dbg, const UpdateChecker::ChannelListEntry &c) -{ - dbg.nospace() << "ChannelListEntry(id=" << c.id << " name=" << c.name << " description=" << c.description << " url=" << c.url << ")"; - return dbg.maybeSpace(); -} - -QString findTestDataUrl(const char *file) -{ - return QUrl::fromLocalFile(QFINDTESTDATA(file)).toString(); -} - -class UpdateCheckerTest : public QObject -{ - Q_OBJECT -private -slots: - void initTestCase() - { - - } - void cleanupTestCase() - { - - } - - void tst_ChannelListParsing_data() - { - QTest::addColumn<QString>("channel"); - QTest::addColumn<QString>("channelUrl"); - QTest::addColumn<bool>("hasChannels"); - QTest::addColumn<bool>("valid"); - QTest::addColumn<QList<UpdateChecker::ChannelListEntry> >("result"); - - QTest::newRow("garbage") - << QString() - << findTestDataUrl("testdata/garbageChannels.json") - << false - << false - << QList<UpdateChecker::ChannelListEntry>(); - QTest::newRow("errors") - << QString() - << findTestDataUrl("testdata/errorChannels.json") - << false - << true - << QList<UpdateChecker::ChannelListEntry>(); - QTest::newRow("no channels") - << QString() - << findTestDataUrl("testdata/noChannels.json") - << false - << true - << QList<UpdateChecker::ChannelListEntry>(); - QTest::newRow("one channel") - << QString("develop") - << findTestDataUrl("testdata/oneChannel.json") - << true - << true - << (QList<UpdateChecker::ChannelListEntry>() << UpdateChecker::ChannelListEntry{"develop", "Develop", "The channel called \"develop\"", "http://example.org/stuff"}); - QTest::newRow("several channels") - << QString("develop") - << findTestDataUrl("testdata/channels.json") - << true - << true - << (QList<UpdateChecker::ChannelListEntry>() - << UpdateChecker::ChannelListEntry{"develop", "Develop", "The channel called \"develop\"", findTestDataUrl("testdata")} - << UpdateChecker::ChannelListEntry{"stable", "Stable", "It's stable at least", findTestDataUrl("testdata")} - << UpdateChecker::ChannelListEntry{"42", "The Channel", "This is the channel that is going to answer all of your questions", "https://dent.me/tea"}); - } - void tst_ChannelListParsing() - { - - QFETCH(QString, channel); - QFETCH(QString, channelUrl); - QFETCH(bool, hasChannels); - QFETCH(bool, valid); - QFETCH(QList<UpdateChecker::ChannelListEntry>, result); - - shared_qobject_ptr<QNetworkAccessManager> nam = new QNetworkAccessManager(); - UpdateChecker checker(nam, channelUrl, channel, 0); - - QSignalSpy channelListLoadedSpy(&checker, SIGNAL(channelListLoaded())); - QVERIFY(channelListLoadedSpy.isValid()); - - checker.updateChanList(false); - - if (valid) - { - QVERIFY(channelListLoadedSpy.wait()); - QCOMPARE(channelListLoadedSpy.size(), 1); - } - else - { - channelListLoadedSpy.wait(); - QCOMPARE(channelListLoadedSpy.size(), 0); - } - - QCOMPARE(checker.hasChannels(), hasChannels); - QCOMPARE(checker.getChannelList(), result); - } - - void tst_UpdateChecking() - { - QString channel = "develop"; - QString channelUrl = findTestDataUrl("testdata/channels.json"); - int currentBuild = 2; - - shared_qobject_ptr<QNetworkAccessManager> nam = new QNetworkAccessManager(); - UpdateChecker checker(nam, channelUrl, channel, currentBuild); - - QSignalSpy updateAvailableSpy(&checker, SIGNAL(updateAvailable(GoUpdate::Status))); - QVERIFY(updateAvailableSpy.isValid()); - QSignalSpy channelListLoadedSpy(&checker, SIGNAL(channelListLoaded())); - QVERIFY(channelListLoadedSpy.isValid()); - - checker.updateChanList(false); - QVERIFY(channelListLoadedSpy.wait()); - - qDebug() << "CWD:" << QDir::current().absolutePath(); - checker.m_channels[0].url = findTestDataUrl("testdata/"); - checker.checkForUpdate(channel, false); - - QVERIFY(updateAvailableSpy.wait()); - - auto status = updateAvailableSpy.first().first().value<GoUpdate::Status>(); - QCOMPARE(checker.m_channels[0].url, status.newRepoUrl); - QCOMPARE(3, status.newVersionId); - QCOMPARE(currentBuild, status.currentVersionId); - } -}; - -QTEST_GUILESS_MAIN(UpdateCheckerTest) - -#include "UpdateChecker_test.moc" diff --git a/launcher/updater/testdata/1.json b/launcher/updater/testdata/1.json deleted file mode 100644 index 7af7e52d..00000000 --- a/launcher/updater/testdata/1.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "ApiVersion": 0, - "Id": 1, - "Name": "1.0.1", - "Files": [ - { - "Path": "fileOne", - "Sources": [ - { - "SourceType": "http", - "Url": "@TEST_DATA_URL@/fileOneA" - } - ], - "Executable": true, - "Perms": 493, - "MD5": "9eb84090956c484e32cb6c08455a667b" - }, - { - "Path": "fileTwo", - "Sources": [ - { - "SourceType": "http", - "Url": "@TEST_DATA_URL@/fileTwo" - } - ], - "Executable": false, - "Perms": 644, - "MD5": "38f94f54fa3eb72b0ea836538c10b043" - }, - { - "Path": "fileThree", - "Sources": [ - { - "SourceType": "http", - "Url": "@TEST_DATA_URL@/fileThree" - } - ], - "Executable": false, - "Perms": "750", - "MD5": "f12df554b21e320be6471d7154130e70" - } - ] -} diff --git a/launcher/updater/testdata/2.json b/launcher/updater/testdata/2.json deleted file mode 100644 index 96d430d5..00000000 --- a/launcher/updater/testdata/2.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "ApiVersion": 0, - "Id": 1, - "Name": "1.0.1", - "Files": [ - { - "Path": "fileOne", - "Sources": [ - { - "SourceType": "http", - "Url": "@TEST_DATA_URL@/fileOneB" - } - ], - "Executable": true, - "Perms": 493, - "MD5": "42915a71277c9016668cce7b82c6b577" - }, - { - "Path": "fileTwo", - "Sources": [ - { - "SourceType": "http", - "Url": "@TEST_DATA_URL@/fileTwo" - } - ], - "Executable": false, - "Perms": 644, - "MD5": "38f94f54fa3eb72b0ea836538c10b043" - } - ] -} diff --git a/launcher/updater/testdata/channels.json b/launcher/updater/testdata/channels.json deleted file mode 100644 index 5c6e42cb..00000000 --- a/launcher/updater/testdata/channels.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "format_version": 0, - "channels": [ - { - "id": "develop", - "name": "Develop", - "description": "The channel called \"develop\"", - "url": "@TEST_DATA_URL@" - }, - { - "id": "stable", - "name": "Stable", - "description": "It's stable at least", - "url": "@TEST_DATA_URL@" - }, - { - "id": "42", - "name": "The Channel", - "description": "This is the channel that is going to answer all of your questions", - "url": "https://dent.me/tea" - } - ] -} diff --git a/launcher/updater/testdata/errorChannels.json b/launcher/updater/testdata/errorChannels.json deleted file mode 100644 index a2cb2165..00000000 --- a/launcher/updater/testdata/errorChannels.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "format_version": 0, - "channels": [ - { - "id": "", - "name": "Develop", - "description": "The channel called \"develop\"", - "url": "http://example.org/stuff" - }, - { - "id": "stable", - "name": "", - "description": "It's stable at least", - "url": "ftp://username@host/path/to/stuff" - }, - { - "id": "42", - "name": "The Channel", - "description": "This is the channel that is going to answer all of your questions", - "url": "" - } - ] -} diff --git a/launcher/updater/testdata/fileOneA b/launcher/updater/testdata/fileOneA deleted file mode 100644 index f2e41136..00000000 --- a/launcher/updater/testdata/fileOneA +++ /dev/null @@ -1 +0,0 @@ -stuff diff --git a/launcher/updater/testdata/fileOneB b/launcher/updater/testdata/fileOneB deleted file mode 100644 index f9aba922..00000000 --- a/launcher/updater/testdata/fileOneB +++ /dev/null @@ -1,3 +0,0 @@ -stuff - -more stuff that came in the new version diff --git a/launcher/updater/testdata/fileThree b/launcher/updater/testdata/fileThree deleted file mode 100644 index 6353ff16..00000000 --- a/launcher/updater/testdata/fileThree +++ /dev/null @@ -1 +0,0 @@ -this is yet another file diff --git a/launcher/updater/testdata/fileTwo b/launcher/updater/testdata/fileTwo deleted file mode 100644 index aad9a93a..00000000 --- a/launcher/updater/testdata/fileTwo +++ /dev/null @@ -1 +0,0 @@ -some other stuff diff --git a/launcher/updater/testdata/garbageChannels.json b/launcher/updater/testdata/garbageChannels.json deleted file mode 100644 index 34437451..00000000 --- a/launcher/updater/testdata/garbageChannels.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "format_version": 0, - "channels": [ - { - "id": "develop", - "name": "Develop", - "description": "The channel called \"develop\"", -aa "url": "http://example.org/stuff" - }, -a "id": "stable", - "name": "Stable", - "description": "It's stable at least", - "url": "ftp://username@host/path/to/stuff" - }, - { - "id": "42"f - "name": "The Channel", - "description": "This is the channel that is going to answer all of your questions", - "url": "https://dent.me/tea" - } - ] -} diff --git a/launcher/updater/testdata/index.json b/launcher/updater/testdata/index.json deleted file mode 100644 index 867bdcfb..00000000 --- a/launcher/updater/testdata/index.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ApiVersion": 0, - "Versions": [ - { "Id": 0, "Name": "1.0.0" }, - { "Id": 1, "Name": "1.0.1" }, - { "Id": 2, "Name": "1.0.2" }, - { "Id": 3, "Name": "1.0.3" } - ] -} diff --git a/launcher/updater/testdata/noChannels.json b/launcher/updater/testdata/noChannels.json deleted file mode 100644 index 76988982..00000000 --- a/launcher/updater/testdata/noChannels.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "format_version": 0, - "channels": [ - ] -} diff --git a/launcher/updater/testdata/oneChannel.json b/launcher/updater/testdata/oneChannel.json deleted file mode 100644 index cc8ed255..00000000 --- a/launcher/updater/testdata/oneChannel.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "format_version": 0, - "channels": [ - { - "id": "develop", - "name": "Develop", - "description": "The channel called \"develop\"", - "url": "http://example.org/stuff" - } - ] -} diff --git a/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml b/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml deleted file mode 100644 index 38ecc809..00000000 --- a/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml +++ /dev/null @@ -1,17 +0,0 @@ -<update version="3"> - <install> - <file> - <source>sourceOne</source> - <dest>destOne</dest> - <mode>0777</mode> - </file> - <file> - <source>PolyMC.exe</source> - <dest>P/o/l/y/M/C/e/x/e</dest> - <mode>0644</mode> - </file> - </install> - <uninstall> - <file>toDelete.abc</file> - </uninstall> -</update> |