271 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			271 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /* 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 <QJsonObject>
 | |
| #include <QJsonArray>
 | |
| #include <QJsonValue>
 | |
| #include <QDebug>
 | |
| 
 | |
| #define API_VERSION 0
 | |
| #define CHANLIST_FORMAT 0
 | |
| 
 | |
| #include "BuildConfig.h"
 | |
| #include "sys.h"
 | |
| 
 | |
| UpdateChecker::UpdateChecker(shared_qobject_ptr<QNetworkAccessManager> nam, QString channelUrl, QString currentChannel, int currentBuild)
 | |
| {
 | |
|     m_network = nam;
 | |
|     m_channelUrl = channelUrl;
 | |
|     m_currentChannel = currentChannel;
 | |
|     m_currentBuild = currentBuild;
 | |
| }
 | |
| 
 | |
| QList<UpdateChecker::ChannelListEntry> 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;
 | |
|     }
 | |
| 
 | |
|     // 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();
 | |
| }
 | |
| 
 | |
| 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<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())
 | |
|         {
 | |
|             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();
 | |
| }
 | |
| 
 |