diff options
author | Forkk <forkk@forkk.net> | 2014-01-02 13:38:20 -0600 |
---|---|---|
committer | Forkk <forkk@forkk.net> | 2014-01-02 13:38:20 -0600 |
commit | 17f1864a71b69b9df14d8e06ed48a65e678d09c9 (patch) | |
tree | 4d98a2b3493a26017150d6ba8c5ae0419de3de7d /logic/updater | |
parent | 4495e20cd7f7f2ab062f3b60f19ac4b79f32c350 (diff) | |
parent | aa5f2c8120cc23de0d57c9f0280512adb9a531b3 (diff) | |
download | MultiMC-17f1864a71b69b9df14d8e06ed48a65e678d09c9.tar MultiMC-17f1864a71b69b9df14d8e06ed48a65e678d09c9.tar.gz MultiMC-17f1864a71b69b9df14d8e06ed48a65e678d09c9.tar.lz MultiMC-17f1864a71b69b9df14d8e06ed48a65e678d09c9.tar.xz MultiMC-17f1864a71b69b9df14d8e06ed48a65e678d09c9.zip |
Merge branch 'develop' of github.com:MultiMC/MultiMC5 into feature_news
Conflicts:
CMakeLists.txt
gui/MainWindow.h
Diffstat (limited to 'logic/updater')
-rw-r--r-- | logic/updater/DownloadUpdateTask.cpp | 436 | ||||
-rw-r--r-- | logic/updater/DownloadUpdateTask.h | 49 | ||||
-rw-r--r-- | logic/updater/UpdateChecker.cpp | 73 | ||||
-rw-r--r-- | logic/updater/UpdateChecker.h | 11 |
4 files changed, 385 insertions, 184 deletions
diff --git a/logic/updater/DownloadUpdateTask.cpp b/logic/updater/DownloadUpdateTask.cpp index d9aab826..6e0a92f0 100644 --- a/logic/updater/DownloadUpdateTask.cpp +++ b/logic/updater/DownloadUpdateTask.cpp @@ -26,9 +26,8 @@ #include <QDomDocument> - -DownloadUpdateTask::DownloadUpdateTask(QString repoUrl, int versionId, QObject* parent) : - Task(parent) +DownloadUpdateTask::DownloadUpdateTask(QString repoUrl, int versionId, QObject *parent) + : Task(parent) { m_cVersionId = MMC->version().build; @@ -45,68 +44,69 @@ void DownloadUpdateTask::executeTask() findCurrentVersionInfo(); } -void DownloadUpdateTask::findCurrentVersionInfo() +void DownloadUpdateTask::processChannels() { - setStatus(tr("Finding information about the current version.")); - auto checker = MMC->updateChecker(); - // 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. - auto processFunc = [this, &checker] () -> void + // Now, check the channel list again. + if (!checker->hasChannels()) { - // 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; - } + // 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; + QList<UpdateChecker::ChannelListEntry> channels = checker->getChannelList(); + QString channelId = MMC->version().channel; - // Search through the channel list for a channel with the correct ID. - for (auto channel : channels) + // Search through the channel list for a channel with the correct ID. + for (auto channel : channels) + { + if (channel.id == channelId) { - if (channel.id == channelId) - { - QLOG_INFO() << "Found matching channel."; - m_cRepoUrl = channel.url; - break; - } + QLOG_INFO() << "Found matching channel."; + m_cRepoUrl = fixPathForTests(channel.url); + break; } + } - // Now that we've done that, load version info. - loadVersionInfo(); - }; + // 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()) + 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, processFunc); + QObject::connect(checker.get(), &UpdateChecker::channelListLoaded, this, + &DownloadUpdateTask::processChannels); checker->updateChanList(); } else { - processFunc(); + processChannels(); } } void DownloadUpdateTask::loadVersionInfo() { - setStatus(tr("Loading version information.")); + setStatus(tr("Loading version information...")); // Create the net job for loading version info. - NetJob* netJob = new NetJob("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)); @@ -115,10 +115,12 @@ void DownloadUpdateTask::loadVersionInfo() { 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::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! @@ -136,7 +138,8 @@ void DownloadUpdateTask::vinfoDownloadFinished() 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. + // 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(); @@ -150,61 +153,73 @@ void DownloadUpdateTask::vinfoDownloadFailed() void DownloadUpdateTask::parseDownloadedVersionInfo() { - setStatus(tr("Reading file lists.")); - - parseVersionInfo(NEW_VERSION, &m_nVersionFileList); + 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 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) { - parseVersionInfo(CURRENT_VERSION, &m_cVersionFileList); + 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. + // Now that we're done loading version info, we can move on to the next step. Process file + // lists and download files. processFileLists(); } -void DownloadUpdateTask::parseVersionInfo(VersionInfoFileEnum vfile, VersionFileList* list) +bool DownloadUpdateTask::parseVersionInfo(const QByteArray &data, VersionFileList *list, + QString *error) { - if (vfile == CURRENT_VERSION) setStatus(tr("Reading file list for current version.")); - else if (vfile == NEW_VERSION) setStatus(tr("Reading file list for new version.")); - - QLOG_DEBUG() << "Reading file list for" << (vfile == NEW_VERSION ? "new" : "current") << "version."; - - QByteArray data; - { - ByteArrayDownloadPtr dl = std::dynamic_pointer_cast<ByteArrayDownload>( - vfile == NEW_VERSION ? m_vinfoNetJob->first() : m_vinfoNetJob->operator[](1)); - data = dl->m_data; - } - QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error != QJsonParseError::NoError) { - QLOG_ERROR() << "Failed to parse version info JSON:" << jsonError.errorString() << "at" << jsonError.offset; - return; + *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(); - VersionFileEntry file{ - fileObj.value("Path").toString(), - fileObj.value("Perms").toVariant().toInt(), - FileSourceList(), - fileObj.value("MD5").toString(), - }; + 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) { @@ -213,11 +228,14 @@ void DownloadUpdateTask::parseVersionInfo(VersionInfoFileEnum vfile, VersionFile QString type = sourceObj.value("SourceType").toString(); if (type == "http") { - file.sources.append(FileSource("http", sourceObj.value("Url").toString())); + file.sources.append( + FileSource("http", fixPathForTests(sourceObj.value("Url").toString()))); } else if (type == "httpc") { - file.sources.append(FileSource("httpc", sourceObj.value("Url").toString(), sourceObj.value("CompressionType").toString())); + file.sources.append( + FileSource("httpc", fixPathForTests(sourceObj.value("Url").toString()), + sourceObj.value("CompressionType").toString())); } else { @@ -229,90 +247,185 @@ void DownloadUpdateTask::parseVersionInfo(VersionInfoFileEnum vfile, VersionFile list->append(file); } + + return true; } void DownloadUpdateTask::processFileLists() { - setStatus(tr("Processing file lists. Figuring out how to install the update.")); + // 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 ¤tVersion, + 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 + // 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 : m_cVersionFileList) + for (VersionFileEntry entry : currentVersion) { + QFileInfo toDelete(entry.path); + if (!toDelete.exists()) + { + QLOG_ERROR() << "Expected file " << toDelete.absoluteFilePath() + << " doesn't exist!"; + QLOG_ERROR() << "CWD: " << QDir::currentPath(); + } bool keep = false; - for (VersionFileEntry newEntry : m_nVersionFileList) + + // + for (VersionFileEntry newEntry : newVersion) { if (newEntry.path == entry.path) { - QLOG_DEBUG() << "Not deleting" << entry.path << "because it is still present in the new version."; + 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) - m_operationList.append(UpdateOperation::DeleteOp(entry.path)); + if (!keep) + { + QFileInfo toDelete(entry.path); + if (toDelete.exists()) + ops.append(UpdateOperation::DeleteOp(entry.path)); + } } - // Create a network job for downloading files. - NetJob* netJob = new NetJob("Update Files"); - // Next, check each file in MultiMC's folder and see if we need to update them. - for (VersionFileEntry entry : m_nVersionFileList) + 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. + // 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; QFile entryFile(entry.path); - if (entryFile.open(QFile::ReadOnly)) + QFileInfo entryInfo(entry.path); + + bool needs_upgrade = false; + if (!entryFile.exists()) { - QCryptographicHash hash(QCryptographicHash::Md5); - hash.addData(entryFile.readAll()); - fileMD5 = hash.result().toHex(); + needs_upgrade = true; + } + else + { + bool pass = true; + if (!entryInfo.isReadable()) + { + QLOG_ERROR() << "File " << entry.path << " is not readable."; + pass = false; + } + if (!entryInfo.isWritable()) + { + QLOG_ERROR() << "File " << entry.path << " is not writable."; + pass = false; + } + if (!entryFile.open(QFile::ReadOnly)) + { + QLOG_ERROR() << "File " << entry.path << " cannot be opened for reading."; + pass = false; + } + if (!pass) + { + QLOG_ERROR() << "CWD: " << QDir::currentPath(); + ops.clear(); + return false; + } } - if (!entryFile.exists() || fileMD5.isEmpty() || fileMD5 != entry.md5) + QCryptographicHash hash(QCryptographicHash::Md5); + auto foo = entryFile.readAll(); + + hash.addData(foo); + fileMD5 = hash.result().toHex(); + if ((fileMD5 != entry.md5)) { - QLOG_DEBUG() << "Found file" << entry.path << "that needs updating."; + 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" << entry.path << " does not need updating."; + continue; + } - // 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) + // yep. this file actually needs an upgrade. PROCEED. + QLOG_DEBUG() << "Found file" << entry.path << " 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") { - if (source.type == "http") - { - QLOG_DEBUG() << "Will download" << entry.path << "from" << source.url; + 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("/", "_")); + // 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("/", "_")); - // We need to download the file to the updatefiles folder and add a task to copy it to its install path. + if (isUpdater) + { + auto cache_entry = MMC->metacache()->resolveEntry("root", entry.path); + QLOG_DEBUG() << "Updater will be in " << cache_entry->getFullPath(); + if(cache_entry->stale) + { + auto download = CacheDownload::make(QUrl(source.url), cache_entry); + job->addNetAction(download); + } + } + 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_check_md5 = true; download->m_expected_md5 = entry.md5; - netJob->addNetAction(download); - - // Now add a copy operation to our operations list to install the file. - m_operationList.append(UpdateOperation::CopyOp(dlPath, entry.path, entry.mode)); + job->addNetAction(download); + ops.append(UpdateOperation::CopyOp(dlPath, entry.path, entry.mode)); } } } } - - // 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")); + return true; } -void DownloadUpdateTask::writeInstallScript(UpdateOperationList& opsList, QString scriptFile) +bool DownloadUpdateTask::writeInstallScript(UpdateOperationList &opsList, QString scriptFile) { // Build the base structure of the XML document. QDomDocument doc; @@ -334,36 +447,38 @@ void DownloadUpdateTask::writeInstallScript(UpdateOperationList& opsList, QStrin 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; - } - break; + 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; + 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; + default: + QLOG_WARN() << "Can't write update operation of type" << op.type + << "to file. Not implemented."; + continue; } } @@ -377,6 +492,36 @@ void DownloadUpdateTask::writeInstallScript(UpdateOperationList& opsList, QStrin else { emitFailed(tr("Failed to write update script file.")); + return false; + } + + return true; +} + +QString DownloadUpdateTask::fixPathForTests(const QString &path) +{ + if (path.startsWith("$PWD")) + { + QString foo = path; + foo.replace("$PWD", qApp->applicationDirPath()); + return QUrl::fromLocalFile(foo).toString(QUrl::FullyEncoded); + } + return path; +} + +bool DownloadUpdateTask::fixPathForOSX(QString &path) +{ + if (path.startsWith("MultiMC.app/")) + { + // remove the prefix and add a new, more appropriate one. + path.remove(0, 12); + path = QString("../../") + path; + return true; + } + else + { + QLOG_ERROR() << "Update path not within .app: " << path; + return false; } } @@ -394,11 +539,10 @@ void DownloadUpdateTask::fileDownloadFailed() void DownloadUpdateTask::fileDownloadProgressChanged(qint64 current, qint64 total) { - setProgress((int)(((float)current / (float)total)*100)); + 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 index f5b23d12..b1d14846 100644 --- a/logic/updater/DownloadUpdateTask.h +++ b/logic/updater/DownloadUpdateTask.h @@ -34,7 +34,8 @@ public: */ QString updateFilesDir(); -protected: +public: + // TODO: We should probably put these data structures into a separate header... /*! @@ -53,7 +54,6 @@ protected: QString url; QString compressionType; }; - typedef QList<FileSource> FileSourceList; /*! @@ -66,10 +66,8 @@ protected: FileSourceList sources; QString md5; }; - typedef QList<VersionFileEntry> VersionFileList; - /*! * Structure that describes an operation to perform when installing updates. */ @@ -100,9 +98,12 @@ protected: // 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. */ @@ -120,6 +121,13 @@ protected: 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 @@ -142,20 +150,25 @@ protected: /*! * Loads the file list from the given version info JSON object into the given list. */ - virtual void parseVersionInfo(VersionInfoFileEnum vfile, VersionFileList* 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 ¤tVersion, 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 void writeInstallScript(UpdateOperationList& opsList, QString scriptFile); + virtual bool writeInstallScript(UpdateOperationList& opsList, QString scriptFile); - VersionFileList m_downloadList; UpdateOperationList m_operationList; VersionFileList m_nVersionFileList; @@ -181,6 +194,26 @@ protected: */ QTemporaryDir m_updateFilesDir; + /*! + * Filters paths + * Path of the format $PWD/path, it is converted to a file:///$PWD/ URL + */ + static QString fixPathForTests(const QString &path); + + /*! + * 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 and prepend ../.. + * + * Returns false if the path couldn't be fixed (is invalid) + * + * Has no effect on systems that aren't OSX + */ + static bool fixPathForOSX(QString &path); + protected slots: void vinfoDownloadFinished(); void vinfoDownloadFailed(); diff --git a/logic/updater/UpdateChecker.cpp b/logic/updater/UpdateChecker.cpp index 5ff1898e..d0795c0d 100644 --- a/logic/updater/UpdateChecker.cpp +++ b/logic/updater/UpdateChecker.cpp @@ -44,17 +44,19 @@ QList<UpdateChecker::ChannelListEntry> UpdateChecker::getChannelList() const bool UpdateChecker::hasChannels() const { - return m_channels.isEmpty(); + return !m_channels.isEmpty(); } -void UpdateChecker::checkForUpdate() +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 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."; + QLOG_DEBUG() << "Channel list isn't loaded yet. Loading channel list and deferring " + "update check."; m_checkUpdateWaiting = true; updateChanList(); return; @@ -72,7 +74,8 @@ void UpdateChecker::checkForUpdate() // TODO: Allow user to select channels. For now, we'll just use the current channel. QString updateChannel = m_currentChannel; - // Find the desired channel within the channel list and get its repo URL. If if cannot be found, error. + // 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) { @@ -91,20 +94,22 @@ void UpdateChecker::checkForUpdate() auto job = new NetJob("GoUpdate Repository Index"); job->addNetAction(ByteArrayDownload::make(indexUrl)); - connect(job, SIGNAL(succeeded()), SLOT(updateCheckFinished())); + connect(job, &NetJob::succeeded, [this, notifyNoUpdate]() + { updateCheckFinished(notifyNoUpdate); }); connect(job, SIGNAL(failed()), SLOT(updateCheckFailed())); indexJob.reset(job); job->start(); } -void UpdateChecker::updateCheckFinished() +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()); + ByteArrayDownloadPtr dl = + std::dynamic_pointer_cast<ByteArrayDownload>(indexJob->first()); data = dl->m_data; indexJob.reset(); } @@ -112,7 +117,8 @@ void UpdateChecker::updateCheckFinished() 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; + QLOG_ERROR() << "Failed to parse GoUpdate repository index. JSON error" + << jsonError.errorString() << "at offset" << jsonError.offset; return; } @@ -122,7 +128,8 @@ void UpdateChecker::updateCheckFinished() 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; + QLOG_ERROR() << "Failed to check for updates. API version mismatch. We're using" + << API_VERSION << "server has" << apiVersion; return; } @@ -132,19 +139,27 @@ void UpdateChecker::updateCheckFinished() for (QJsonValue versionVal : versions) { QJsonObject version = versionVal.toObject(); - if (newestVersion.value("Id").toVariant().toInt() < version.value("Id").toVariant().toInt()) + if (newestVersion.value("Id").toVariant().toInt() < + version.value("Id").toVariant().toInt()) { - QLOG_DEBUG() << "Found newer version with ID" << version.value("Id").toVariant().toInt(); + QLOG_DEBUG() << "Found newer version with ID" + << 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. + // 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) { // Update! - emit updateAvailable(m_repoUrl, newestVersion.value("Name").toVariant().toString(), newBuildNumber); + emit updateAvailable(m_repoUrl, newestVersion.value("Name").toVariant().toString(), + newBuildNumber); + } + else if (notifyNoUpdate) + { + emit noUpdateFound(); } m_updateChecking = false; @@ -163,12 +178,13 @@ void UpdateChecker::updateChanList() 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."; + << "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"); + 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); @@ -180,7 +196,8 @@ void UpdateChecker::chanListDownloadFinished() { QByteArray data; { - ByteArrayDownloadPtr dl = std::dynamic_pointer_cast<ByteArrayDownload>(chanListJob->first()); + ByteArrayDownloadPtr dl = + std::dynamic_pointer_cast<ByteArrayDownload>(chanListJob->first()); data = dl->m_data; chanListJob.reset(); } @@ -190,17 +207,20 @@ void UpdateChecker::chanListDownloadFinished() if (jsonError.error != QJsonParseError::NoError) { // TODO: Report errors to the user. - QLOG_ERROR() << "Failed to parse channel list JSON:" << jsonError.errorString() << "at" << jsonError.offset; + 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; + QLOG_ERROR() + << "Failed to check for updates. Channel list format version mismatch. We're using" + << CHANLIST_FORMAT << "server has" << formatVersion; return; } @@ -210,12 +230,10 @@ void UpdateChecker::chanListDownloadFinished() 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() - }; + 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."; @@ -233,7 +251,7 @@ void UpdateChecker::chanListDownloadFinished() // If we're waiting to check for updates, do that now. if (m_checkUpdateWaiting) - checkForUpdate(); + checkForUpdate(false); emit channelListLoaded(); } @@ -244,4 +262,3 @@ void UpdateChecker::chanListDownloadFailed() QLOG_ERROR() << "Failed to download channel list."; emit channelListLoaded(); } - diff --git a/logic/updater/UpdateChecker.h b/logic/updater/UpdateChecker.h index 59fb8e47..a47e8903 100644 --- a/logic/updater/UpdateChecker.h +++ b/logic/updater/UpdateChecker.h @@ -25,7 +25,10 @@ class UpdateChecker : public QObject public: UpdateChecker(); - void checkForUpdate(); + void checkForUpdate(bool notifyNoUpdate); + + void setCurrentChannel(const QString &channel) { m_currentChannel = channel; } + 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). @@ -62,14 +65,18 @@ signals: //! Signal emitted when the channel list finishes loading or fails to load. void channelListLoaded(); + void noUpdateFound(); + private slots: - void updateCheckFinished(); + void updateCheckFinished(bool notifyNoUpdate); void updateCheckFailed(); void chanListDownloadFinished(); void chanListDownloadFailed(); private: + friend class UpdateCheckerTest; + NetJobPtr indexJob; NetJobPtr chanListJob; |