/* Copyright 2013-2021 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 #include "BuildConfig.h" #include "sys.h" UpdateChecker::UpdateChecker(shared_qobject_ptr nam, QString channelUrl, QString currentChannel, int currentBuild) { m_network = nam; m_channelUrl = channelUrl; m_currentChannel = currentChannel; m_currentBuild = currentBuild; } QList UpdateChecker::getChannelList() const { return m_channels; } bool UpdateChecker::hasChannels() const { return !m_channels.isEmpty(); } #ifdef Q_OS_MAC SparkleUpdater* UpdateChecker::getSparkleUpdater() { return m_sparkleUpdater; } #endif void UpdateChecker::checkForUpdate(QString updateChannel, bool notifyNoUpdate) { #ifdef Q_OS_MAC m_sparkleUpdater->setAllowedChannel(updateChannel); if (notifyNoUpdate) { qDebug() << "Checking for updates."; m_sparkleUpdater->checkForUpdates(); } else { // Sparkle already handles automatic update checks. return; } #else 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; } // 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; 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; } m_updateChecking = true; 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(); #endif } 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; } m_chanListLoading = true; chanListJob = new NetJob("Update System Channel List", m_network); chanListJob->addNetAction(Net::Download::makeByteArray(QUrl(m_channelUrl), &chanlistData)); connect(chanListJob.get(), &NetJob::succeeded, [this, notifyNoUpdate]() { chanListDownloadFinished(notifyNoUpdate); }); connect(chanListJob.get(), &NetJob::failed, this, &UpdateChecker::chanListDownloadFailed); chanListJob->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(); }