/* Copyright 2013-2017 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 #include #include #include #define API_VERSION 0 #define CHANLIST_FORMAT 0 UpdateChecker::UpdateChecker(QString channelListUrl, QString currentChannel, int currentBuild) { m_channelListUrl = channelListUrl; m_currentChannel = currentChannel; m_currentBuild = currentBuild; } QList UpdateChecker::getChannelList() const { return m_channels; } bool UpdateChecker::hasChannels() const { return !m_channels.isEmpty(); } void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate) { 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 (m_updateChecking) { qDebug() << "Ignoring update check request. Already checking for updates."; return; } m_updateChecking = true; // Find the desired channel within the channel list and get its repo URL. If if cannot be // found, error. m_newRepoUrl = ""; for (ChannelListEntry entry : m_channels) { if (entry.id == updateChannel) m_newRepoUrl = entry.url; if (entry.id == m_currentChannel) m_currentRepoUrl = entry.url; } qDebug() << "m_repoUrl = " << m_newRepoUrl; // If we didn't find our channel, error. if (m_newRepoUrl.isEmpty()) { qCritical() << "m_repoUrl is empty!"; emit updateCheckFailed(); return; } QUrl indexUrl = QUrl(m_newRepoUrl).resolved(QUrl("index.json")); auto job = new NetJob("GoUpdate Repository Index"); job->addNetAction(Net::Download::makeByteArray(indexUrl, &indexData)); connect(job, &NetJob::succeeded, [this, notifyNoUpdate](){ updateCheckFinished(notifyNoUpdate); }); connect(job, &NetJob::failed, this, &UpdateChecker::updateCheckFailed); indexJob.reset(job); job->start(); } void UpdateChecker::updateCheckFinished(bool notifyNoUpdate) { qDebug() << "Finished downloading repo index. Checking for new versions."; QJsonParseError jsonError; indexJob.reset(); QJsonDocument jsonDoc = QJsonDocument::fromJson(indexData, &jsonError); indexData.clear(); if (jsonError.error != QJsonParseError::NoError || !jsonDoc.isObject()) { qCritical() << "Failed to parse GoUpdate repository index. JSON error" << jsonError.errorString() << "at offset" << jsonError.offset; m_updateChecking = false; return; } QJsonObject object = jsonDoc.object(); bool success = false; int apiVersion = object.value("ApiVersion").toVariant().toInt(&success); if (apiVersion != API_VERSION || !success) { qCritical() << "Failed to check for updates. API version mismatch. We're using" << API_VERSION << "server has" << apiVersion; m_updateChecking = false; return; } qDebug() << "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 != m_currentBuild) { qDebug() << "Found newer version with ID" << newBuildNumber; // Update! GoUpdate::Status updateStatus; updateStatus.updateAvailable = true; updateStatus.currentVersionId = m_currentBuild; updateStatus.currentRepoUrl = m_currentRepoUrl; updateStatus.newVersionId = newBuildNumber; updateStatus.newRepoUrl = m_newRepoUrl; emit updateAvailable(updateStatus); } else if (notifyNoUpdate) { emit noUpdateFound(); } m_updateChecking = false; } void UpdateChecker::updateCheckFailed() { qCritical() << "Update check failed for reasons unknown."; } void UpdateChecker::updateChanList(bool notifyNoUpdate) { qDebug() << "Loading the channel list."; if (m_chanListLoading) { qDebug() << "Ignoring channel list update request. Already grabbing channel list."; return; } if (m_channelListUrl.isEmpty()) { qCritical() << "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(Net::Download::makeByteArray(QUrl(m_channelListUrl), &chanlistData)); connect(job, &NetJob::succeeded, [this, notifyNoUpdate]() { chanListDownloadFinished(notifyNoUpdate); }); QObject::connect(job, &NetJob::failed, this, &UpdateChecker::chanListDownloadFailed); chanListJob.reset(job); job->start(); } void UpdateChecker::chanListDownloadFinished(bool notifyNoUpdate) { chanListJob.reset(); QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(chanlistData, &jsonError); chanlistData.clear(); if (jsonError.error != QJsonParseError::NoError) { // TODO: Report errors to the user. qCritical() << "Failed to parse channel list JSON:" << jsonError.errorString() << "at" << jsonError.offset; m_chanListLoading = false; return; } QJsonObject object = jsonDoc.object(); bool success = false; int formatVersion = object.value("format_version").toVariant().toInt(&success); if (formatVersion != CHANLIST_FORMAT || !success) { qCritical() << "Failed to check for updates. Channel list format version mismatch. We're using" << CHANLIST_FORMAT << "server has" << formatVersion; m_chanListLoading = false; return; } // Load channels into a temporary array. QList 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()) { qCritical() << "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; qDebug() << "Successfully loaded UpdateChecker channel list."; // If we're waiting to check for updates, do that now. if (m_checkUpdateWaiting) checkForUpdate(m_deferredUpdateChannel, notifyNoUpdate); emit channelListLoaded(); } void UpdateChecker::chanListDownloadFailed(QString reason) { m_chanListLoading = false; qCritical() << QString("Failed to download channel list: %1").arg(reason); emit channelListLoaded(); }