aboutsummaryrefslogtreecommitdiff
path: root/launcher/updater
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/updater')
-rw-r--r--launcher/updater/DownloadTask_test.cpp204
-rw-r--r--launcher/updater/ExternalUpdater.h87
-rw-r--r--launcher/updater/MacSparkleUpdater.h126
-rw-r--r--launcher/updater/MacSparkleUpdater.mm222
-rw-r--r--launcher/updater/UpdateChecker.cpp131
-rw-r--r--launcher/updater/UpdateChecker.h21
-rw-r--r--launcher/updater/UpdateChecker_test.cpp148
-rw-r--r--launcher/updater/testdata/1.json43
-rw-r--r--launcher/updater/testdata/2.json31
-rw-r--r--launcher/updater/testdata/channels.json23
-rw-r--r--launcher/updater/testdata/errorChannels.json23
-rw-r--r--launcher/updater/testdata/fileOneA1
-rw-r--r--launcher/updater/testdata/fileOneB3
-rw-r--r--launcher/updater/testdata/fileThree1
-rw-r--r--launcher/updater/testdata/fileTwo1
-rw-r--r--launcher/updater/testdata/garbageChannels.json22
-rw-r--r--launcher/updater/testdata/index.json9
-rw-r--r--launcher/updater/testdata/noChannels.json5
-rw-r--r--launcher/updater/testdata/oneChannel.json11
-rw-r--r--launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml17
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>