summaryrefslogtreecommitdiffstats
path: root/logic/updater
diff options
context:
space:
mode:
Diffstat (limited to 'logic/updater')
-rw-r--r--logic/updater/DownloadUpdateTask.cpp543
-rw-r--r--logic/updater/DownloadUpdateTask.h217
-rw-r--r--logic/updater/NotificationChecker.cpp121
-rw-r--r--logic/updater/NotificationChecker.h54
-rw-r--r--logic/updater/UpdateChecker.cpp263
-rw-r--r--logic/updater/UpdateChecker.h111
6 files changed, 1309 insertions, 0 deletions
diff --git a/logic/updater/DownloadUpdateTask.cpp b/logic/updater/DownloadUpdateTask.cpp
new file mode 100644
index 00000000..83679f19
--- /dev/null
+++ b/logic/updater/DownloadUpdateTask.cpp
@@ -0,0 +1,543 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "DownloadUpdateTask.h"
+
+#include "MultiMC.h"
+#include "logic/updater/UpdateChecker.h"
+#include "logic/net/NetJob.h"
+#include "pathutils.h"
+
+#include <QFile>
+#include <QTemporaryDir>
+#include <QCryptographicHash>
+
+#include <QDomDocument>
+
+DownloadUpdateTask::DownloadUpdateTask(QString repoUrl, int versionId, QObject *parent)
+ : Task(parent)
+{
+ m_cVersionId = MMC->version().build;
+
+ m_nRepoUrl = repoUrl;
+ m_nVersionId = versionId;
+
+ m_updateFilesDir.setAutoRemove(false);
+}
+
+void DownloadUpdateTask::executeTask()
+{
+ // GO!
+ // This will call the next step when it's done.
+ findCurrentVersionInfo();
+}
+
+void DownloadUpdateTask::processChannels()
+{
+ auto checker = MMC->updateChecker();
+
+ // Now, check the channel list again.
+ if (!checker->hasChannels())
+ {
+ // We still couldn't load the channel list. Give up. Call loadVersionInfo and return.
+ QLOG_INFO() << "Reloading the channel list didn't work. Giving up.";
+ loadVersionInfo();
+ return;
+ }
+
+ QList<UpdateChecker::ChannelListEntry> channels = checker->getChannelList();
+ QString channelId = MMC->version().channel;
+
+ m_cRepoUrl.clear();
+ // Search through the channel list for a channel with the correct ID.
+ for (auto channel : channels)
+ {
+ if (channel.id == channelId)
+ {
+ QLOG_INFO() << "Found matching channel.";
+ m_cRepoUrl = channel.url;
+ break;
+ }
+ }
+
+ // Now that we've done that, load version info.
+ loadVersionInfo();
+}
+
+void DownloadUpdateTask::findCurrentVersionInfo()
+{
+ setStatus(tr("Finding information about the current version..."));
+
+ auto checker = MMC->updateChecker();
+
+ if (!checker->hasChannels())
+ {
+ // Load the channel list and wait for it to finish loading.
+ QLOG_INFO() << "No channel list entries found. Will try reloading it.";
+
+ QObject::connect(checker.get(), &UpdateChecker::channelListLoaded, this,
+ &DownloadUpdateTask::processChannels);
+ checker->updateChanList();
+ }
+ else
+ {
+ processChannels();
+ }
+}
+
+void DownloadUpdateTask::loadVersionInfo()
+{
+ setStatus(tr("Loading version information..."));
+
+ // Create the net job for loading version info.
+ NetJob *netJob = new NetJob("Version Info");
+
+ // Find the index URL.
+ QUrl newIndexUrl = QUrl(m_nRepoUrl).resolved(QString::number(m_nVersionId) + ".json");
+ QLOG_DEBUG() << m_nRepoUrl << " turns into " << newIndexUrl;
+
+ // Add a net action to download the version info for the version we're updating to.
+ netJob->addNetAction(ByteArrayDownload::make(newIndexUrl));
+
+ // If we have a current version URL, get that one too.
+ if (!m_cRepoUrl.isEmpty())
+ {
+ QUrl cIndexUrl = QUrl(m_cRepoUrl).resolved(QString::number(m_cVersionId) + ".json");
+ netJob->addNetAction(ByteArrayDownload::make(cIndexUrl));
+ QLOG_DEBUG() << m_cRepoUrl << " turns into " << cIndexUrl;
+ }
+
+ // Connect slots so we know when it's done.
+ QObject::connect(netJob, &NetJob::succeeded, this,
+ &DownloadUpdateTask::vinfoDownloadFinished);
+ QObject::connect(netJob, &NetJob::failed, this, &DownloadUpdateTask::vinfoDownloadFailed);
+
+ // Store the NetJob in a class member. We don't want to lose it!
+ m_vinfoNetJob.reset(netJob);
+
+ // Finally, we start the network job and the thread's event loop to wait for it to finish.
+ netJob->start();
+}
+
+void DownloadUpdateTask::vinfoDownloadFinished()
+{
+ // Both downloads succeeded. OK. Parse stuff.
+ parseDownloadedVersionInfo();
+}
+
+void DownloadUpdateTask::vinfoDownloadFailed()
+{
+ // Something failed. We really need the second download (current version info), so parse
+ // downloads anyways as long as the first one succeeded.
+ if (m_vinfoNetJob->first()->m_status != Job_Failed)
+ {
+ parseDownloadedVersionInfo();
+ return;
+ }
+
+ // TODO: Give a more detailed error message.
+ QLOG_ERROR() << "Failed to download version info files.";
+ emitFailed(tr("Failed to download version info files."));
+}
+
+void DownloadUpdateTask::parseDownloadedVersionInfo()
+{
+ setStatus(tr("Reading file list for new version..."));
+ QLOG_DEBUG() << "Reading file list for new version...";
+ QString error;
+ if (!parseVersionInfo(
+ std::dynamic_pointer_cast<ByteArrayDownload>(m_vinfoNetJob->first())->m_data,
+ &m_nVersionFileList, &error))
+ {
+ emitFailed(error);
+ return;
+ }
+
+ // If there is a second entry in the network job's list, load it as the current version's
+ // info.
+ if (m_vinfoNetJob->size() >= 2 && m_vinfoNetJob->operator[](1)->m_status != Job_Failed)
+ {
+ setStatus(tr("Reading file list for current version..."));
+ QLOG_DEBUG() << "Reading file list for current version...";
+ QString error;
+ parseVersionInfo(
+ std::dynamic_pointer_cast<ByteArrayDownload>(m_vinfoNetJob->operator[](1))->m_data,
+ &m_cVersionFileList, &error);
+ }
+
+ // We don't need this any more.
+ m_vinfoNetJob.reset();
+
+ // Now that we're done loading version info, we can move on to the next step. Process file
+ // lists and download files.
+ processFileLists();
+}
+
+bool DownloadUpdateTask::parseVersionInfo(const QByteArray &data, VersionFileList *list,
+ QString *error)
+{
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ *error = QString("Failed to parse version info JSON: %1 at %2")
+ .arg(jsonError.errorString())
+ .arg(jsonError.offset);
+ QLOG_ERROR() << error;
+ return false;
+ }
+
+ QJsonObject json = jsonDoc.object();
+
+ QLOG_DEBUG() << data;
+ QLOG_DEBUG() << "Loading version info from JSON.";
+ QJsonArray filesArray = json.value("Files").toArray();
+ for (QJsonValue fileValue : filesArray)
+ {
+ QJsonObject fileObj = fileValue.toObject();
+
+ QString file_path = fileObj.value("Path").toString();
+#ifdef Q_OS_MAC
+ // On OSX, the paths for the updater need to be fixed.
+ // basically, anything that isn't in the .app folder is ignored.
+ // everything else is changed so the code that processes the files actually finds
+ // them and puts the replacements in the right spots.
+ if (!fixPathForOSX(file_path))
+ continue;
+#endif
+ VersionFileEntry file{file_path, fileObj.value("Perms").toVariant().toInt(),
+ FileSourceList(), fileObj.value("MD5").toString(), };
+ QLOG_DEBUG() << "File" << file.path << "with perms" << file.mode;
+
+ QJsonArray sourceArray = fileObj.value("Sources").toArray();
+ for (QJsonValue val : sourceArray)
+ {
+ QJsonObject sourceObj = val.toObject();
+
+ QString type = sourceObj.value("SourceType").toString();
+ if (type == "http")
+ {
+ file.sources.append(
+ FileSource("http", sourceObj.value("Url").toString()));
+ }
+ else if (type == "httpc")
+ {
+ file.sources.append(
+ FileSource("httpc", sourceObj.value("Url").toString(),
+ sourceObj.value("CompressionType").toString()));
+ }
+ else
+ {
+ QLOG_WARN() << "Unknown source type" << type << "ignored.";
+ }
+ }
+
+ QLOG_DEBUG() << "Loaded info for" << file.path;
+
+ list->append(file);
+ }
+
+ return true;
+}
+
+void DownloadUpdateTask::processFileLists()
+{
+ // Create a network job for downloading files.
+ NetJob *netJob = new NetJob("Update Files");
+
+ if (!processFileLists(netJob, m_cVersionFileList, m_nVersionFileList, m_operationList))
+ {
+ emitFailed(tr("Failed to process update lists..."));
+ return;
+ }
+
+ // Add listeners to wait for the downloads to finish.
+ QObject::connect(netJob, &NetJob::succeeded, this,
+ &DownloadUpdateTask::fileDownloadFinished);
+ QObject::connect(netJob, &NetJob::progress, this,
+ &DownloadUpdateTask::fileDownloadProgressChanged);
+ QObject::connect(netJob, &NetJob::failed, this, &DownloadUpdateTask::fileDownloadFailed);
+
+ // Now start the download.
+ setStatus(tr("Downloading %1 update files.").arg(QString::number(netJob->size())));
+ QLOG_DEBUG() << "Begin downloading update files to" << m_updateFilesDir.path();
+ m_filesNetJob.reset(netJob);
+ netJob->start();
+
+ writeInstallScript(m_operationList, PathCombine(m_updateFilesDir.path(), "file_list.xml"));
+}
+
+bool
+DownloadUpdateTask::processFileLists(NetJob *job,
+ const DownloadUpdateTask::VersionFileList &currentVersion,
+ const DownloadUpdateTask::VersionFileList &newVersion,
+ DownloadUpdateTask::UpdateOperationList &ops)
+{
+ setStatus(tr("Processing file lists - figuring out how to install the update..."));
+
+ // First, if we've loaded the current version's file list, we need to iterate through it and
+ // delete anything in the current one version's list that isn't in the new version's list.
+ for (VersionFileEntry entry : currentVersion)
+ {
+ QFileInfo toDelete(PathCombine(MMC->root(), entry.path));
+ if (!toDelete.exists())
+ {
+ QLOG_ERROR() << "Expected file " << toDelete.absoluteFilePath()
+ << " doesn't exist!";
+ }
+ bool keep = false;
+
+ //
+ for (VersionFileEntry newEntry : newVersion)
+ {
+ if (newEntry.path == entry.path)
+ {
+ QLOG_DEBUG() << "Not deleting" << entry.path
+ << "because it is still present in the new version.";
+ keep = true;
+ break;
+ }
+ }
+
+ // If the loop reaches the end and we didn't find a match, delete the file.
+ if (!keep)
+ {
+ if (toDelete.exists())
+ ops.append(UpdateOperation::DeleteOp(entry.path));
+ }
+ }
+
+ // Next, check each file in MultiMC's folder and see if we need to update them.
+ for (VersionFileEntry entry : newVersion)
+ {
+ // TODO: Let's not MD5sum a ton of files on the GUI thread. We should probably find a
+ // way to do this in the background.
+ QString fileMD5;
+ QString realEntryPath = PathCombine(MMC->root(), entry.path);
+ QFile entryFile(realEntryPath);
+ QFileInfo entryInfo(realEntryPath);
+
+ bool needs_upgrade = false;
+ if (!entryFile.exists())
+ {
+ needs_upgrade = true;
+ }
+ else
+ {
+ bool pass = true;
+ if (!entryInfo.isReadable())
+ {
+ QLOG_ERROR() << "File " << realEntryPath << " is not readable.";
+ pass = false;
+ }
+ if (!entryInfo.isWritable())
+ {
+ QLOG_ERROR() << "File " << realEntryPath << " is not writable.";
+ pass = false;
+ }
+ if (!entryFile.open(QFile::ReadOnly))
+ {
+ QLOG_ERROR() << "File " << realEntryPath << " cannot be opened for reading.";
+ pass = false;
+ }
+ if (!pass)
+ {
+ QLOG_ERROR() << "ROOT: " << MMC->root();
+ ops.clear();
+ return false;
+ }
+ }
+
+ if(!needs_upgrade)
+ {
+ QCryptographicHash hash(QCryptographicHash::Md5);
+ auto foo = entryFile.readAll();
+
+ hash.addData(foo);
+ fileMD5 = hash.result().toHex();
+ if ((fileMD5 != entry.md5))
+ {
+ QLOG_DEBUG() << "MD5Sum does not match!";
+ QLOG_DEBUG() << "Expected:'" << entry.md5 << "'";
+ QLOG_DEBUG() << "Got: '" << fileMD5 << "'";
+ needs_upgrade = true;
+ }
+ }
+
+ // skip file. it doesn't need an upgrade.
+ if (!needs_upgrade)
+ {
+ QLOG_DEBUG() << "File" << realEntryPath << " does not need updating.";
+ continue;
+ }
+
+ // yep. this file actually needs an upgrade. PROCEED.
+ QLOG_DEBUG() << "Found file" << realEntryPath << " that needs updating.";
+
+ // if it's the updater we want to treat it separately
+ bool isUpdater = entry.path.endsWith("updater") || entry.path.endsWith("updater.exe");
+
+ // Go through the sources list and find one to use.
+ // TODO: Make a NetAction that takes a source list and tries each of them until one
+ // works. For now, we'll just use the first http one.
+ for (FileSource source : entry.sources)
+ {
+ if (source.type == "http")
+ {
+ QLOG_DEBUG() << "Will download" << entry.path << "from" << source.url;
+
+ // Download it to updatedir/<filepath>-<md5> where filepath is the file's
+ // path with slashes replaced by underscores.
+ QString dlPath =
+ PathCombine(m_updateFilesDir.path(), QString(entry.path).replace("/", "_"));
+
+ if (isUpdater)
+ {
+#ifdef MultiMC_UPDATER_FORCE_LOCAL
+ QLOG_DEBUG() << "Skipping updater download and using local version.";
+#else
+ auto cache_entry = MMC->metacache()->resolveEntry("root", entry.path);
+ QLOG_DEBUG() << "Updater will be in " << cache_entry->getFullPath();
+ // force check.
+ cache_entry->stale = true;
+
+ auto download = CacheDownload::make(QUrl(source.url), cache_entry);
+ job->addNetAction(download);
+#endif
+ }
+ else
+ {
+ // We need to download the file to the updatefiles folder and add a task
+ // to copy it to its install path.
+ auto download = MD5EtagDownload::make(source.url, dlPath);
+ download->m_expected_md5 = entry.md5;
+ job->addNetAction(download);
+ ops.append(UpdateOperation::CopyOp(dlPath, entry.path, entry.mode));
+ }
+ }
+ }
+ }
+ return true;
+}
+
+bool DownloadUpdateTask::writeInstallScript(UpdateOperationList &opsList, QString scriptFile)
+{
+ // Build the base structure of the XML document.
+ QDomDocument doc;
+
+ QDomElement root = doc.createElement("update");
+ root.setAttribute("version", "3");
+ doc.appendChild(root);
+
+ QDomElement installFiles = doc.createElement("install");
+ root.appendChild(installFiles);
+
+ QDomElement removeFiles = doc.createElement("uninstall");
+ root.appendChild(removeFiles);
+
+ // Write the operation list to the XML document.
+ for (UpdateOperation op : opsList)
+ {
+ QDomElement file = doc.createElement("file");
+
+ switch (op.type)
+ {
+ case UpdateOperation::OP_COPY:
+ {
+ // Install the file.
+ QDomElement name = doc.createElement("source");
+ QDomElement path = doc.createElement("dest");
+ QDomElement mode = doc.createElement("mode");
+ name.appendChild(doc.createTextNode(op.file));
+ path.appendChild(doc.createTextNode(op.dest));
+ // We need to add a 0 at the beginning here, because Qt doesn't convert to octal
+ // correctly.
+ mode.appendChild(doc.createTextNode("0" + QString::number(op.mode, 8)));
+ file.appendChild(name);
+ file.appendChild(path);
+ file.appendChild(mode);
+ installFiles.appendChild(file);
+ QLOG_DEBUG() << "Will install file " << op.file << " to " << op.dest;
+ }
+ break;
+
+ case UpdateOperation::OP_DELETE:
+ {
+ // Delete the file.
+ file.appendChild(doc.createTextNode(op.file));
+ removeFiles.appendChild(file);
+ QLOG_DEBUG() << "Will remove file" << op.file;
+ }
+ break;
+
+ default:
+ QLOG_WARN() << "Can't write update operation of type" << op.type
+ << "to file. Not implemented.";
+ continue;
+ }
+ }
+
+ // Write the XML document to the file.
+ QFile outFile(scriptFile);
+
+ if (outFile.open(QIODevice::WriteOnly))
+ {
+ outFile.write(doc.toByteArray());
+ }
+ else
+ {
+ emitFailed(tr("Failed to write update script file."));
+ return false;
+ }
+
+ return true;
+}
+
+bool DownloadUpdateTask::fixPathForOSX(QString &path)
+{
+ if (path.startsWith("MultiMC.app/"))
+ {
+ // remove the prefix and add a new, more appropriate one.
+ path.remove(0, 12);
+ return true;
+ }
+ else
+ {
+ QLOG_ERROR() << "Update path not within .app: " << path;
+ return false;
+ }
+}
+
+void DownloadUpdateTask::fileDownloadFinished()
+{
+ emitSucceeded();
+}
+
+void DownloadUpdateTask::fileDownloadFailed()
+{
+ // TODO: Give more info about the failure.
+ QLOG_ERROR() << "Failed to download update files.";
+ emitFailed(tr("Failed to download update files."));
+}
+
+void DownloadUpdateTask::fileDownloadProgressChanged(qint64 current, qint64 total)
+{
+ setProgress((int)(((float)current / (float)total) * 100));
+}
+
+QString DownloadUpdateTask::updateFilesDir()
+{
+ return m_updateFilesDir.path();
+}
diff --git a/logic/updater/DownloadUpdateTask.h b/logic/updater/DownloadUpdateTask.h
new file mode 100644
index 00000000..518bc235
--- /dev/null
+++ b/logic/updater/DownloadUpdateTask.h
@@ -0,0 +1,217 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "logic/tasks/Task.h"
+#include "logic/net/NetJob.h"
+
+/*!
+ * The DownloadUpdateTask is a task that takes a given version ID and repository URL,
+ * downloads that version's files from the repository, and prepares to install them.
+ */
+class DownloadUpdateTask : public Task
+{
+ Q_OBJECT
+
+public:
+ explicit DownloadUpdateTask(QString repoUrl, int versionId, QObject* parent=0);
+
+ /*!
+ * Gets the directory that contains the update files.
+ */
+ QString updateFilesDir();
+
+public:
+
+ // TODO: We should probably put these data structures into a separate header...
+
+ /*!
+ * Struct that describes an entry in a VersionFileEntry's `Sources` list.
+ */
+ struct FileSource
+ {
+ FileSource(QString type, QString url, QString compression="")
+ {
+ this->type = type;
+ this->url = url;
+ this->compressionType = compression;
+ }
+
+ QString type;
+ QString url;
+ QString compressionType;
+ };
+ typedef QList<FileSource> FileSourceList;
+
+ /*!
+ * Structure that describes an entry in a GoUpdate version's `Files` list.
+ */
+ struct VersionFileEntry
+ {
+ QString path;
+ int mode;
+ FileSourceList sources;
+ QString md5;
+ };
+ typedef QList<VersionFileEntry> VersionFileList;
+
+ /*!
+ * Structure that describes an operation to perform when installing updates.
+ */
+ struct UpdateOperation
+ {
+ static UpdateOperation CopyOp(QString fsource, QString fdest, int fmode=0644) { return UpdateOperation{OP_COPY, fsource, fdest, fmode}; }
+ static UpdateOperation MoveOp(QString fsource, QString fdest, int fmode=0644) { return UpdateOperation{OP_MOVE, fsource, fdest, fmode}; }
+ static UpdateOperation DeleteOp(QString file) { return UpdateOperation{OP_DELETE, file, "", 0644}; }
+ static UpdateOperation ChmodOp(QString file, int fmode) { return UpdateOperation{OP_CHMOD, file, "", fmode}; }
+
+ //! Specifies the type of operation that this is.
+ enum Type
+ {
+ OP_COPY,
+ OP_DELETE,
+ OP_MOVE,
+ OP_CHMOD,
+ } type;
+
+ //! The file to operate on. If this is a DELETE or CHMOD operation, this is the file that will be modified.
+ QString file;
+
+ //! The destination file. If this is a DELETE or CHMOD operation, this field will be ignored.
+ QString dest;
+
+ //! The mode to change the source file to. Ignored if this isn't a CHMOD operation.
+ int mode;
+
+ // Yeah yeah, polymorphism blah blah inheritance, blah blah object oriented. I'm lazy, OK?
+ };
+ typedef QList<UpdateOperation> UpdateOperationList;
+
+protected:
+ friend class DownloadUpdateTaskTest;
+
+
+ /*!
+ * Used for arguments to parseVersionInfo and friends to specify which version info file to parse.
+ */
+ enum VersionInfoFileEnum { NEW_VERSION, CURRENT_VERSION };
+
+
+ //! Entry point for tasks.
+ virtual void executeTask();
+
+ /*!
+ * Attempts to find the version ID and repository URL for the current version.
+ * The function will look up the repository URL in the UpdateChecker's channel list.
+ * If the repository URL can't be found, this function will return false.
+ */
+ virtual void findCurrentVersionInfo();
+
+ /*!
+ * This runs after we've tried loading the channel list.
+ * If the channel list doesn't need to be loaded, this will be called immediately.
+ * If the channel list does need to be loaded, this will be called when it's done.
+ */
+ void processChannels();
+
+ /*!
+ * Downloads the version info files from the repository.
+ * The files for both the current build, and the build that we're updating to need to be downloaded.
+ * If the current version's info file can't be found, MultiMC will not delete files that
+ * were removed between versions. It will still replace files that have changed, however.
+ * Note that although the repository URL for the current version is not given to the update task,
+ * the task will attempt to look it up in the UpdateChecker's channel list.
+ * If an error occurs here, the function will call emitFailed and return false.
+ */
+ virtual void loadVersionInfo();
+
+ /*!
+ * This function is called when version information is finished downloading.
+ * This handles parsing the JSON downloaded by the version info network job and then calls processFileLists.
+ * Note that this function will sometimes be called even if the version info download emits failed. If
+ * we couldn't download the current version's info file, we can still update. This will be called even if the
+ * current version's info file fails to download, as long as the new version's info file succeeded.
+ */
+ virtual void parseDownloadedVersionInfo();
+
+ /*!
+ * Loads the file list from the given version info JSON object into the given list.
+ */
+ virtual bool parseVersionInfo(const QByteArray &data, VersionFileList* list, QString *error);
+
+ /*!
+ * Takes a list of file entries for the current version's files and the new version's files
+ * and populates the downloadList and operationList with information about how to download and install the update.
+ */
+ virtual bool processFileLists(NetJob *job, const VersionFileList &currentVersion, const VersionFileList &newVersion, UpdateOperationList &ops);
+
+ /*!
+ * Calls \see processFileLists to populate the \see m_operationList and a NetJob, and then executes
+ * the NetJob to fetch all needed files
+ */
+ virtual void processFileLists();
+
+ /*!
+ * Takes the operations list and writes an install script for the updater to the update files directory.
+ */
+ virtual bool writeInstallScript(UpdateOperationList& opsList, QString scriptFile);
+
+ UpdateOperationList m_operationList;
+
+ VersionFileList m_nVersionFileList;
+ VersionFileList m_cVersionFileList;
+
+ //! Network job for downloading version info files.
+ NetJobPtr m_vinfoNetJob;
+
+ //! Network job for downloading update files.
+ NetJobPtr m_filesNetJob;
+
+ // Version ID and repo URL for the new version.
+ int m_nVersionId;
+ QString m_nRepoUrl;
+
+ // Version ID and repo URL for the currently installed version.
+ int m_cVersionId;
+ QString m_cRepoUrl;
+
+ /*!
+ * Temporary directory to store update files in.
+ * This will be set to not auto delete. Task will fail if this fails to be created.
+ */
+ QTemporaryDir m_updateFilesDir;
+
+ /*!
+ * Filters paths
+ * This fixes destination paths for OSX.
+ * The updater runs in MultiMC.app/Contents/MacOs by default
+ * The destination paths are such as this: MultiMC.app/blah/blah
+ *
+ * Therefore we chop off the 'MultiMC.app' prefix
+ *
+ * Returns false if the path couldn't be fixed (is invalid)
+ */
+ static bool fixPathForOSX(QString &path);
+
+protected slots:
+ void vinfoDownloadFinished();
+ void vinfoDownloadFailed();
+
+ void fileDownloadFinished();
+ void fileDownloadFailed();
+ void fileDownloadProgressChanged(qint64 current, qint64 total);
+};
+
diff --git a/logic/updater/NotificationChecker.cpp b/logic/updater/NotificationChecker.cpp
new file mode 100644
index 00000000..191e90a3
--- /dev/null
+++ b/logic/updater/NotificationChecker.cpp
@@ -0,0 +1,121 @@
+#include "NotificationChecker.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#include "MultiMC.h"
+#include "MultiMCVersion.h"
+#include "logic/net/CacheDownload.h"
+
+NotificationChecker::NotificationChecker(QObject *parent)
+ : QObject(parent), m_notificationsUrl(QUrl(NOTIFICATION_URL))
+{
+ // this will call checkForNotifications once the event loop is running
+ QMetaObject::invokeMethod(this, "checkForNotifications", Qt::QueuedConnection);
+}
+
+QUrl NotificationChecker::notificationsUrl() const
+{
+ return m_notificationsUrl;
+}
+void NotificationChecker::setNotificationsUrl(const QUrl &notificationsUrl)
+{
+ m_notificationsUrl = notificationsUrl;
+}
+
+QList<NotificationChecker::NotificationEntry> NotificationChecker::notificationEntries() const
+{
+ return m_entries;
+}
+
+void NotificationChecker::checkForNotifications()
+{
+ if (!m_notificationsUrl.isValid())
+ {
+ QLOG_ERROR() << "Failed to check for notifications. No notifications URL set."
+ << "If you'd like to use MultiMC's notification system, please pass the "
+ "URL to CMake at compile time.";
+ return;
+ }
+ if (m_checkJob)
+ {
+ return;
+ }
+ m_checkJob.reset(new NetJob("Checking for notifications"));
+ auto entry = MMC->metacache()->resolveEntry("root", "notifications.json");
+ entry->stale = true;
+ m_checkJob->addNetAction(m_download = CacheDownload::make(m_notificationsUrl, entry));
+ connect(m_download.get(), &CacheDownload::succeeded, this,
+ &NotificationChecker::downloadSucceeded);
+ m_checkJob->start();
+}
+
+void NotificationChecker::downloadSucceeded(int)
+{
+ m_entries.clear();
+
+ QFile file(m_download->getTargetFilepath());
+ if (file.open(QFile::ReadOnly))
+ {
+ QJsonArray root = QJsonDocument::fromJson(file.readAll()).array();
+ for (auto it = root.begin(); it != root.end(); ++it)
+ {
+ QJsonObject obj = (*it).toObject();
+ NotificationEntry entry;
+ entry.id = obj.value("id").toDouble();
+ entry.message = obj.value("message").toString();
+ entry.channel = obj.value("channel").toString();
+ entry.platform = obj.value("platform").toString();
+ entry.from = obj.value("from").toString();
+ entry.to = obj.value("to").toString();
+ const QString type = obj.value("type").toString("critical");
+ if (type == "critical")
+ {
+ entry.type = NotificationEntry::Critical;
+ }
+ else if (type == "warning")
+ {
+ entry.type = NotificationEntry::Warning;
+ }
+ else if (type == "information")
+ {
+ entry.type = NotificationEntry::Information;
+ }
+ m_entries.append(entry);
+ }
+ }
+
+ m_checkJob.reset();
+
+ emit notificationCheckFinished();
+}
+
+bool NotificationChecker::NotificationEntry::applies() const
+{
+ MultiMCVersion version = MMC->version();
+ bool channelApplies = channel.isEmpty() || channel == version.channel;
+ bool platformApplies = platform.isEmpty() || platform == version.platform;
+ bool fromApplies =
+ from.isEmpty() || from == FULL_VERSION_STR || !versionLessThan(FULL_VERSION_STR, from);
+ bool toApplies =
+ to.isEmpty() || to == FULL_VERSION_STR || !versionLessThan(to, FULL_VERSION_STR);
+ return channelApplies && platformApplies && fromApplies && toApplies;
+}
+
+bool NotificationChecker::NotificationEntry::versionLessThan(const QString &v1,
+ const QString &v2)
+{
+ QStringList l1 = v1.split('.');
+ QStringList l2 = v2.split('.');
+ while (!l1.isEmpty() && !l2.isEmpty())
+ {
+ int one = l1.isEmpty() ? 0 : l1.takeFirst().toInt();
+ int two = l2.isEmpty() ? 0 : l2.takeFirst().toInt();
+ if (one != two)
+ {
+ return one < two;
+ }
+ }
+ return false;
+}
diff --git a/logic/updater/NotificationChecker.h b/logic/updater/NotificationChecker.h
new file mode 100644
index 00000000..915ee54d
--- /dev/null
+++ b/logic/updater/NotificationChecker.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include <QObject>
+
+#include "logic/net/NetJob.h"
+#include "logic/net/CacheDownload.h"
+
+class NotificationChecker : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit NotificationChecker(QObject *parent = 0);
+
+ QUrl notificationsUrl() const;
+ void setNotificationsUrl(const QUrl &notificationsUrl);
+
+ struct NotificationEntry
+ {
+ int id;
+ QString message;
+ enum
+ {
+ Critical,
+ Warning,
+ Information
+ } type;
+ QString channel;
+ QString platform;
+ QString from;
+ QString to;
+ bool applies() const;
+ static bool versionLessThan(const QString &v1, const QString &v2);
+ };
+
+ QList<NotificationEntry> notificationEntries() const;
+
+public
+slots:
+ void checkForNotifications();
+
+private
+slots:
+ void downloadSucceeded(int);
+
+signals:
+ void notificationCheckFinished();
+
+private:
+ QList<NotificationEntry> m_entries;
+ QUrl m_notificationsUrl;
+ NetJobPtr m_checkJob;
+ CacheDownloadPtr m_download;
+};
diff --git a/logic/updater/UpdateChecker.cpp b/logic/updater/UpdateChecker.cpp
new file mode 100644
index 00000000..8e2aa8b3
--- /dev/null
+++ b/logic/updater/UpdateChecker.cpp
@@ -0,0 +1,263 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "UpdateChecker.h"
+
+#include "MultiMC.h"
+
+#include "logger/QsLog.h"
+
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+
+#include <settingsobject.h>
+
+#define API_VERSION 0
+#define CHANLIST_FORMAT 0
+
+UpdateChecker::UpdateChecker()
+{
+ m_channelListUrl = CHANLIST_URL;
+ m_updateChecking = false;
+ m_chanListLoading = false;
+ m_checkUpdateWaiting = false;
+ m_chanListLoaded = false;
+}
+
+QList<UpdateChecker::ChannelListEntry> UpdateChecker::getChannelList() const
+{
+ return m_channels;
+}
+
+bool UpdateChecker::hasChannels() const
+{
+ return !m_channels.isEmpty();
+}
+
+void UpdateChecker::checkForUpdate(bool notifyNoUpdate)
+{
+ QLOG_DEBUG() << "Checking for updates.";
+
+ // If the channel list hasn't loaded yet, load it and defer checking for updates until
+ // later.
+ if (!m_chanListLoaded)
+ {
+ QLOG_DEBUG() << "Channel list isn't loaded yet. Loading channel list and deferring "
+ "update check.";
+ m_checkUpdateWaiting = true;
+ updateChanList();
+ return;
+ }
+
+ if (m_updateChecking)
+ {
+ QLOG_DEBUG() << "Ignoring update check request. Already checking for updates.";
+ return;
+ }
+
+ m_updateChecking = true;
+
+ // Get the channel we're checking.
+ QString updateChannel = MMC->settings()->get("UpdateChannel").toString();
+
+ // Find the desired channel within the channel list and get its repo URL. If if cannot be
+ // found, error.
+ m_repoUrl = "";
+ for (ChannelListEntry entry : m_channels)
+ {
+ if (entry.id == updateChannel)
+ m_repoUrl = entry.url;
+ }
+
+ // If we didn't find our channel, error.
+ if (m_repoUrl.isEmpty())
+ {
+ emit updateCheckFailed();
+ return;
+ }
+
+ QUrl indexUrl = QUrl(m_repoUrl).resolved(QUrl("index.json"));
+
+ auto job = new NetJob("GoUpdate Repository Index");
+ job->addNetAction(ByteArrayDownload::make(indexUrl));
+ connect(job, &NetJob::succeeded, [this, notifyNoUpdate]()
+ { updateCheckFinished(notifyNoUpdate); });
+ connect(job, SIGNAL(failed()), SLOT(updateCheckFailed()));
+ indexJob.reset(job);
+ job->start();
+}
+
+void UpdateChecker::updateCheckFinished(bool notifyNoUpdate)
+{
+ QLOG_DEBUG() << "Finished downloading repo index. Checking for new versions.";
+
+ QJsonParseError jsonError;
+ QByteArray data;
+ {
+ ByteArrayDownloadPtr dl =
+ std::dynamic_pointer_cast<ByteArrayDownload>(indexJob->first());
+ data = dl->m_data;
+ indexJob.reset();
+ }
+
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError || !jsonDoc.isObject())
+ {
+ QLOG_ERROR() << "Failed to parse GoUpdate repository index. JSON error"
+ << jsonError.errorString() << "at offset" << jsonError.offset;
+ return;
+ }
+
+ QJsonObject object = jsonDoc.object();
+
+ bool success = false;
+ int apiVersion = object.value("ApiVersion").toVariant().toInt(&success);
+ if (apiVersion != API_VERSION || !success)
+ {
+ QLOG_ERROR() << "Failed to check for updates. API version mismatch. We're using"
+ << API_VERSION << "server has" << apiVersion;
+ return;
+ }
+
+ QLOG_DEBUG() << "Processing repository version list.";
+ QJsonObject newestVersion;
+ QJsonArray versions = object.value("Versions").toArray();
+ for (QJsonValue versionVal : versions)
+ {
+ QJsonObject version = versionVal.toObject();
+ if (newestVersion.value("Id").toVariant().toInt() <
+ version.value("Id").toVariant().toInt())
+ {
+ newestVersion = version;
+ }
+ }
+
+ // We've got the version with the greatest ID number. Now compare it to our current build
+ // number and update if they're different.
+ int newBuildNumber = newestVersion.value("Id").toVariant().toInt();
+ if (newBuildNumber != MMC->version().build)
+ {
+ QLOG_DEBUG() << "Found newer version with ID" << newBuildNumber;
+ // Update!
+ emit updateAvailable(m_repoUrl, newestVersion.value("Name").toVariant().toString(),
+ newBuildNumber);
+ }
+ else if (notifyNoUpdate)
+ {
+ emit noUpdateFound();
+ }
+
+ m_updateChecking = false;
+}
+
+void UpdateChecker::updateCheckFailed()
+{
+ // TODO: log errors better
+ QLOG_ERROR() << "Update check failed for reasons unknown.";
+}
+
+void UpdateChecker::updateChanList()
+{
+ QLOG_DEBUG() << "Loading the channel list.";
+
+ if (m_channelListUrl.isEmpty())
+ {
+ QLOG_ERROR() << "Failed to update channel list. No channel list URL set."
+ << "If you'd like to use MultiMC's update system, please pass the channel "
+ "list URL to CMake at compile time.";
+ return;
+ }
+
+ m_chanListLoading = true;
+ NetJob *job = new NetJob("Update System Channel List");
+ job->addNetAction(ByteArrayDownload::make(QUrl(m_channelListUrl)));
+ QObject::connect(job, &NetJob::succeeded, this, &UpdateChecker::chanListDownloadFinished);
+ QObject::connect(job, &NetJob::failed, this, &UpdateChecker::chanListDownloadFailed);
+ chanListJob.reset(job);
+ job->start();
+}
+
+void UpdateChecker::chanListDownloadFinished()
+{
+ QByteArray data;
+ {
+ ByteArrayDownloadPtr dl =
+ std::dynamic_pointer_cast<ByteArrayDownload>(chanListJob->first());
+ data = dl->m_data;
+ chanListJob.reset();
+ }
+
+ QJsonParseError jsonError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError)
+ {
+ // TODO: Report errors to the user.
+ QLOG_ERROR() << "Failed to parse channel list JSON:" << jsonError.errorString() << "at"
+ << jsonError.offset;
+ return;
+ }
+
+ QJsonObject object = jsonDoc.object();
+
+ bool success = false;
+ int formatVersion = object.value("format_version").toVariant().toInt(&success);
+ if (formatVersion != CHANLIST_FORMAT || !success)
+ {
+ QLOG_ERROR()
+ << "Failed to check for updates. Channel list format version mismatch. We're using"
+ << CHANLIST_FORMAT << "server has" << formatVersion;
+ return;
+ }
+
+ // Load channels into a temporary array.
+ QList<ChannelListEntry> loadedChannels;
+ QJsonArray channelArray = object.value("channels").toArray();
+ for (QJsonValue chanVal : channelArray)
+ {
+ QJsonObject channelObj = chanVal.toObject();
+ ChannelListEntry entry{channelObj.value("id").toVariant().toString(),
+ channelObj.value("name").toVariant().toString(),
+ channelObj.value("description").toVariant().toString(),
+ channelObj.value("url").toVariant().toString()};
+ if (entry.id.isEmpty() || entry.name.isEmpty() || entry.url.isEmpty())
+ {
+ QLOG_ERROR() << "Channel list entry with empty ID, name, or URL. Skipping.";
+ continue;
+ }
+ loadedChannels.append(entry);
+ }
+
+ // Swap the channel list we just loaded into the object's channel list.
+ m_channels.swap(loadedChannels);
+
+ m_chanListLoading = false;
+ m_chanListLoaded = true;
+ QLOG_INFO() << "Successfully loaded UpdateChecker channel list.";
+
+ // If we're waiting to check for updates, do that now.
+ if (m_checkUpdateWaiting)
+ checkForUpdate(false);
+
+ emit channelListLoaded();
+}
+
+void UpdateChecker::chanListDownloadFailed()
+{
+ m_chanListLoading = false;
+ QLOG_ERROR() << "Failed to download channel list.";
+ emit channelListLoaded();
+}
+
diff --git a/logic/updater/UpdateChecker.h b/logic/updater/UpdateChecker.h
new file mode 100644
index 00000000..3b0ee28d
--- /dev/null
+++ b/logic/updater/UpdateChecker.h
@@ -0,0 +1,111 @@
+/* Copyright 2013 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "logic/net/NetJob.h"
+
+#include <QUrl>
+
+class UpdateChecker : public QObject
+{
+ Q_OBJECT
+
+public:
+ UpdateChecker();
+ void checkForUpdate(bool notifyNoUpdate);
+
+ void setChannelListUrl(const QString &url) { m_channelListUrl = url; }
+
+ /*!
+ * Causes the update checker to download the channel list from the URL specified in config.h (generated by CMake).
+ * If this isn't called before checkForUpdate(), it will automatically be called.
+ */
+ void updateChanList();
+
+ /*!
+ * An entry in the channel list.
+ */
+ struct ChannelListEntry
+ {
+ QString id;
+ QString name;
+ QString description;
+ QString url;
+ };
+
+ /*!
+ * Returns a the current channel list.
+ * If the channel list hasn't been loaded, this list will be empty.
+ */
+ QList<ChannelListEntry> getChannelList() const;
+
+ /*!
+ * Returns false if the channel list is empty.
+ */
+ bool hasChannels() const;
+
+signals:
+ //! Signal emitted when an update is available. Passes the URL for the repo and the ID and name for the version.
+ void updateAvailable(QString repoUrl, QString versionName, int versionId);
+
+ //! Signal emitted when the channel list finishes loading or fails to load.
+ void channelListLoaded();
+
+ void noUpdateFound();
+
+private slots:
+ void updateCheckFinished(bool notifyNoUpdate);
+ void updateCheckFailed();
+
+ void chanListDownloadFinished();
+ void chanListDownloadFailed();
+
+private:
+ friend class UpdateCheckerTest;
+
+ NetJobPtr indexJob;
+ NetJobPtr chanListJob;
+
+ QString m_repoUrl;
+
+ QString m_channelListUrl;
+
+ QList<ChannelListEntry> m_channels;
+
+ /*!
+ * True while the system is checking for updates.
+ * If checkForUpdate is called while this is true, it will be ignored.
+ */
+ bool m_updateChecking;
+
+ /*!
+ * True if the channel list has loaded.
+ * If this is false, trying to check for updates will call updateChanList first.
+ */
+ bool m_chanListLoaded;
+
+ /*!
+ * Set to true while the channel list is currently loading.
+ */
+ bool m_chanListLoading;
+
+ /*!
+ * Set to true when checkForUpdate is called while the channel list isn't loaded.
+ * When the channel list finishes loading, if this is true, the update checker will check for updates.
+ */
+ bool m_checkUpdateWaiting;
+};
+