diff options
Diffstat (limited to 'logic')
54 files changed, 2435 insertions, 731 deletions
diff --git a/logic/BaseInstance.h b/logic/BaseInstance.h index 2d7537d6..93e57414 100644 --- a/logic/BaseInstance.h +++ b/logic/BaseInstance.h @@ -150,7 +150,7 @@ public: virtual SettingsObject &settings() const; /// returns a valid update task if update is needed, NULL otherwise - virtual Task *doUpdate(bool prepare_for_launch) = 0; + virtual std::shared_ptr<Task> doUpdate(bool only_prepare) = 0; /// returns a valid minecraft process, ready for launch with the given account. virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account) = 0; diff --git a/logic/JavaChecker.cpp b/logic/JavaChecker.cpp index daad7281..2b94fbb6 100644 --- a/logic/JavaChecker.cpp +++ b/logic/JavaChecker.cpp @@ -1,6 +1,8 @@ #include "JavaChecker.h" #include <QFile> #include <QProcess> +#include <QMap> +#include <QTemporaryFile> #define CHECKER_FILE "JavaChecker.jar" @@ -8,16 +10,17 @@ JavaChecker::JavaChecker(QObject *parent) : QObject(parent) { } -void JavaChecker::performCheck(QString path) +void JavaChecker::performCheck() { - if(QFile::exists(CHECKER_FILE)) - { - QFile::remove(CHECKER_FILE); - } - // extract the checker - QFile(":/java/checker.jar").copy(CHECKER_FILE); + checkerJar.setFileTemplate("checker_XXXXXX.jar"); + checkerJar.open(); + QFile inner(":/java/checker.jar"); + inner.open(QIODevice::ReadOnly); + checkerJar.write(inner.readAll()); + inner.close(); + checkerJar.close(); - QStringList args = {"-jar", CHECKER_FILE}; + QStringList args = {"-jar", checkerJar.fileName()}; process.reset(new QProcess()); process->setArguments(args); @@ -39,31 +42,55 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) killTimer.stop(); QProcessPtr _process; _process.swap(process); + checkerJar.remove(); + + JavaCheckResult result; + { + result.path = path; + } if (status == QProcess::CrashExit || exitcode == 1) { - emit checkFinished({}); + emit checkFinished(result); return; } + bool success = true; QString p_stdout = _process->readAllStandardOutput(); - auto parts = p_stdout.split('=', QString::SkipEmptyParts); - if (parts.size() != 2 || parts[0] != "os.arch") + QMap<QString, QString> results; + QStringList lines = p_stdout.split("\n", QString::SkipEmptyParts); + for(QString line : lines) { - emit checkFinished({}); + line = line.trimmed(); + + auto parts = line.split('=', QString::SkipEmptyParts); + if(parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) + { + success = false; + } + else + { + results.insert(parts[0], parts[1]); + } + } + + if(!results.contains("os.arch") || !results.contains("java.version") || !success) + { + emit checkFinished(result); return; } - auto os_arch = parts[1].remove('\n').remove('\r'); + auto os_arch = results["os.arch"]; + auto java_version = results["java.version"]; bool is_64 = os_arch == "x86_64" || os_arch == "amd64"; - JavaCheckResult result; - { - result.valid = true; - result.is_64bit = is_64; - result.mojangPlatform = is_64 ? "64" : "32"; - result.realPlatform = os_arch; - } + + result.valid = true; + result.is_64bit = is_64; + result.mojangPlatform = is_64 ? "64" : "32"; + result.realPlatform = os_arch; + result.javaVersion = java_version; + emit checkFinished(result); } @@ -72,7 +99,13 @@ void JavaChecker::error(QProcess::ProcessError err) if(err == QProcess::FailedToStart) { killTimer.stop(); - emit checkFinished({}); + + JavaCheckResult result; + { + result.path = path; + } + + emit checkFinished(result); return; } } diff --git a/logic/JavaChecker.h b/logic/JavaChecker.h index 34782383..291bf46c 100644 --- a/logic/JavaChecker.h +++ b/logic/JavaChecker.h @@ -1,29 +1,39 @@ #pragma once #include <QProcess> #include <QTimer> +#include <QTemporaryFile> #include <memory> +class JavaChecker; + + struct JavaCheckResult { + QString path; QString mojangPlatform; QString realPlatform; + QString javaVersion; bool valid = false; bool is_64bit = false; }; -typedef std::shared_ptr<QProcess> QProcessPtr; +typedef std::shared_ptr<QProcess> QProcessPtr; +typedef std::shared_ptr<JavaChecker> JavaCheckerPtr; class JavaChecker : public QObject { Q_OBJECT public: explicit JavaChecker(QObject *parent = 0); - void performCheck(QString path); + void performCheck(); + + QString path; signals: void checkFinished(JavaCheckResult result); private: QProcessPtr process; QTimer killTimer; + QTemporaryFile checkerJar; public slots: void timeout(); diff --git a/logic/JavaCheckerJob.cpp b/logic/JavaCheckerJob.cpp new file mode 100644 index 00000000..36a8a050 --- /dev/null +++ b/logic/JavaCheckerJob.cpp @@ -0,0 +1,49 @@ +/* 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 "JavaCheckerJob.h" +#include "pathutils.h" +#include "MultiMC.h" + +#include "logger/QsLog.h" + +void JavaCheckerJob::partFinished(JavaCheckResult result) +{ + num_finished++; + QLOG_INFO() << m_job_name.toLocal8Bit() << "progress:" << num_finished << "/" + << javacheckers.size(); + emit progress(num_finished, javacheckers.size()); + + javaresults.append(result); + int result_size = javacheckers.size(); + + emit progress(num_finished, result_size); + + if (num_finished == javacheckers.size()) + { + emit finished(javaresults); + } +} + +void JavaCheckerJob::start() +{ + QLOG_INFO() << m_job_name.toLocal8Bit() << " started."; + m_running = true; + for (auto iter : javacheckers) + { + connect(iter.get(), SIGNAL(checkFinished(JavaCheckResult)), SLOT(partFinished(JavaCheckResult))); + iter->performCheck(); + } +} diff --git a/logic/JavaCheckerJob.h b/logic/JavaCheckerJob.h new file mode 100644 index 00000000..132a92d4 --- /dev/null +++ b/logic/JavaCheckerJob.h @@ -0,0 +1,100 @@ +/* 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 <QtNetwork> +#include <QLabel> +#include "JavaChecker.h" +#include "logic/tasks/ProgressProvider.h" + +class JavaCheckerJob; +typedef std::shared_ptr<JavaCheckerJob> JavaCheckerJobPtr; + +class JavaCheckerJob : public ProgressProvider +{ + Q_OBJECT +public: + explicit JavaCheckerJob(QString job_name) : ProgressProvider(), m_job_name(job_name) {}; + + bool addJavaCheckerAction(JavaCheckerPtr base) + { + javacheckers.append(base); + total_progress++; + // if this is already running, the action needs to be started right away! + if (isRunning()) + { + emit progress(current_progress, total_progress); + connect(base.get(), SIGNAL(checkFinished(JavaCheckResult)), SLOT(partFinished(JavaCheckResult))); + + base->performCheck(); + } + return true; + } + + JavaCheckerPtr operator[](int index) + { + return javacheckers[index]; + } + ; + JavaCheckerPtr first() + { + if (javacheckers.size()) + return javacheckers[0]; + return JavaCheckerPtr(); + } + int size() const + { + return javacheckers.size(); + } + virtual void getProgress(qint64 ¤t, qint64 &total) + { + current = current_progress; + total = total_progress; + } + ; + virtual QString getStatus() const + { + return m_job_name; + } + ; + virtual bool isRunning() const + { + return m_running; + } + ; + +signals: + void started(); + void progress(int current, int total); + void finished(QList<JavaCheckResult>); +public +slots: + virtual void start(); + // FIXME: implement + virtual void abort() {}; +private +slots: + void partFinished(JavaCheckResult result); + +private: + QString m_job_name; + QList<JavaCheckerPtr> javacheckers; + QList<JavaCheckResult> javaresults; + qint64 current_progress = 0; + qint64 total_progress = 0; + int num_finished = 0; + bool m_running = false; +}; diff --git a/logic/JavaUtils.cpp b/logic/JavaUtils.cpp index 61d0231a..e1b3bc64 100644 --- a/logic/JavaUtils.cpp +++ b/logic/JavaUtils.cpp @@ -26,11 +26,24 @@ #include "JavaUtils.h" #include "logger/QsLog.h" #include "gui/dialogs/VersionSelectDialog.h" +#include "JavaCheckerJob.h" +#include "lists/JavaVersionList.h" JavaUtils::JavaUtils() { } +JavaVersionPtr JavaUtils::MakeJavaPtr(QString path, QString id, QString arch) +{ + JavaVersionPtr javaVersion(new JavaVersion()); + + javaVersion->id = id; + javaVersion->arch = arch; + javaVersion->path = path; + + return javaVersion; +} + JavaVersionPtr JavaUtils::GetDefaultJava() { JavaVersionPtr javaVersion(new JavaVersion()); @@ -38,7 +51,6 @@ JavaVersionPtr JavaUtils::GetDefaultJava() javaVersion->id = "java"; javaVersion->arch = "unknown"; javaVersion->path = "java"; - javaVersion->recommended = false; return javaVersion; } @@ -112,7 +124,6 @@ QList<JavaVersionPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString javaVersion->arch = archType; javaVersion->path = QDir(PathCombine(value, "bin")).absoluteFilePath("java.exe"); - javaVersion->recommended = (recommended == subKeyName); javas.append(javaVersion); } @@ -128,9 +139,9 @@ QList<JavaVersionPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString return javas; } -QList<JavaVersionPtr> JavaUtils::FindJavaPaths() +QList<QString> JavaUtils::FindJavaPaths() { - QList<JavaVersionPtr> javas; + QList<JavaVersionPtr> java_candidates; QList<JavaVersionPtr> JRE64s = this->FindJavaFromRegistryKey( KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment"); @@ -141,58 +152,56 @@ QList<JavaVersionPtr> JavaUtils::FindJavaPaths() QList<JavaVersionPtr> JDK32s = this->FindJavaFromRegistryKey( KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Development Kit"); - javas.append(JRE64s); - javas.append(JDK64s); - javas.append(JRE32s); - javas.append(JDK32s); - - if (javas.size() <= 0) - { - QLOG_WARN() << "Failed to find Java in the Windows registry - defaulting to \"java\""; - javas.append(this->GetDefaultJava()); - return javas; - } - - QLOG_INFO() << "Found the following Java installations (64 -> 32, JRE -> JDK): "; - - for (auto &java : javas) + java_candidates.append(JRE64s); + java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/java.exe")); + java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/java.exe")); + java_candidates.append(JDK64s); + java_candidates.append(JRE32s); + java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/java.exe")); + java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/java.exe")); + java_candidates.append(JDK32s); + java_candidates.append(MakeJavaPtr(this->GetDefaultJava()->path)); + + QList<QString> candidates; + for(JavaVersionPtr java_candidate : java_candidates) { - QString sRec; - if (java->recommended) - sRec = "(Recommended)"; - QLOG_INFO() << java->id << java->arch << " at " << java->path << sRec; + if(!candidates.contains(java_candidate->path)) + { + candidates.append(java_candidate->path); + } } - return javas; + return candidates; } + #elif OSX -QList<JavaVersionPtr> JavaUtils::FindJavaPaths() +QList<QString> JavaUtils::FindJavaPaths() { QLOG_INFO() << "OS X Java detection incomplete - defaulting to \"java\""; - QList<JavaVersionPtr> javas; - javas.append(this->GetDefaultJava()); + QList<QString> javas; + javas.append(this->GetDefaultJava()->path); return javas; } #elif LINUX -QList<JavaVersionPtr> JavaUtils::FindJavaPaths() +QList<QString> JavaUtils::FindJavaPaths() { QLOG_INFO() << "Linux Java detection incomplete - defaulting to \"java\""; - QList<JavaVersionPtr> javas; - javas.append(this->GetDefaultJava()); + QList<QString> javas; + javas.append(this->GetDefaultJava()->path); return javas; } #else -QList<JavaVersionPtr> JavaUtils::FindJavaPaths() +QList<QString> JavaUtils::FindJavaPaths() { QLOG_INFO() << "Unknown operating system build - defaulting to \"java\""; - QList<JavaVersionPtr> javas; - javas.append(this->GetDefaultJava()); + QList<QString> javas; + javas.append(this->GetDefaultJava()->path); return javas; } diff --git a/logic/JavaUtils.h b/logic/JavaUtils.h index 44f576b4..22a68ef3 100644 --- a/logic/JavaUtils.h +++ b/logic/JavaUtils.h @@ -19,21 +19,23 @@ #include <QWidget> #include <osutils.h> - -#include "logic/lists/JavaVersionList.h" +#include "JavaCheckerJob.h" +#include "JavaChecker.h" +#include "lists/JavaVersionList.h" #if WINDOWS #include <windows.h> #endif -class JavaUtils +class JavaUtils : public QObject { + Q_OBJECT public: JavaUtils(); - QList<JavaVersionPtr> FindJavaPaths(); + JavaVersionPtr MakeJavaPtr(QString path, QString id = "unknown", QString arch = "unknown"); + QList<QString> FindJavaPaths(); JavaVersionPtr GetDefaultJava(); -private: #if WINDOWS QList<JavaVersionPtr> FindJavaFromRegistryKey(DWORD keyType, QString keyName); diff --git a/logic/LegacyInstance.cpp b/logic/LegacyInstance.cpp index 72b6c51a..fef27bcd 100644 --- a/logic/LegacyInstance.cpp +++ b/logic/LegacyInstance.cpp @@ -44,12 +44,12 @@ LegacyInstance::LegacyInstance(const QString &rootDir, SettingsObject *settings, settings->registerSetting(new Setting("IntendedJarVersion", "")); } -Task *LegacyInstance::doUpdate(bool prepare_for_launch) +std::shared_ptr<Task> LegacyInstance::doUpdate(bool only_prepare) { // make sure the jar mods list is initialized by asking for it. auto list = jarModList(); // create an update task - return new LegacyUpdate(this, prepare_for_launch , this); + return std::shared_ptr<Task> (new LegacyUpdate(this, only_prepare , this)); } MinecraftProcess *LegacyInstance::prepareForLaunch(MojangAccountPtr account) @@ -105,7 +105,7 @@ MinecraftProcess *LegacyInstance::prepareForLaunch(MojangAccountPtr account) #endif args << "-jar" << LAUNCHER_FILE; - args << account->currentProfile()->name(); + args << account->currentProfile()->name; args << account->sessionId(); args << windowTitle; args << windowSize; diff --git a/logic/LegacyInstance.h b/logic/LegacyInstance.h index a17ef281..1e7d9eb6 100644 --- a/logic/LegacyInstance.h +++ b/logic/LegacyInstance.h @@ -76,7 +76,7 @@ public: virtual bool shouldUpdate() const override; virtual void setShouldUpdate(bool val) override; - virtual Task *doUpdate(bool prepare_for_launch) override; + virtual std::shared_ptr<Task> doUpdate(bool only_prepare) override; virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account) override; virtual void cleanupAfterRun() override; diff --git a/logic/LegacyUpdate.cpp b/logic/LegacyUpdate.cpp index 3fc17351..e71b270e 100644 --- a/logic/LegacyUpdate.cpp +++ b/logic/LegacyUpdate.cpp @@ -25,15 +25,32 @@ #include <quazipfile.h> #include <JlCompress.h> #include "logger/QsLog.h" +#include "logic/net/URLConstants.h" -LegacyUpdate::LegacyUpdate(BaseInstance *inst, bool prepare_for_launch, QObject *parent) - : Task(parent), m_inst(inst), m_prepare_for_launch(prepare_for_launch) +LegacyUpdate::LegacyUpdate(BaseInstance *inst, bool only_prepare, QObject *parent) + : Task(parent), m_inst(inst), m_only_prepare(only_prepare) { } void LegacyUpdate::executeTask() { - lwjglStart(); + if(m_only_prepare) + { + // FIXME: think this through some more. + LegacyInstance *inst = (LegacyInstance *)m_inst; + if (!inst->shouldUpdate() || inst->shouldUseCustomBaseJar()) + { + ModTheJar(); + } + else + { + emitSucceeded(); + } + } + else + { + lwjglStart(); + } } void LegacyUpdate::lwjglStart() @@ -245,16 +262,20 @@ void LegacyUpdate::jarStart() // Build a list of URLs that will need to be downloaded. setStatus("Downloading new minecraft.jar"); - QString urlstr("http://s3.amazonaws.com/Minecraft.Download/versions/"); - QString intended_version_id = inst->intendedVersionId(); - urlstr += intended_version_id + "/" + intended_version_id + ".jar"; + QString version_id = inst->intendedVersionId(); + QString localPath = version_id + "/" + version_id + ".jar"; + QString urlstr = "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + localPath; - auto dljob = new NetJob("Minecraft.jar for version " + intended_version_id); - dljob->addNetAction(FileDownload::make(QUrl(urlstr), inst->defaultBaseJar())); - legacyDownloadJob.reset(dljob); + auto dljob = new NetJob("Minecraft.jar for version " + version_id); + + + auto metacache = MMC->metacache(); + auto entry = metacache->resolveEntry("versions", localPath); + dljob->addNetAction(CacheDownload::make(QUrl(urlstr), entry)); connect(dljob, SIGNAL(succeeded()), SLOT(jarFinished())); connect(dljob, SIGNAL(failed()), SLOT(jarFailed())); connect(dljob, SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); + legacyDownloadJob.reset(dljob); legacyDownloadJob->start(); } @@ -466,4 +487,4 @@ void LegacyUpdate::ModTheJar() // inst->UpdateVersion(true); emitSucceeded(); return; -}
\ No newline at end of file +} diff --git a/logic/LegacyUpdate.h b/logic/LegacyUpdate.h index d753197f..0b573ca5 100644 --- a/logic/LegacyUpdate.h +++ b/logic/LegacyUpdate.h @@ -31,7 +31,7 @@ class LegacyUpdate : public Task { Q_OBJECT public: - explicit LegacyUpdate(BaseInstance *inst, bool prepare_for_launch, QObject *parent = 0); + explicit LegacyUpdate(BaseInstance *inst, bool only_prepare, QObject *parent = 0); virtual void executeTask(); private @@ -72,5 +72,5 @@ private: private: NetJobPtr legacyDownloadJob; BaseInstance *m_inst = nullptr; - bool m_prepare_for_launch = false; + bool m_only_prepare = false; }; diff --git a/logic/MinecraftProcess.cpp b/logic/MinecraftProcess.cpp index 5d99bfae..209929b7 100644 --- a/logic/MinecraftProcess.cpp +++ b/logic/MinecraftProcess.cpp @@ -75,20 +75,22 @@ QString MinecraftProcess::censorPrivateInfo(QString in) { if(!m_account) return in; - else + + QString sessionId = m_account->sessionId(); + QString accessToken = m_account->accessToken(); + QString clientToken = m_account->clientToken(); + in.replace(sessionId, "<SESSION ID>"); + in.replace(accessToken, "<ACCESS TOKEN>"); + in.replace(clientToken, "<CLIENT TOKEN>"); + auto profile = m_account->currentProfile(); + if(profile) { - QString sessionId = m_account->sessionId(); - QString accessToken = m_account->accessToken(); - QString clientToken = m_account->clientToken(); - QString profileId = m_account->currentProfile()->id(); - QString profileName = m_account->currentProfile()->name(); - in.replace(sessionId, "<SESSION ID>"); - in.replace(accessToken, "<ACCESS TOKEN>"); - in.replace(clientToken, "<CLIENT TOKEN>"); + QString profileId = profile->id; + QString profileName = profile->name; in.replace(profileId, "<PROFILE ID>"); in.replace(profileName, "<PROFILE NAME>"); - return in; } + return in; } // console window diff --git a/logic/OneSixAssets.cpp b/logic/OneSixAssets.cpp deleted file mode 100644 index 400aff2c..00000000 --- a/logic/OneSixAssets.cpp +++ /dev/null @@ -1,127 +0,0 @@ -/* 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 <QString> -#include "logger/QsLog.h" -#include <QtXml/QtXml> -#include "OneSixAssets.h" -#include "net/NetJob.h" -#include "net/HttpMetaCache.h" -#include "net/S3ListBucket.h" -#include "MultiMC.h" - -#define ASSETS_URL "http://resources.download.minecraft.net/" - -class ThreadedDeleter : public QThread -{ - Q_OBJECT -public: - void run() - { - QLOG_INFO() << "Cleaning up assets folder..."; - QDirIterator iter(m_base, QDirIterator::Subdirectories); - int base_length = m_base.length(); - while (iter.hasNext()) - { - QString filename = iter.next(); - QFileInfo current(filename); - // we keep the dirs... whatever - if (current.isDir()) - continue; - QString trimmedf = filename; - trimmedf.remove(0, base_length + 1); - if (m_whitelist.contains(trimmedf)) - { - QLOG_TRACE() << trimmedf << " gets to live"; - } - else - { - // DO NOT TOLERATE JUNK - QLOG_TRACE() << trimmedf << " dies"; - QFile f(filename); - f.remove(); - } - } - } - QString m_base; - QStringList m_whitelist; -}; - -void OneSixAssets::downloadFinished() -{ - deleter = new ThreadedDeleter(); - QDir dir("assets"); - deleter->m_base = dir.absolutePath(); - deleter->m_whitelist = nuke_whitelist; - connect(deleter, SIGNAL(finished()), SIGNAL(finished())); - deleter->start(); -} - -void OneSixAssets::S3BucketFinished() -{ - QString prefix(ASSETS_URL); - nuke_whitelist.clear(); - - emit filesStarted(); - - auto firstJob = index_job->first(); - auto objectList = std::dynamic_pointer_cast<S3ListBucket>(firstJob)->objects; - - NetJob *job = new NetJob("Assets"); - - connect(job, SIGNAL(succeeded()), SLOT(downloadFinished())); - connect(job, SIGNAL(failed()), SIGNAL(failed())); - connect(job, SIGNAL(filesProgress(int, int, int)), SIGNAL(filesProgress(int, int, int))); - - auto metacache = MMC->metacache(); - - for (auto object : objectList) - { - // Filter folder keys (zero size) - if (object.size == 0) - continue; - - nuke_whitelist.append(object.Key); - - auto entry = metacache->resolveEntry("assets", object.Key, object.ETag); - if (entry->stale) - { - job->addNetAction(CacheDownload::make(QUrl(prefix + object.Key), entry)); - } - } - if (job->size()) - { - files_job.reset(job); - files_job->start(); - } - else - { - delete job; - emit finished(); - } -} - -void OneSixAssets::start() -{ - auto job = new NetJob("Assets index"); - job->addNetAction(S3ListBucket::make(QUrl(ASSETS_URL))); - connect(job, SIGNAL(succeeded()), SLOT(S3BucketFinished())); - connect(job, SIGNAL(failed()), SIGNAL(failed())); - emit indexStarted(); - index_job.reset(job); - job->start(); -} - -#include "OneSixAssets.moc" diff --git a/logic/OneSixInstance.cpp b/logic/OneSixInstance.cpp index 08b63bf9..fd41b9e5 100644 --- a/logic/OneSixInstance.cpp +++ b/logic/OneSixInstance.cpp @@ -26,6 +26,7 @@ #include <JlCompress.h> #include "gui/dialogs/OneSixModEditDialog.h" #include "logger/QsLog.h" +#include "logic/assets/AssetsUtils.h" OneSixInstance::OneSixInstance(const QString &rootDir, SettingsObject *setting_obj, QObject *parent) @@ -37,9 +38,9 @@ OneSixInstance::OneSixInstance(const QString &rootDir, SettingsObject *setting_o reloadFullVersion(); } -Task *OneSixInstance::doUpdate(bool prepare_for_launch) +std::shared_ptr<Task> OneSixInstance::doUpdate(bool only_prepare) { - return new OneSixUpdate(this, prepare_for_launch); + return std::shared_ptr<Task> (new OneSixUpdate(this, only_prepare)); } QString replaceTokensIn(QString text, QMap<QString, QString> with) @@ -66,6 +67,63 @@ QString replaceTokensIn(QString text, QMap<QString, QString> with) return result; } +QDir OneSixInstance::reconstructAssets(std::shared_ptr<OneSixVersion> version) +{ + QDir assetsDir = QDir("assets/"); + QDir indexDir = QDir(PathCombine(assetsDir.path(), "indexes")); + QDir objectDir = QDir(PathCombine(assetsDir.path(), "objects")); + QDir virtualDir = QDir(PathCombine(assetsDir.path(), "virtual")); + + QString indexPath = PathCombine(indexDir.path(), version->assets + ".json"); + QFile indexFile(indexPath); + QDir virtualRoot(PathCombine(virtualDir.path(), version->assets)); + + if(!indexFile.exists()) + { + QLOG_ERROR() << "No assets index file" << indexPath << "; can't reconstruct assets"; + return virtualRoot; + } + + QLOG_DEBUG() << "reconstructAssets" << assetsDir.path() << indexDir.path() << objectDir.path() << virtualDir.path() << virtualRoot.path(); + + AssetsIndex index; + bool loadAssetsIndex = AssetsUtils::loadAssetsIndexJson(indexPath, &index); + + if(loadAssetsIndex) + { + if(index.isVirtual) + { + QLOG_INFO() << "Reconstructing virtual assets folder at" << virtualRoot.path(); + + for(QString map : index.objects.keys()) + { + AssetObject asset_object = index.objects.value(map); + QString target_path = PathCombine(virtualRoot.path(), map); + QFile target(target_path); + + QString tlk = asset_object.hash.left(2); + + QString original_path = PathCombine(PathCombine(objectDir.path(), tlk), asset_object.hash); + QFile original(original_path); + if(!target.exists()) + { + QFileInfo info(target_path); + QDir target_dir = info.dir(); + //QLOG_DEBUG() << target_dir; + if(!target_dir.exists()) QDir("").mkpath(target_dir.path()); + + bool couldCopy = original.copy(target_path); + QLOG_DEBUG() << " Copying" << original_path << "to" << target_path << QString::number(couldCopy);// << original.errorString(); + } + } + + // TODO: Write last used time to virtualRoot/.lastused + } + } + + return virtualRoot; +} + QStringList OneSixInstance::processMinecraftArgs(MojangAccountPtr account) { I_D(OneSixInstance); @@ -77,8 +135,8 @@ QStringList OneSixInstance::processMinecraftArgs(MojangAccountPtr account) token_mapping["auth_username"] = account->username(); token_mapping["auth_session"] = account->sessionId(); token_mapping["auth_access_token"] = account->accessToken(); - token_mapping["auth_player_name"] = account->currentProfile()->name(); - token_mapping["auth_uuid"] = account->currentProfile()->id(); + token_mapping["auth_player_name"] = account->currentProfile()->name; + token_mapping["auth_uuid"] = account->currentProfile()->id; // this is for offline?: /* @@ -93,9 +151,22 @@ QStringList OneSixInstance::processMinecraftArgs(MojangAccountPtr account) QString absRootDir = QDir(minecraftRoot()).absolutePath(); token_mapping["game_directory"] = absRootDir; QString absAssetsDir = QDir("assets/").absolutePath(); - token_mapping["game_assets"] = absAssetsDir; - //TODO: this is something new and not even fully implemented in the vanilla launcher. - token_mapping["user_properties"] = "{ }"; + token_mapping["game_assets"] = reconstructAssets(d->version).absolutePath(); + + auto user = account->user(); + QJsonObject userAttrs; + for(auto key: user.properties.keys()) + { + auto array = QJsonArray::fromStringList(user.properties.values(key)); + userAttrs.insert(key, array); + } + QJsonDocument value(userAttrs); + + token_mapping["user_properties"] = value.toJson(QJsonDocument::Compact); + token_mapping["user_type"] = account->currentProfile()->legacy ? "legacy" : "mojang"; + // 1.7.3+ assets tokens + token_mapping["assets_root"] = absAssetsDir; + token_mapping["assets_index_name"] = version->assets; QStringList parts = args_pattern.split(' ', QString::SkipEmptyParts); for (int i = 0; i < parts.length(); i++) diff --git a/logic/OneSixInstance.h b/logic/OneSixInstance.h index 042c104b..f869e345 100644 --- a/logic/OneSixInstance.h +++ b/logic/OneSixInstance.h @@ -16,6 +16,7 @@ #pragma once #include <QStringList> +#include <QDir> #include "BaseInstance.h" @@ -39,7 +40,7 @@ public: QString loaderModsDir() const; virtual QString instanceConfigFolder() const override; - virtual Task *doUpdate(bool prepare_for_launch) override; + virtual std::shared_ptr<Task> doUpdate(bool only_prepare) override; virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account) override; virtual void cleanupAfterRun() override; @@ -73,4 +74,5 @@ public: private: QStringList processMinecraftArgs(MojangAccountPtr account); + QDir reconstructAssets(std::shared_ptr<OneSixVersion> version); }; diff --git a/logic/OneSixLibrary.cpp b/logic/OneSixLibrary.cpp index ad4aec44..4b6ed9dc 100644 --- a/logic/OneSixLibrary.cpp +++ b/logic/OneSixLibrary.cpp @@ -18,6 +18,7 @@ #include "OneSixLibrary.h" #include "OneSixRule.h" #include "OpSys.h" +#include "logic/net/URLConstants.h" void OneSixLibrary::finalize() { @@ -140,9 +141,9 @@ QJsonObject OneSixLibrary::toJson() libRoot.insert("MMC-absoluteUrl", m_absolute_url); if (m_hint.size()) libRoot.insert("MMC-hint", m_hint); - if (m_base_url != "http://s3.amazonaws.com/Minecraft.Download/libraries/" && - m_base_url != "https://s3.amazonaws.com/Minecraft.Download/libraries/" && - m_base_url != "https://libraries.minecraft.net/") + if (m_base_url != "http://" + URLConstants::AWS_DOWNLOAD_LIBRARIES && + m_base_url != "https://" + URLConstants::AWS_DOWNLOAD_LIBRARIES && + m_base_url != "https://" + URLConstants::LIBRARY_BASE) libRoot.insert("url", m_base_url); if (isNative() && m_native_suffixes.size()) { diff --git a/logic/OneSixLibrary.h b/logic/OneSixLibrary.h index f8dc3aef..5cb867c2 100644 --- a/logic/OneSixLibrary.h +++ b/logic/OneSixLibrary.h @@ -21,6 +21,7 @@ #include <QJsonObject> #include <memory> +#include "logic/net/URLConstants.h" #include "OpSys.h" class Rule; @@ -30,7 +31,7 @@ class OneSixLibrary private: // basic values used internally (so far) QString m_name; - QString m_base_url = "https://libraries.minecraft.net/"; + QString m_base_url = "https://" + URLConstants::LIBRARY_BASE; QList<std::shared_ptr<Rule>> m_rules; // custom values diff --git a/logic/OneSixUpdate.cpp b/logic/OneSixUpdate.cpp index 25e16328..66950fc4 100644 --- a/logic/OneSixUpdate.cpp +++ b/logic/OneSixUpdate.cpp @@ -29,12 +29,14 @@ #include "OneSixLibrary.h" #include "OneSixInstance.h" #include "net/ForgeMirrors.h" +#include "net/URLConstants.h" +#include "assets/AssetsUtils.h" #include "pathutils.h" #include <JlCompress.h> -OneSixUpdate::OneSixUpdate(BaseInstance *inst, bool prepare_for_launch, QObject *parent) - : Task(parent), m_inst(inst), m_prepare_for_launch(prepare_for_launch) +OneSixUpdate::OneSixUpdate(BaseInstance *inst, bool only_prepare, QObject *parent) + : Task(parent), m_inst(inst), m_only_prepare(only_prepare) { } @@ -50,6 +52,24 @@ void OneSixUpdate::executeTask() return; } + if (m_only_prepare) + { + if (m_inst->shouldUpdate()) + { + emitFailed("Unable to update instance in offline mode."); + return; + } + setStatus("Testing the Java installation."); + QString java_path = m_inst->settings().get("JavaPath").toString(); + + checker.reset(new JavaChecker()); + connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this, + SLOT(checkFinishedOffline(JavaCheckResult))); + checker->path = java_path; + checker->performCheck(); + return; + } + if (m_inst->shouldUpdate()) { // Get a pointer to the version object that corresponds to the instance's version. @@ -65,35 +85,44 @@ void OneSixUpdate::executeTask() } else { - checkJava(); + checkJavaOnline(); } } -void OneSixUpdate::checkJava() +void OneSixUpdate::checkJavaOnline() { - QLOG_INFO() << m_inst->name() << ": checking java binary"; setStatus("Testing the Java installation."); - // TODO: cache this so we don't have to run an extra java process every time. QString java_path = m_inst->settings().get("JavaPath").toString(); checker.reset(new JavaChecker()); connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this, - SLOT(checkFinished(JavaCheckResult))); - checker->performCheck(java_path); + SLOT(checkFinishedOnline(JavaCheckResult))); + checker->path = java_path; + checker->performCheck(); } -void OneSixUpdate::checkFinished(JavaCheckResult result) +void OneSixUpdate::checkFinishedOnline(JavaCheckResult result) { if (result.valid) { - QLOG_INFO() << m_inst->name() << ": java is " - << (result.is_64bit ? "64 bit" : "32 bit"); java_is_64bit = result.is_64bit; jarlibStart(); } else { - QLOG_INFO() << m_inst->name() << ": java isn't valid"; + emitFailed("The java binary doesn't work. Check the settings and correct the problem"); + } +} + +void OneSixUpdate::checkFinishedOffline(JavaCheckResult result) +{ + if (result.valid) + { + java_is_64bit = result.is_64bit; + prepareForLaunch(); + } + else + { emitFailed("The java binary doesn't work. Check the settings and correct the problem"); } } @@ -103,8 +132,7 @@ void OneSixUpdate::versionFileStart() QLOG_INFO() << m_inst->name() << ": getting version file."; setStatus("Getting the version files from Mojang."); - QString urlstr("http://s3.amazonaws.com/Minecraft.Download/versions/"); - urlstr += targetVersion->descriptor() + "/" + targetVersion->descriptor() + ".json"; + QString urlstr = "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + targetVersion->descriptor() + "/" + targetVersion->descriptor() + ".json"; auto job = new NetJob("Version index"); job->addNetAction(ByteArrayDownload::make(QUrl(urlstr))); specificVersionDownloadJob.reset(job); @@ -160,7 +188,7 @@ void OneSixUpdate::versionFileFinished() } inst->reloadFullVersion(); - checkJava(); + checkJavaOnline(); } void OneSixUpdate::versionFileFailed() @@ -168,6 +196,90 @@ void OneSixUpdate::versionFileFailed() emitFailed("Failed to download the version description. Try again."); } +void OneSixUpdate::assetIndexStart() +{ + setStatus("Updating asset index."); + OneSixInstance *inst = (OneSixInstance *)m_inst; + std::shared_ptr<OneSixVersion> version = inst->getFullVersion(); + QString assetName = version->assets; + QUrl indexUrl = "http://" + URLConstants::AWS_DOWNLOAD_INDEXES + assetName + ".json"; + QString localPath = assetName + ".json"; + auto job = new NetJob("Asset index for " + inst->name()); + + auto metacache = MMC->metacache(); + auto entry = metacache->resolveEntry("asset_indexes", localPath); + job->addNetAction(CacheDownload::make(indexUrl, entry)); + jarlibDownloadJob.reset(job); + + connect(jarlibDownloadJob.get(), SIGNAL(succeeded()), SLOT(assetIndexFinished())); + connect(jarlibDownloadJob.get(), SIGNAL(failed()), SLOT(assetIndexFailed())); + connect(jarlibDownloadJob.get(), SIGNAL(progress(qint64, qint64)), + SIGNAL(progress(qint64, qint64))); + + jarlibDownloadJob->start(); +} + +void OneSixUpdate::assetIndexFinished() +{ + AssetsIndex index; + + OneSixInstance *inst = (OneSixInstance *)m_inst; + std::shared_ptr<OneSixVersion> version = inst->getFullVersion(); + QString assetName = version->assets; + + QString asset_fname = "assets/indexes/" + assetName + ".json"; + if (!AssetsUtils::loadAssetsIndexJson(asset_fname, &index)) + { + emitFailed("Failed to read the assets index!"); + } + + QList<Md5EtagDownloadPtr> dls; + for (auto object : index.objects.values()) + { + QString objectName = object.hash.left(2) + "/" + object.hash; + QFileInfo objectFile("assets/objects/" + objectName); + if ((!objectFile.isFile()) || (objectFile.size() != object.size)) + { + auto objectDL = MD5EtagDownload::make( + QUrl("http://" + URLConstants::RESOURCE_BASE + objectName), + objectFile.filePath()); + dls.append(objectDL); + } + } + if(dls.size()) + { + setStatus("Getting the assets files from Mojang..."); + auto job = new NetJob("Assets for " + inst->name()); + for(auto dl: dls) + job->addNetAction(dl); + jarlibDownloadJob.reset(job); + connect(jarlibDownloadJob.get(), SIGNAL(succeeded()), SLOT(assetsFinished())); + connect(jarlibDownloadJob.get(), SIGNAL(failed()), SLOT(assetsFailed())); + connect(jarlibDownloadJob.get(), SIGNAL(progress(qint64, qint64)), + SIGNAL(progress(qint64, qint64))); + jarlibDownloadJob->start(); + return; + } + assetsFinished(); +} + +void OneSixUpdate::assetIndexFailed() +{ + emitFailed("Failed to download the assets index!"); +} + +void OneSixUpdate::assetsFinished() +{ + prepareForLaunch(); +} + +void OneSixUpdate::assetsFailed() +{ + emitFailed("Failed to download assets!"); +} + + + void OneSixUpdate::jarlibStart() { setStatus("Getting the library files from Mojang."); @@ -181,17 +293,22 @@ void OneSixUpdate::jarlibStart() return; } + // Build a list of URLs that will need to be downloaded. std::shared_ptr<OneSixVersion> version = inst->getFullVersion(); + // minecraft.jar for this version + { + QString version_id = version->id; + QString localPath = version_id + "/" + version_id + ".jar"; + QString urlstr = "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + localPath; - // download the right jar, save it in versions/$version/$version.jar - QString urlstr("http://s3.amazonaws.com/Minecraft.Download/versions/"); - urlstr += version->id + "/" + version->id + ".jar"; - QString targetstr("versions/"); - targetstr += version->id + "/" + version->id + ".jar"; + auto job = new NetJob("Libraries for instance " + inst->name()); - auto job = new NetJob("Libraries for instance " + inst->name()); - job->addNetAction(FileDownload::make(QUrl(urlstr), targetstr)); - jarlibDownloadJob.reset(job); + auto metacache = MMC->metacache(); + auto entry = metacache->resolveEntry("versions", localPath); + job->addNetAction(CacheDownload::make(QUrl(urlstr), entry)); + + jarlibDownloadJob.reset(job); + } auto libs = version->getActiveNativeLibs(); libs.append(version->getActiveNormalLibs()); @@ -240,10 +357,7 @@ void OneSixUpdate::jarlibStart() void OneSixUpdate::jarlibFinished() { - if (m_prepare_for_launch) - prepareForLaunch(); - else - emitSucceeded(); + assetIndexStart(); } void OneSixUpdate::jarlibFailed() diff --git a/logic/OneSixUpdate.h b/logic/OneSixUpdate.h index b86c205f..00b769c7 100644 --- a/logic/OneSixUpdate.h +++ b/logic/OneSixUpdate.h @@ -43,11 +43,20 @@ slots: void jarlibFinished(); void jarlibFailed(); - void checkJava(); - void checkFinished(JavaCheckResult result); + void assetIndexStart(); + void assetIndexFinished(); + void assetIndexFailed(); + + void assetsFinished(); + void assetsFailed(); + + void checkJavaOnline(); + void checkFinishedOnline(JavaCheckResult result); + void checkFinishedOffline(JavaCheckResult result); // extract the appropriate libraries void prepareForLaunch(); + private: NetJobPtr specificVersionDownloadJob; NetJobPtr jarlibDownloadJob; @@ -55,7 +64,7 @@ private: // target version, determined during this task std::shared_ptr<MinecraftVersion> targetVersion; BaseInstance *m_inst = nullptr; - bool m_prepare_for_launch = false; + bool m_only_prepare = false; std::shared_ptr<JavaChecker> checker; bool java_is_64bit = false; diff --git a/logic/OneSixVersion.cpp b/logic/OneSixVersion.cpp index cc5b1de1..8ae685f0 100644 --- a/logic/OneSixVersion.cpp +++ b/logic/OneSixVersion.cpp @@ -17,6 +17,8 @@ #include "logic/OneSixLibrary.h" #include "logic/OneSixRule.h" +#include "logger/QsLog.h" + std::shared_ptr<OneSixVersion> fromJsonV4(QJsonObject root, std::shared_ptr<OneSixVersion> fullVersion) { @@ -60,6 +62,18 @@ std::shared_ptr<OneSixVersion> fromJsonV4(QJsonObject root, fullVersion->releaseTime = root.value("releaseTime").toString(); fullVersion->time = root.value("time").toString(); + auto assetsID = root.value("assets"); + if (assetsID.isString()) + { + fullVersion->assets = assetsID.toString(); + } + else + { + fullVersion->assets = "legacy"; + } + + QLOG_DEBUG() << "Assets version:" << fullVersion->assets; + // Iterate through the list, if it's a list. auto librariesValue = root.value("libraries"); if (!librariesValue.isArray()) @@ -151,7 +165,7 @@ std::shared_ptr<OneSixVersion> OneSixVersion::fromJson(QJsonObject root) root.value("minimumLauncherVersion").toDouble(); // ADD MORE HERE :D - if (launcher_ver > 0 && launcher_ver <= 11) + if (launcher_ver > 0 && launcher_ver <= 13) return fromJsonV4(root, readVersion); else { diff --git a/logic/OneSixVersion.h b/logic/OneSixVersion.h index 5718dafe..036f3d53 100644 --- a/logic/OneSixVersion.h +++ b/logic/OneSixVersion.h @@ -56,6 +56,8 @@ public: QString releaseTime; /// Release type - "release" or "snapshot" QString type; + /// Assets type - "legacy" or a version ID + QString assets; /** * DEPRECATED: Old versions of the new vanilla launcher used this * ex: "username_session_version" diff --git a/logic/assets/AssetsUtils.cpp b/logic/assets/AssetsUtils.cpp new file mode 100644 index 00000000..11d928cf --- /dev/null +++ b/logic/assets/AssetsUtils.cpp @@ -0,0 +1,227 @@ +/* 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 <QDir> +#include <QDirIterator> +#include <QCryptographicHash> +#include <QJsonParseError> +#include <QJsonDocument> +#include <QJsonObject> + +#include "AssetsUtils.h" +#include "MultiMC.h" + +namespace AssetsUtils +{ +void migrateOldAssets() +{ + QDir assets_dir("assets"); + if (!assets_dir.exists()) + return; + assets_dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); + int base_length = assets_dir.path().length(); + + QList<QString> blacklist = {"indexes", "objects", "virtual"}; + + if (!assets_dir.exists("objects")) + assets_dir.mkdir("objects"); + QDir objects_dir("assets/objects"); + + QDirIterator iterator(assets_dir, QDirIterator::Subdirectories); + int successes = 0; + int failures = 0; + while (iterator.hasNext()) + { + QString currentDir = iterator.next(); + currentDir = currentDir.remove(0, base_length + 1); + + bool ignore = false; + for (QString blacklisted : blacklist) + { + if (currentDir.startsWith(blacklisted)) + ignore = true; + } + + if (!iterator.fileInfo().isDir() && !ignore) + { + QString filename = iterator.filePath(); + + QFile input(filename); + input.open(QIODevice::ReadOnly | QIODevice::WriteOnly); + QString sha1sum = + QCryptographicHash::hash(input.readAll(), QCryptographicHash::Sha1) + .toHex() + .constData(); + + QString object_name = filename.remove(0, base_length + 1); + QLOG_DEBUG() << "Processing" << object_name << ":" << sha1sum << input.size(); + + QString object_tlk = sha1sum.left(2); + QString object_tlk_dir = objects_dir.path() + "/" + object_tlk; + + QDir tlk_dir(object_tlk_dir); + if (!tlk_dir.exists()) + objects_dir.mkdir(object_tlk); + + QString new_filename = tlk_dir.path() + "/" + sha1sum; + QFile new_object(new_filename); + if (!new_object.exists()) + { + bool rename_success = input.rename(new_filename); + QLOG_DEBUG() << " Doesn't exist, copying to" << new_filename << ":" + << QString::number(rename_success); + if (rename_success) + successes++; + else + failures++; + } + else + { + input.remove(); + QLOG_DEBUG() << " Already exists, deleting original and not copying."; + } + } + } + + if (successes + failures == 0) + { + QLOG_DEBUG() << "No legacy assets needed importing."; + } + else + { + QLOG_DEBUG() << "Finished copying legacy assets:" << successes << "successes and" + << failures << "failures."; + + QDirIterator cleanup_iterator(assets_dir); + + while (cleanup_iterator.hasNext()) + { + QString currentDir = cleanup_iterator.next(); + currentDir = currentDir.remove(0, base_length + 1); + + bool ignore = false; + for (QString blacklisted : blacklist) + { + if (currentDir.startsWith(blacklisted)) + ignore = true; + } + + if (cleanup_iterator.fileInfo().isDir() && !ignore) + { + QString path = cleanup_iterator.filePath(); + QDir folder(path); + + QLOG_DEBUG() << "Cleaning up legacy assets folder:" << path; + + folder.removeRecursively(); + } + } + } +} + +/* + * Returns true on success, with index populated + * index is undefined otherwise + */ +bool loadAssetsIndexJson(QString path, AssetsIndex *index) +{ + /* + { + "objects": { + "icons/icon_16x16.png": { + "hash": "bdf48ef6b5d0d23bbb02e17d04865216179f510a", + "size": 3665 + }, + ... + } + } + } + */ + + QFile file(path); + + // Try to open the file and fail if we can't. + // TODO: We should probably report this error to the user. + if (!file.open(QIODevice::ReadOnly)) + { + QLOG_ERROR() << "Failed to read assets index file" << path; + return false; + } + + // Read the file and close it. + QByteArray jsonData = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); + + // Fail if the JSON is invalid. + if (parseError.error != QJsonParseError::NoError) + { + QLOG_ERROR() << "Failed to parse assets index file:" << parseError.errorString() + << "at offset " << QString::number(parseError.offset); + return false; + } + + // Make sure the root is an object. + if (!jsonDoc.isObject()) + { + QLOG_ERROR() << "Invalid assets index JSON: Root should be an array."; + return false; + } + + QJsonObject root = jsonDoc.object(); + + QJsonValue isVirtual = root.value("virtual"); + if (!isVirtual.isUndefined()) + { + index->isVirtual = isVirtual.toBool(false); + } + + QJsonValue objects = root.value("objects"); + QVariantMap map = objects.toVariant().toMap(); + + for (QVariantMap::const_iterator iter = map.begin(); iter != map.end(); ++iter) + { + // QLOG_DEBUG() << iter.key(); + + QVariant variant = iter.value(); + QVariantMap nested_objects = variant.toMap(); + + AssetObject object; + + for (QVariantMap::const_iterator nested_iter = nested_objects.begin(); + nested_iter != nested_objects.end(); ++nested_iter) + { + // QLOG_DEBUG() << nested_iter.key() << nested_iter.value().toString(); + QString key = nested_iter.key(); + QVariant value = nested_iter.value(); + + if (key == "hash") + { + object.hash = value.toString(); + } + else if (key == "size") + { + object.size = value.toDouble(); + } + } + + index->objects.insert(iter.key(), object); + } + + return true; +} +} diff --git a/logic/OneSixAssets.h b/logic/assets/AssetsUtils.h index 948068b8..5276d5a5 100644 --- a/logic/OneSixAssets.h +++ b/logic/assets/AssetsUtils.h @@ -14,32 +14,26 @@ */ #pragma once -#include "net/NetJob.h" -class Private; -class ThreadedDeleter; +#include <QString> +#include <QMap> -class OneSixAssets : public QObject -{ - Q_OBJECT -signals: - void failed(); - void finished(); - void indexStarted(); - void filesStarted(); - void filesProgress(int, int, int); - -public -slots: - void S3BucketFinished(); - void downloadFinished(); +class AssetObject; -public: - void start(); +struct AssetObject +{ + QString hash; + qint64 size; +}; -private: - ThreadedDeleter *deleter; - QStringList nuke_whitelist; - NetJobPtr index_job; - NetJobPtr files_job; +struct AssetsIndex +{ + QMap<QString, AssetObject> objects; + bool isVirtual = false; }; + +namespace AssetsUtils +{ +void migrateOldAssets(); +bool loadAssetsIndexJson(QString file, AssetsIndex* index); +} diff --git a/logic/auth/MojangAccount.cpp b/logic/auth/MojangAccount.cpp index 4a61cf19..bc6af98f 100644 --- a/logic/auth/MojangAccount.cpp +++ b/logic/auth/MojangAccount.cpp @@ -16,113 +16,17 @@ */ #include "MojangAccount.h" +#include "flows/RefreshTask.h" +#include "flows/AuthenticateTask.h" #include <QUuid> #include <QJsonObject> #include <QJsonArray> +#include <QRegExp> +#include <QStringList> #include <logger/QsLog.h> -MojangAccount::MojangAccount(const QString &username, QObject *parent) : QObject(parent) -{ - // Generate a client token. - m_clientToken = QUuid::createUuid().toString(); - - m_username = username; - - m_currentProfile = -1; -} - -MojangAccount::MojangAccount(const QString &username, const QString &clientToken, - const QString &accessToken, QObject *parent) - : QObject(parent) -{ - m_username = username; - m_clientToken = clientToken; - m_accessToken = accessToken; - - m_currentProfile = -1; -} - -MojangAccount::MojangAccount(const MojangAccount &other, QObject *parent) -{ - m_username = other.username(); - m_clientToken = other.clientToken(); - m_accessToken = other.accessToken(); - - m_profiles = other.m_profiles; - m_currentProfile = other.m_currentProfile; -} - -QString MojangAccount::username() const -{ - return m_username; -} - -QString MojangAccount::clientToken() const -{ - return m_clientToken; -} - -void MojangAccount::setClientToken(const QString &clientToken) -{ - m_clientToken = clientToken; -} - -QString MojangAccount::accessToken() const -{ - return m_accessToken; -} - -void MojangAccount::setAccessToken(const QString &accessToken) -{ - m_accessToken = accessToken; -} - -QString MojangAccount::sessionId() const -{ - return "token:" + m_accessToken + ":" + currentProfile()->id(); -} - -const QList<AccountProfile> MojangAccount::profiles() const -{ - return m_profiles; -} - -const AccountProfile *MojangAccount::currentProfile() const -{ - if (m_currentProfile < 0) - { - if (m_profiles.length() > 0) - return &m_profiles.at(0); - else - return nullptr; - } - else - return &m_profiles.at(m_currentProfile); -} - -bool MojangAccount::setProfile(const QString &profileId) -{ - const QList<AccountProfile> &profiles = this->profiles(); - for (int i = 0; i < profiles.length(); i++) - { - if (profiles.at(i).id() == profileId) - { - m_currentProfile = i; - return true; - } - } - return false; -} - -void MojangAccount::loadProfiles(const ProfileList &profiles) -{ - m_profiles.clear(); - for (auto profile : profiles) - m_profiles.append(profile); -} - MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object) { // The JSON object must at least have a username for it to be valid. @@ -143,78 +47,160 @@ MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object) return nullptr; } - ProfileList profiles; + QList<AccountProfile> profiles; for (QJsonValue profileVal : profileArray) { QJsonObject profileObject = profileVal.toObject(); QString id = profileObject.value("id").toString(""); QString name = profileObject.value("name").toString(""); + bool legacy = profileObject.value("legacy").toBool(false); if (id.isEmpty() || name.isEmpty()) { QLOG_WARN() << "Unable to load a profile because it was missing an ID or a name."; continue; } - profiles.append(AccountProfile(id, name)); + profiles.append({id, name, legacy}); } - MojangAccountPtr account(new MojangAccount(username, clientToken, accessToken)); - account->loadProfiles(profiles); + MojangAccountPtr account(new MojangAccount()); + if(object.value("user").isObject()) + { + User u; + QJsonObject userStructure = object.value("user").toObject(); + u.id = userStructure.value("id").toString(); + /* + QJsonObject propMap = userStructure.value("properties").toObject(); + for(auto key: propMap.keys()) + { + auto values = propMap.operator[](key).toArray(); + for(auto value: values) + u.properties.insert(key, value.toString()); + } + */ + account->m_user = u; + } + account->m_username = username; + account->m_clientToken = clientToken; + account->m_accessToken = accessToken; + account->m_profiles = profiles; // Get the currently selected profile. QString currentProfile = object.value("activeProfile").toString(""); if (!currentProfile.isEmpty()) - account->setProfile(currentProfile); + account->setCurrentProfile(currentProfile); + + return account; +} +MojangAccountPtr MojangAccount::createFromUsername(const QString& username) +{ + MojangAccountPtr account(new MojangAccount()); + account->m_clientToken = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + account->m_username = username; return account; } -QJsonObject MojangAccount::saveToJson() +QJsonObject MojangAccount::saveToJson() const { QJsonObject json; - json.insert("username", username()); - json.insert("clientToken", clientToken()); - json.insert("accessToken", accessToken()); + json.insert("username", m_username); + json.insert("clientToken", m_clientToken); + json.insert("accessToken", m_accessToken); QJsonArray profileArray; for (AccountProfile profile : m_profiles) { QJsonObject profileObj; - profileObj.insert("id", profile.id()); - profileObj.insert("name", profile.name()); + profileObj.insert("id", profile.id); + profileObj.insert("name", profile.name); + profileObj.insert("legacy", profile.legacy); profileArray.append(profileObj); } json.insert("profiles", profileArray); - if (currentProfile() != nullptr) - json.insert("activeProfile", currentProfile()->id()); + QJsonObject userStructure; + { + userStructure.insert("id", m_user.id); + /* + QJsonObject userAttrs; + for(auto key: m_user.properties.keys()) + { + auto array = QJsonArray::fromStringList(m_user.properties.values(key)); + userAttrs.insert(key, array); + } + userStructure.insert("properties", userAttrs); + */ + } + json.insert("user", userStructure); + + if (m_currentProfile != -1) + json.insert("activeProfile", currentProfile()->id); return json; } - -AccountProfile::AccountProfile(const QString& id, const QString& name) +bool MojangAccount::setCurrentProfile(const QString &profileId) { - m_id = id; - m_name = name; + for (int i = 0; i < m_profiles.length(); i++) + { + if (m_profiles[i].id == profileId) + { + m_currentProfile = i; + return true; + } + } + return false; } -AccountProfile::AccountProfile(const AccountProfile &other) +const AccountProfile* MojangAccount::currentProfile() const { - m_id = other.m_id; - m_name = other.m_name; + if(m_currentProfile == -1) + return nullptr; + return &m_profiles[m_currentProfile]; } -QString AccountProfile::id() const +AccountStatus MojangAccount::accountStatus() const { - return m_id; + if(m_accessToken.isEmpty()) + return NotVerified; + if(!m_online) + return Verified; + return Online; } -QString AccountProfile::name() const +std::shared_ptr<Task> MojangAccount::login(QString password) { - return m_name; + if(m_currentTask) + return m_currentTask; + if(password.isEmpty()) + { + m_currentTask.reset(new RefreshTask(this)); + } + else + { + m_currentTask.reset(new AuthenticateTask(this, password)); + } + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + return m_currentTask; } -void MojangAccount::propagateChange() +void MojangAccount::authSucceeded() { + m_online = true; + m_currentTask.reset(); emit changed(); } + +void MojangAccount::authFailed(QString reason) +{ + // This is emitted when the yggdrasil tasks time out or are cancelled. + // -> we treat the error as no-op + if(reason != "Yggdrasil task cancelled.") + { + m_online = false; + m_accessToken = QString(); + emit changed(); + } + m_currentTask.reset(); +} diff --git a/logic/auth/MojangAccount.h b/logic/auth/MojangAccount.h index 25a85790..325aa826 100644 --- a/logic/auth/MojangAccount.h +++ b/logic/auth/MojangAccount.h @@ -20,43 +20,42 @@ #include <QList> #include <QJsonObject> #include <QPair> +#include <QMap> #include <memory> +class Task; +class YggdrasilTask; class MojangAccount; typedef std::shared_ptr<MojangAccount> MojangAccountPtr; Q_DECLARE_METATYPE(MojangAccountPtr) /** - * Class that represents a profile within someone's Mojang account. + * A profile within someone's Mojang account. * * Currently, the profile system has not been implemented by Mojang yet, * but we might as well add some things for it in MultiMC right now so * we don't have to rip the code to pieces to add it later. */ -class AccountProfile +struct AccountProfile { -public: - AccountProfile(const QString &id, const QString &name); - AccountProfile(const AccountProfile &other); - - QString id() const; - QString name() const; - -protected: - QString m_id; - QString m_name; + QString id; + QString name; + bool legacy; }; -typedef QList<AccountProfile> ProfileList; - struct User { QString id; - // pair of key:value - // we don't know if the keys:value mapping is 1:1, so a list is used. - QList<QPair<QString, QString>> properties; + QMultiMap<QString,QString> properties; +}; + +enum AccountStatus +{ + NotVerified, + Verified, + Online }; /** @@ -68,106 +67,121 @@ struct User class MojangAccount : public QObject { Q_OBJECT -public: - /** - * Constructs a new MojangAccount with the given username. - * The client token will be generated automatically and the access token will be blank. - */ - explicit MojangAccount(const QString &username, QObject *parent = 0); +public: /* construction */ + //! Do not copy accounts. ever. + explicit MojangAccount(const MojangAccount &other, QObject *parent) = delete; - /** - * Constructs a new MojangAccount with the given username, client token, and access token. - */ - explicit MojangAccount(const QString &username, const QString &clientToken, - const QString &accessToken, QObject *parent = 0); + //! Default constructor + explicit MojangAccount(QObject *parent = 0) : QObject(parent) {}; - /** - * Constructs a new MojangAccount matching the given account. - */ - MojangAccount(const MojangAccount &other, QObject *parent); + //! Creates an empty account for the specified user name. + static MojangAccountPtr createFromUsername(const QString &username); - /** - * Loads a MojangAccount from the given JSON object. - */ + //! Loads a MojangAccount from the given JSON object. static MojangAccountPtr loadFromJson(const QJsonObject &json); - /** - * Saves a MojangAccount to a JSON object and returns it. - */ - QJsonObject saveToJson(); + //! Saves a MojangAccount to a JSON object and returns it. + QJsonObject saveToJson() const; +public: /* manipulation */ /** - * Update the account on disk and lists (it changed, for whatever reason) - * This is called by various Yggdrasil tasks. - */ - void propagateChange(); + * Sets the currently selected profile to the profile with the given ID string. + * If profileId is not in the list of available profiles, the function will simply return + * false. + */ + bool setCurrentProfile(const QString &profileId); + + /** + * Attempt to login. Empty password means we use the token. + * If the attempt fails because we already are performing some task, it returns false. + */ + std::shared_ptr<Task> login(QString password = QString()); + + void downgrade() + { + m_online = false; + } +public: /* queries */ + const QString &username() const + { + return m_username; + } + + const QString &clientToken() const + { + return m_clientToken; + } + + const QString &accessToken() const + { + return m_accessToken; + } + + const QList<AccountProfile> &profiles() const + { + return m_profiles; + } + + const User & user() + { + return m_user; + } + + //! Get the session ID required for legacy Minecraft versions + QString sessionId() const + { + if (m_currentProfile != -1 && !m_accessToken.isEmpty()) + return "token:" + m_accessToken + ":" + m_profiles[m_currentProfile].id; + return "-"; + } + + //! Returns the currently selected profile (if none, returns nullptr) + const AccountProfile *currentProfile() const; - /** - * This MojangAccount's username. May be an email address if the account is migrated. - */ - QString username() const; + //! Returns whether the account is NotVerified, Verified or Online + AccountStatus accountStatus() const; +signals: /** - * This MojangAccount's client token. This is a UUID used by Mojang's auth servers to identify this client. - * This is unique for each MojangAccount. + * This signal is emitted when the account changes */ - QString clientToken() const; + void changed(); - /** - * Sets the MojangAccount's client token to the given value. - */ - void setClientToken(const QString &token); + // TODO: better signalling for the various possible state changes - especially errors - /** - * This MojangAccount's access token. - * If the user has not chosen to stay logged in, this will be an empty string. - */ - QString accessToken() const; +protected: /* variables */ + QString m_username; - /** - * Changes this MojangAccount's access token to the given value. - */ - void setAccessToken(const QString &token); + // Used to identify the client - the user can have multiple clients for the same account + // Think: different launchers, all connecting to the same account/profile + QString m_clientToken; - /** - * Get full session ID - */ - QString sessionId() const; + // Blank if not logged in. + QString m_accessToken; - /** - * Returns a list of the available account profiles. - */ - const ProfileList profiles() const; + // Index of the selected profile within the list of available + // profiles. -1 if nothing is selected. + int m_currentProfile = -1; - /** - * Returns a pointer to the currently selected profile. - * If no profile is selected, returns the first profile in the profile list or nullptr if there are none. - */ - const AccountProfile *currentProfile() const; + // List of available profiles. + QList<AccountProfile> m_profiles; - /** - * Sets the currently selected profile to the profile with the given ID string. - * If profileId is not in the list of available profiles, the function will simply return false. - */ - bool setProfile(const QString &profileId); + // the user structure, whatever it is. + User m_user; - /** - * Clears the current account profile list and replaces it with the given profile list. - */ - void loadProfiles(const ProfileList &profiles); + // true when the account is verified + bool m_online = false; -signals: - /** - * This isgnal is emitted whrn the account changes - */ - void changed(); + // current task we are executing here + std::shared_ptr<YggdrasilTask> m_currentTask; -protected: - QString m_username; - QString m_clientToken; - QString m_accessToken; // Blank if not logged in. - int m_currentProfile; // Index of the selected profile within the list of available - // profiles. -1 if nothing is selected. - ProfileList m_profiles; // List of available profiles. - User m_user; // the user structure, whatever it is. +private slots: + void authSucceeded(); + void authFailed(QString reason); + +public: + friend class YggdrasilTask; + friend class AuthenticateTask; + friend class ValidateTask; + friend class RefreshTask; }; diff --git a/logic/lists/MojangAccountList.cpp b/logic/auth/MojangAccountList.cpp index 439b5da6..33990662 100644 --- a/logic/lists/MojangAccountList.cpp +++ b/logic/auth/MojangAccountList.cpp @@ -13,7 +13,7 @@ * limitations under the License. */ -#include "logic/lists/MojangAccountList.h" +#include "logic/auth/MojangAccountList.h" #include <QIODevice> #include <QFile> @@ -28,7 +28,7 @@ #include "logic/auth/MojangAccount.h" -#define ACCOUNT_LIST_FORMAT_VERSION 1 +#define ACCOUNT_LIST_FORMAT_VERSION 2 MojangAccountList::MojangAccountList(QObject *parent) : QAbstractListModel(parent) { @@ -84,10 +84,7 @@ void MojangAccountList::removeAccount(QModelIndex index) MojangAccountPtr MojangAccountList::activeAccount() const { - if (m_activeAccount.isEmpty()) - return nullptr; - else - return findAccount(m_activeAccount); + return m_activeAccount; } void MojangAccountList::setActiveAccount(const QString &username) @@ -95,14 +92,14 @@ void MojangAccountList::setActiveAccount(const QString &username) beginResetModel(); if (username.isEmpty()) { - m_activeAccount = ""; + m_activeAccount = nullptr; } else { for (MojangAccountPtr account : m_accounts) { if (account->username() == username) - m_activeAccount = username; + m_activeAccount = account; } } endResetModel(); @@ -152,9 +149,6 @@ QVariant MojangAccountList::data(const QModelIndex &index, int role) const case Qt::DisplayRole: switch (index.column()) { - case ActiveColumn: - return account->username() == m_activeAccount; - case NameColumn: return account->username(); @@ -168,6 +162,13 @@ QVariant MojangAccountList::data(const QModelIndex &index, int role) const case PointerRole: return qVariantFromValue(account); + case Qt::CheckStateRole: + switch (index.column()) + { + case ActiveColumn: + return account == m_activeAccount; + } + default: return QVariant(); } @@ -216,6 +217,36 @@ int MojangAccountList::columnCount(const QModelIndex &parent) const return 2; } +Qt::ItemFlags MojangAccountList::flags(const QModelIndex &index) const +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + { + return Qt::NoItemFlags; + } + + return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable; +} + +bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + { + return false; + } + + if(role == Qt::CheckStateRole) + { + if(value == Qt::Checked) + { + MojangAccountPtr account = this->at(index.row()); + this->setActiveAccount(account->username()); + } + } + + emit dataChanged(index, index); + return true; +} + void MojangAccountList::updateListData(QList<MojangAccountPtr> versions) { beginResetModel(); @@ -303,11 +334,9 @@ bool MojangAccountList::loadList(const QString &filePath) QLOG_WARN() << "Failed to load an account."; } } - endResetModel(); - // Load the active account. - m_activeAccount = root.value("activeAccount").toString(""); - + m_activeAccount = findAccount(root.value("activeAccount").toString("")); + endResetModel(); return true; } @@ -347,8 +376,11 @@ bool MojangAccountList::saveList(const QString &filePath) // Insert the account list into the root object. root.insert("accounts", accounts); - // Save the active account. - root.insert("activeAccount", m_activeAccount); + if(m_activeAccount) + { + // Save the active account. + root.insert("activeAccount", m_activeAccount->username()); + } // Create a JSON document object to convert our JSON to bytes. QJsonDocument doc(root); diff --git a/logic/lists/MojangAccountList.h b/logic/auth/MojangAccountList.h index 744f3c51..c7e30958 100644 --- a/logic/lists/MojangAccountList.h +++ b/logic/auth/MojangAccountList.h @@ -64,6 +64,8 @@ public: virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const; virtual int rowCount(const QModelIndex &parent) const; virtual int columnCount(const QModelIndex &parent) const; + virtual Qt::ItemFlags flags(const QModelIndex &index) const; + virtual bool setData(const QModelIndex &index, const QVariant &value, int role); /*! * Adds a the given Mojang account to the account list. @@ -161,10 +163,9 @@ protected: QList<MojangAccountPtr> m_accounts; /*! - * Username of the account that is currently active. - * Empty string if no account is active. + * Account that is currently active. */ - QString m_activeAccount; + MojangAccountPtr m_activeAccount; //! Path to the account list file. Empty string if there isn't one. QString m_listFilePath; diff --git a/logic/auth/YggdrasilTask.cpp b/logic/auth/YggdrasilTask.cpp index 797e84cd..088e1fc0 100644 --- a/logic/auth/YggdrasilTask.cpp +++ b/logic/auth/YggdrasilTask.cpp @@ -24,17 +24,11 @@ #include <MultiMC.h> #include <logic/auth/MojangAccount.h> +#include <logic/net/URLConstants.h> -YggdrasilTask::YggdrasilTask(MojangAccountPtr account, QObject *parent) : Task(parent) +YggdrasilTask::YggdrasilTask(MojangAccount *account, QObject *parent) + : Task(parent), m_account(account) { - m_error = nullptr; - m_account = account; -} - -YggdrasilTask::~YggdrasilTask() -{ - if (m_error) - delete m_error; } void YggdrasilTask::executeTask() @@ -45,97 +39,126 @@ void YggdrasilTask::executeTask() QJsonDocument doc(getRequestContent()); auto worker = MMC->qnam(); - connect(worker.get(), SIGNAL(finished(QNetworkReply *)), this, - SLOT(processReply(QNetworkReply *))); - - QUrl reqUrl("https://authserver.mojang.com/" + getEndpoint()); + QUrl reqUrl("https://" + URLConstants::AUTH_BASE + getEndpoint()); QNetworkRequest netRequest(reqUrl); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QByteArray requestData = doc.toJson(); m_netReply = worker->post(netRequest, requestData); + connect(m_netReply, &QNetworkReply::finished, this, &YggdrasilTask::processReply); + connect(m_netReply, &QNetworkReply::uploadProgress, this, &YggdrasilTask::refreshTimers); + connect(m_netReply, &QNetworkReply::downloadProgress, this, &YggdrasilTask::refreshTimers); + timeout_keeper.setSingleShot(true); + timeout_keeper.start(timeout_max); + counter.setSingleShot(false); + counter.start(time_step); + progress(0, timeout_max); + connect(&timeout_keeper, &QTimer::timeout, this, &YggdrasilTask::abort); + connect(&counter, &QTimer::timeout, this, &YggdrasilTask::heartbeat); } -void YggdrasilTask::processReply(QNetworkReply *reply) +void YggdrasilTask::refreshTimers(qint64, qint64) { - setStatus(getStateMessage(STATE_PROCESSING_RESPONSE)); + timeout_keeper.stop(); + timeout_keeper.start(timeout_max); + progress(count = 0, timeout_max); +} +void YggdrasilTask::heartbeat() +{ + count += time_step; + progress(count, timeout_max); +} - if (m_netReply != reply) - // Wrong reply for some reason... - return; +void YggdrasilTask::abort() +{ + progress(timeout_max, timeout_max); + m_netReply->abort(); +} - if (reply->error() == QNetworkReply::OperationCanceledError) +void YggdrasilTask::processReply() +{ + setStatus(getStateMessage(STATE_PROCESSING_RESPONSE)); + + // any network errors lead to offline mode right now + if (m_netReply->error() >= QNetworkReply::ConnectionRefusedError && + m_netReply->error() <= QNetworkReply::UnknownNetworkError) { + // WARNING/FIXME: the value here is used in MojangAccount to detect the cancel/timeout emitFailed("Yggdrasil task cancelled."); return; } - else + + // Try to parse the response regardless of the response code. + // Sometimes the auth server will give more information and an error code. + QJsonParseError jsonError; + QByteArray replyData = m_netReply->readAll(); + QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError); + // Check the response code. + int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (responseCode == 200) { - // Try to parse the response regardless of the response code. - // Sometimes the auth server will give more information and an error code. - QJsonParseError jsonError; - QByteArray replyData = reply->readAll(); - QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError); - // Check the response code. - int responseCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (responseCode == 200) + // If the response code was 200, then there shouldn't be an error. Make sure + // anyways. + // Also, sometimes an empty reply indicates success. If there was no data received, + // pass an empty json object to the processResponse function. + if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0) { - // If the response code was 200, then there shouldn't be an error. Make sure anyways. - // Also, sometimes an empty reply indicates success. If there was no data received, - // pass an empty json object to the processResponse function. - if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0) - { - if (!processResponse(replyData.size() > 0 ? doc.object() : QJsonObject())) - { - YggdrasilTask::Error *err = getError(); - if (err) - emitFailed(err->getErrorMessage()); - else - emitFailed(tr("An unknown error occurred when processing the response " - "from the authentication server.")); - } - else - { - emitSucceeded(); - } - } - else + if (processResponse(replyData.size() > 0 ? doc.object() : QJsonObject())) { - emitFailed(tr("Failed to parse Yggdrasil JSON response: %1 at offset %2.").arg(jsonError.errorString()).arg(jsonError.offset)); + emitSucceeded(); + return; } + + // errors happened anyway? + emitFailed(m_error ? m_error->m_errorMessageVerbose + : tr("An unknown error occurred when processing the response " + "from the authentication server.")); } else { - // If the response code was not 200, then Yggdrasil may have given us information about the error. - // If we can parse the response, then get information from it. Otherwise just say there was an unknown error. - if (jsonError.error == QJsonParseError::NoError) - { - // We were able to parse the server's response. Woo! - // Call processError. If a subclass has overridden it then they'll handle their stuff there. - QLOG_DEBUG() << "The request failed, but the server gave us an error message. Processing error."; - emitFailed(processError(doc.object())); - } - else - { - // The server didn't say anything regarding the error. Give the user an unknown error. - QLOG_DEBUG() << "The request failed and the server gave no error message. Unknown error."; - emitFailed(tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(reply->errorString())); - } + emitFailed(tr("Failed to parse Yggdrasil JSON response: %1 at offset %2.") + .arg(jsonError.errorString()) + .arg(jsonError.offset)); } + return; + } + + // If the response code was not 200, then Yggdrasil may have given us information + // about the error. + // If we can parse the response, then get information from it. Otherwise just say + // there was an unknown error. + if (jsonError.error == QJsonParseError::NoError) + { + // We were able to parse the server's response. Woo! + // Call processError. If a subclass has overridden it then they'll handle their + // stuff there. + QLOG_DEBUG() << "The request failed, but the server gave us an error message. " + "Processing error."; + emitFailed(processError(doc.object())); + } + else + { + // The server didn't say anything regarding the error. Give the user an unknown + // error. + QLOG_DEBUG() << "The request failed and the server gave no error message. " + "Unknown error."; + emitFailed(tr("An unknown error occurred when trying to communicate with the " + "authentication server: %1").arg(m_netReply->errorString())); } } QString YggdrasilTask::processError(QJsonObject responseData) { QJsonValue errorVal = responseData.value("error"); - QJsonValue msgVal = responseData.value("errorMessage"); + QJsonValue errorMessageValue = responseData.value("errorMessage"); QJsonValue causeVal = responseData.value("cause"); - if (errorVal.isString() && msgVal.isString()) + if (errorVal.isString() && errorMessageValue.isString()) { - m_error = new Error(errorVal.toString(""), msgVal.toString(""), causeVal.toString("")); - return m_error->getDisplayMessage(); + m_error = std::shared_ptr<Error>(new Error{ + errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("")}); + return m_error->m_errorMessageVerbose; } else { @@ -156,13 +179,3 @@ QString YggdrasilTask::getStateMessage(const YggdrasilTask::State state) const return tr("Processing. Please wait."); } } - -YggdrasilTask::Error *YggdrasilTask::getError() const -{ - return this->m_error; -} - -MojangAccountPtr YggdrasilTask::getMojangAccount() const -{ - return this->m_account; -} diff --git a/logic/auth/YggdrasilTask.h b/logic/auth/YggdrasilTask.h index 62638c9d..1f81a2d0 100644 --- a/logic/auth/YggdrasilTask.h +++ b/logic/auth/YggdrasilTask.h @@ -19,6 +19,7 @@ #include <QString> #include <QJsonObject> +#include <QTimer> #include "logic/auth/MojangAccount.h" @@ -31,45 +32,18 @@ class YggdrasilTask : public Task { Q_OBJECT public: - explicit YggdrasilTask(MojangAccountPtr account, QObject *parent = 0); - ~YggdrasilTask(); + explicit YggdrasilTask(MojangAccount * account, QObject *parent = 0); /** * Class describing a Yggdrasil error response. */ - class Error + struct Error { - public: - Error(const QString& shortError, const QString& errorMessage, const QString& cause) : - m_shortError(shortError), m_errorMessage(errorMessage), m_cause(cause) {} - - QString getShortError() const { return m_shortError; } - QString getErrorMessage() const { return m_errorMessage; } - QString getCause() const { return m_cause; } - - /// Gets the string to display in the GUI for describing this error. - QString getDisplayMessage() - { - return getErrorMessage(); - } - - protected: - QString m_shortError; - QString m_errorMessage; + QString m_errorMessageShort; + QString m_errorMessageVerbose; QString m_cause; }; - /** - * Gets the Mojang account that this task is operating on. - */ - virtual MojangAccountPtr getMojangAccount() const; - - /** - * Returns a pointer to a YggdrasilTask::Error object if an error has occurred. - * If no error has occurred, returns a null pointer. - */ - virtual Error *getError() const; - protected: /** * Enum for describing the state of the current task. @@ -120,13 +94,25 @@ protected: */ virtual QString getStateMessage(const State state) const; - MojangAccountPtr m_account; - - QNetworkReply *m_netReply; - - Error *m_error; - protected slots: - void processReply(QNetworkReply *reply); + void processReply(); + void refreshTimers(qint64, qint64); + void heartbeat(); + +public +slots: + virtual void abort() override; + +protected: + // FIXME: segfault disaster waiting to happen + MojangAccount *m_account = nullptr; + QNetworkReply *m_netReply = nullptr; + std::shared_ptr<Error> m_error; + QTimer timeout_keeper; + QTimer counter; + int count = 0; // num msec since time reset + + const int timeout_max = 10000; + const int time_step = 50; }; diff --git a/logic/auth/flows/AuthenticateTask.cpp b/logic/auth/flows/AuthenticateTask.cpp index ec2004d6..f60be35d 100644 --- a/logic/auth/flows/AuthenticateTask.cpp +++ b/logic/auth/flows/AuthenticateTask.cpp @@ -26,7 +26,7 @@ #include "logger/QsLog.h" -AuthenticateTask::AuthenticateTask(MojangAccountPtr account, const QString &password, +AuthenticateTask::AuthenticateTask(MojangAccount * account, const QString &password, QObject *parent) : YggdrasilTask(account, parent), m_password(password) { @@ -59,14 +59,14 @@ QJsonObject AuthenticateTask::getRequestContent() const req.insert("agent", agent); } - req.insert("username", getMojangAccount()->username()); + req.insert("username", m_account->username()); req.insert("password", m_password); req.insert("requestUser", true); // If we already have a client token, give it to the server. // Otherwise, let the server give us one. - if (!getMojangAccount()->clientToken().isEmpty()) - req.insert("clientToken", getMojangAccount()->clientToken()); + if (!m_account->m_clientToken.isEmpty()) + req.insert("clientToken", m_account->m_clientToken); return req; } @@ -76,7 +76,7 @@ bool AuthenticateTask::processResponse(QJsonObject responseData) // Read the response data. We need to get the client token, access token, and the selected // profile. QLOG_DEBUG() << "Processing authentication response."; - + // QLOG_DEBUG() << responseData; // If we already have a client token, make sure the one the server gave us matches our // existing one. QLOG_DEBUG() << "Getting client token."; @@ -88,8 +88,7 @@ bool AuthenticateTask::processResponse(QJsonObject responseData) QLOG_ERROR() << "Server didn't send a client token."; return false; } - if (!getMojangAccount()->clientToken().isEmpty() && - clientToken != getMojangAccount()->clientToken()) + if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken) { // The server changed our client token! Obey its wishes, but complain. That's what I do // for my parents, so... @@ -97,7 +96,7 @@ bool AuthenticateTask::processResponse(QJsonObject responseData) << "'. This shouldn't happen, but it isn't really a big deal."; } // Set the client token. - getMojangAccount()->setClientToken(clientToken); + m_account->m_clientToken = clientToken; // Now, we set the access token. QLOG_DEBUG() << "Getting access token."; @@ -109,7 +108,7 @@ bool AuthenticateTask::processResponse(QJsonObject responseData) QLOG_ERROR() << "Server didn't send an access token."; } // Set the access token. - getMojangAccount()->setAccessToken(accessToken); + m_account->m_accessToken = accessToken; // Now we load the list of available profiles. // Mojang hasn't yet implemented the profile system, @@ -117,13 +116,14 @@ bool AuthenticateTask::processResponse(QJsonObject responseData) // don't have trouble implementing it later. QLOG_DEBUG() << "Loading profile list."; QJsonArray availableProfiles = responseData.value("availableProfiles").toArray(); - ProfileList loadedProfiles; + QList<AccountProfile> loadedProfiles; for (auto iter : availableProfiles) { QJsonObject profile = iter.toObject(); // Profiles are easy, we just need their ID and name. QString id = profile.value("id").toString(""); QString name = profile.value("name").toString(""); + bool legacy = profile.value("legacy").toBool(false); if (id.isEmpty() || name.isEmpty()) { @@ -135,10 +135,10 @@ bool AuthenticateTask::processResponse(QJsonObject responseData) } // Now, add a new AccountProfile entry to the list. - loadedProfiles.append(AccountProfile(id, name)); + loadedProfiles.append({id, name, legacy}); } // Put the list of profiles we loaded into the MojangAccount object. - getMojangAccount()->loadProfiles(loadedProfiles); + m_account->m_profiles = loadedProfiles; // Finally, we set the current profile to the correct value. This is pretty simple. // We do need to make sure that the current profile that the server gave us @@ -153,7 +153,7 @@ bool AuthenticateTask::processResponse(QJsonObject responseData) QLOG_ERROR() << "Server didn't specify a currently selected profile."; return false; } - if (!getMojangAccount()->setProfile(currentProfileId)) + if (!m_account->setCurrentProfile(currentProfileId)) { // TODO: Set an error to display to the user. QLOG_ERROR() << "Server specified a selected profile that wasn't in the available " @@ -162,23 +162,20 @@ bool AuthenticateTask::processResponse(QJsonObject responseData) } // this is what the vanilla launcher passes to the userProperties launch param - // doesn't seem to be used for anything so far? I don't get any of this data on my account - // (peterixxx) - // is it a good idea to log this? if (responseData.contains("user")) { + User u; auto obj = responseData.value("user").toObject(); - auto userId = obj.value("id").toString(); + u.id = obj.value("id").toString(); auto propArray = obj.value("properties").toArray(); - QLOG_DEBUG() << "User ID: " << userId; - QLOG_DEBUG() << "User Properties: "; for (auto prop : propArray) { auto propTuple = prop.toObject(); auto name = propTuple.value("name").toString(); auto value = propTuple.value("value").toString(); - QLOG_DEBUG() << name << " : " << value; + u.properties.insert(name, value); } + m_account->m_user = u; } // We've made it through the minefield of possible errors. Return true to indicate that diff --git a/logic/auth/flows/AuthenticateTask.h b/logic/auth/flows/AuthenticateTask.h index 3b99caad..b6564657 100644 --- a/logic/auth/flows/AuthenticateTask.h +++ b/logic/auth/flows/AuthenticateTask.h @@ -30,7 +30,7 @@ class AuthenticateTask : public YggdrasilTask { Q_OBJECT public: - AuthenticateTask(MojangAccountPtr account, const QString &password, QObject *parent = 0); + AuthenticateTask(MojangAccount *account, const QString &password, QObject *parent = 0); protected: virtual QJsonObject getRequestContent() const; diff --git a/logic/auth/flows/InvalidateTask.cpp b/logic/auth/flows/InvalidateTask.cpp deleted file mode 100644 index e69de29b..00000000 --- a/logic/auth/flows/InvalidateTask.cpp +++ /dev/null diff --git a/logic/auth/flows/InvalidateTask.h b/logic/auth/flows/InvalidateTask.h deleted file mode 100644 index e69de29b..00000000 --- a/logic/auth/flows/InvalidateTask.h +++ /dev/null diff --git a/logic/auth/flows/RefreshTask.cpp b/logic/auth/flows/RefreshTask.cpp index b56ed9bc..5f68ccc7 100644 --- a/logic/auth/flows/RefreshTask.cpp +++ b/logic/auth/flows/RefreshTask.cpp @@ -25,7 +25,7 @@ #include "logger/QsLog.h" -RefreshTask::RefreshTask(MojangAccountPtr account, QObject *parent) +RefreshTask::RefreshTask(MojangAccount *account, QObject *parent) : YggdrasilTask(account, parent) { } @@ -44,13 +44,12 @@ QJsonObject RefreshTask::getRequestContent() const * "requestUser": true/false // request the user structure * } */ - auto account = getMojangAccount(); QJsonObject req; - req.insert("clientToken", account->clientToken()); - req.insert("accessToken", account->accessToken()); + req.insert("clientToken", m_account->m_clientToken); + req.insert("accessToken", m_account->m_accessToken); /* { - auto currentProfile = account->currentProfile(); + auto currentProfile = m_account->currentProfile(); QJsonObject profile; profile.insert("id", currentProfile->id()); profile.insert("name", currentProfile->name()); @@ -64,12 +63,11 @@ QJsonObject RefreshTask::getRequestContent() const bool RefreshTask::processResponse(QJsonObject responseData) { - auto account = getMojangAccount(); - // Read the response data. We need to get the client token, access token, and the selected // profile. QLOG_DEBUG() << "Processing authentication response."; + // QLOG_DEBUG() << responseData; // If we already have a client token, make sure the one the server gave us matches our // existing one. QString clientToken = responseData.value("clientToken").toString(""); @@ -80,7 +78,7 @@ bool RefreshTask::processResponse(QJsonObject responseData) QLOG_ERROR() << "Server didn't send a client token."; return false; } - if (!account->clientToken().isEmpty() && clientToken != account->clientToken()) + if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken) { // The server changed our client token! Obey its wishes, but complain. That's what I do // for my parents, so... @@ -104,7 +102,7 @@ bool RefreshTask::processResponse(QJsonObject responseData) // profile) QJsonObject currentProfile = responseData.value("selectedProfile").toObject(); QString currentProfileId = currentProfile.value("id").toString(""); - if (account->currentProfile()->id() != currentProfileId) + if (m_account->currentProfile()->id != currentProfileId) { // TODO: Set an error to display to the user. QLOG_ERROR() << "Server didn't specify the same selected profile as ours."; @@ -114,26 +112,26 @@ bool RefreshTask::processResponse(QJsonObject responseData) // this is what the vanilla launcher passes to the userProperties launch param if (responseData.contains("user")) { + User u; auto obj = responseData.value("user").toObject(); - auto userId = obj.value("id").toString(); + u.id = obj.value("id").toString(); auto propArray = obj.value("properties").toArray(); - QLOG_DEBUG() << "User ID: " << userId; - QLOG_DEBUG() << "User Properties: "; for (auto prop : propArray) { auto propTuple = prop.toObject(); auto name = propTuple.value("name").toString(); auto value = propTuple.value("value").toString(); - QLOG_DEBUG() << name << " : " << value; + u.properties.insert(name, value); } + m_account->m_user = u; } + // We've made it through the minefield of possible errors. Return true to indicate that // we've succeeded. QLOG_DEBUG() << "Finished reading refresh response."; // Reset the access token. - account->setAccessToken(accessToken); - account->propagateChange(); + m_account->m_accessToken = accessToken; return true; } @@ -147,9 +145,9 @@ QString RefreshTask::getStateMessage(const YggdrasilTask::State state) const switch (state) { case STATE_SENDING_REQUEST: - return tr("Refreshing: Sending request."); + return tr("Refreshing login token."); case STATE_PROCESSING_RESPONSE: - return tr("Refreshing: Processing response."); + return tr("Refreshing login token: Processing response."); default: return YggdrasilTask::getStateMessage(state); } diff --git a/logic/auth/flows/RefreshTask.h b/logic/auth/flows/RefreshTask.h index 2596f6c7..2fd50c60 100644 --- a/logic/auth/flows/RefreshTask.h +++ b/logic/auth/flows/RefreshTask.h @@ -30,7 +30,7 @@ class RefreshTask : public YggdrasilTask { Q_OBJECT public: - RefreshTask(MojangAccountPtr account, QObject *parent = 0); + RefreshTask(MojangAccount * account, QObject *parent = 0); protected: virtual QJsonObject getRequestContent() const; diff --git a/logic/auth/flows/ValidateTask.cpp b/logic/auth/flows/ValidateTask.cpp index d9e0e46b..84d5e703 100644 --- a/logic/auth/flows/ValidateTask.cpp +++ b/logic/auth/flows/ValidateTask.cpp @@ -26,7 +26,7 @@ #include "logger/QsLog.h" -ValidateTask::ValidateTask(MojangAccountPtr account, QObject *parent) +ValidateTask::ValidateTask(MojangAccount * account, QObject *parent) : YggdrasilTask(account, parent) { } @@ -34,7 +34,7 @@ ValidateTask::ValidateTask(MojangAccountPtr account, QObject *parent) QJsonObject ValidateTask::getRequestContent() const { QJsonObject req; - req.insert("accessToken", getMojangAccount()->accessToken()); + req.insert("accessToken", m_account->m_accessToken); return req; } diff --git a/logic/auth/flows/ValidateTask.h b/logic/auth/flows/ValidateTask.h index 3ff78c6a..0e34f0c3 100644 --- a/logic/auth/flows/ValidateTask.h +++ b/logic/auth/flows/ValidateTask.h @@ -13,6 +13,10 @@ * limitations under the License. */ +/* + * :FIXME: DEAD CODE, DEAD CODE, DEAD CODE! :FIXME: + */ + #pragma once #include <logic/auth/YggdrasilTask.h> @@ -28,7 +32,7 @@ class ValidateTask : public YggdrasilTask { Q_OBJECT public: - ValidateTask(MojangAccountPtr account, QObject *parent = 0); + ValidateTask(MojangAccount *account, QObject *parent = 0); protected: virtual QJsonObject getRequestContent() const; diff --git a/logic/lists/JavaVersionList.cpp b/logic/lists/JavaVersionList.cpp index 3dc1969b..d2f0972c 100644 --- a/logic/lists/JavaVersionList.cpp +++ b/logic/lists/JavaVersionList.cpp @@ -21,7 +21,8 @@ #include <QRegExp> #include "logger/QsLog.h" -#include <logic/JavaUtils.h> +#include "logic/JavaCheckerJob.h" +#include "logic/JavaUtils.h" JavaVersionList::JavaVersionList(QObject *parent) : BaseVersionList(parent) { @@ -49,7 +50,7 @@ int JavaVersionList::count() const int JavaVersionList::columnCount(const QModelIndex &parent) const { - return 4; + return 3; } QVariant JavaVersionList::data(const QModelIndex &index, int role) const @@ -75,9 +76,6 @@ QVariant JavaVersionList::data(const QModelIndex &index, int role) const case 2: return version->path; - case 3: - return version->recommended ? tr("Yes") : tr("No"); - default: return QVariant(); } @@ -109,9 +107,6 @@ QVariant JavaVersionList::headerData(int section, Qt::Orientation orientation, i case 2: return "Path"; - case 3: - return "Recommended"; - default: return QVariant(); } @@ -128,9 +123,6 @@ QVariant JavaVersionList::headerData(int section, Qt::Orientation orientation, i case 2: return "Path to this Java version."; - case 3: - return "Whether the version is recommended or not."; - default: return QVariant(); } @@ -142,15 +134,15 @@ QVariant JavaVersionList::headerData(int section, Qt::Orientation orientation, i BaseVersionPtr JavaVersionList::getTopRecommended() const { - for (int i = 0; i < m_vlist.length(); i++) + auto first = m_vlist.first(); + if(first != nullptr) { - auto ver = std::dynamic_pointer_cast<JavaVersion>(m_vlist.at(i)); - if (ver->recommended) - { - return m_vlist.at(i); - } + return first; + } + else + { + return BaseVersionPtr(); } - return BaseVersionPtr(); } void JavaVersionList::updateListData(QList<BaseVersionPtr> versions) @@ -183,20 +175,63 @@ void JavaListLoadTask::executeTask() setStatus("Detecting Java installations..."); JavaUtils ju; - QList<JavaVersionPtr> javas = ju.FindJavaPaths(); + QList<QString> candidate_paths = ju.FindJavaPaths(); + + auto job = new JavaCheckerJob("Java detection"); + connect(job, SIGNAL(finished(QList<JavaCheckResult>)), this, SLOT(javaCheckerFinished(QList<JavaCheckResult>))); + connect(job, SIGNAL(progress(int, int)), this, SLOT(checkerProgress(int, int))); + + QLOG_DEBUG() << "Probing the following Java paths: "; + for(QString candidate : candidate_paths) + { + QLOG_DEBUG() << " " << candidate; + + auto candidate_checker = new JavaChecker(); + candidate_checker->path = candidate; + job->addJavaCheckerAction(JavaCheckerPtr(candidate_checker)); + } + + job->start(); +} + +void JavaListLoadTask::checkerProgress(int current, int total) +{ + float progress = (current * 100.0) / total; + this->setProgress((int) progress); +} + +void JavaListLoadTask::javaCheckerFinished(QList<JavaCheckResult> results) +{ + QList<JavaVersionPtr> candidates; + + QLOG_DEBUG() << "Found the following valid Java installations:"; + for(JavaCheckResult result : results) + { + if(result.valid) + { + JavaVersionPtr javaVersion(new JavaVersion()); + + javaVersion->id = result.javaVersion; + javaVersion->arch = result.mojangPlatform; + javaVersion->path = result.path; + candidates.append(javaVersion); + + QLOG_DEBUG() << " " << javaVersion->id << javaVersion->arch << javaVersion->path; + } + } QList<BaseVersionPtr> javas_bvp; - for (int i = 0; i < javas.length(); i++) + for (auto &java : candidates) { - BaseVersionPtr java = std::dynamic_pointer_cast<BaseVersion>(javas.at(i)); + //QLOG_INFO() << java->id << java->arch << " at " << java->path; + BaseVersionPtr bp_java = std::dynamic_pointer_cast<BaseVersion>(java); - if (java) + if (bp_java) { - javas_bvp.append(java); + javas_bvp.append(bp_java); } } m_list->updateListData(javas_bvp); - emitSucceeded(); } diff --git a/logic/lists/JavaVersionList.h b/logic/lists/JavaVersionList.h index f816c932..879b2480 100644 --- a/logic/lists/JavaVersionList.h +++ b/logic/lists/JavaVersionList.h @@ -20,6 +20,7 @@ #include "BaseVersionList.h" #include "logic/tasks/Task.h" +#include "logic/JavaCheckerJob.h" class JavaListLoadTask; @@ -43,7 +44,6 @@ struct JavaVersion : public BaseVersion QString id; QString arch; QString path; - bool recommended; }; typedef std::shared_ptr<JavaVersion> JavaVersionPtr; @@ -85,6 +85,9 @@ public: ~JavaListLoadTask(); virtual void executeTask(); +public slots: + void javaCheckerFinished(QList<JavaCheckResult> results); + void checkerProgress(int current, int total); protected: JavaVersionList *m_list; diff --git a/logic/lists/MinecraftVersionList.cpp b/logic/lists/MinecraftVersionList.cpp index 2a9c0d3b..523b81ac 100644 --- a/logic/lists/MinecraftVersionList.cpp +++ b/logic/lists/MinecraftVersionList.cpp @@ -15,6 +15,7 @@ #include "MinecraftVersionList.h" #include "MultiMC.h" +#include "logic/net/URLConstants.h" #include <QtXml> @@ -28,10 +29,6 @@ #include <QtNetwork> -#define MCVLIST_URLBASE "http://s3.amazonaws.com/Minecraft.Download/versions/" -#define ASSETS_URLBASE "http://assets.minecraft.net/" -#define MCN_URLBASE "http://sonicrules.org/mcnweb.py" - MinecraftVersionList::MinecraftVersionList(QObject *parent) : BaseVersionList(parent) { } @@ -144,7 +141,7 @@ void MCVListLoadTask::executeTask() { setStatus("Loading instance version list..."); auto worker = MMC->qnam(); - vlistReply = worker->get(QNetworkRequest(QUrl(QString(MCVLIST_URLBASE) + "versions.json"))); + vlistReply = worker->get(QNetworkRequest(QUrl("http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + "versions.json"))); connect(vlistReply, SIGNAL(finished()), this, SLOT(list_downloaded())); } @@ -270,7 +267,7 @@ void MCVListLoadTask::list_downloaded() continue; } // Get the download URL. - QString dlUrl = QString(MCVLIST_URLBASE) + versionID + "/"; + QString dlUrl = "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + versionID + "/"; // Now, we construct the version object and add it to the list. std::shared_ptr<MinecraftVersion> mcVersion(new MinecraftVersion()); diff --git a/logic/net/FileDownload.cpp b/logic/net/MD5EtagDownload.cpp index 239af351..4b9f52af 100644 --- a/logic/net/FileDownload.cpp +++ b/logic/net/MD5EtagDownload.cpp @@ -14,12 +14,12 @@ */ #include "MultiMC.h" -#include "FileDownload.h" +#include "MD5EtagDownload.h" #include <pathutils.h> #include <QCryptographicHash> #include "logger/QsLog.h" -FileDownload::FileDownload(QUrl url, QString target_path) : NetAction() +MD5EtagDownload::MD5EtagDownload(QUrl url, QString target_path) : NetAction() { m_url = url; m_target_path = target_path; @@ -27,7 +27,7 @@ FileDownload::FileDownload(QUrl url, QString target_path) : NetAction() m_status = Job_NotStarted; } -void FileDownload::start() +void MD5EtagDownload::start() { QString filename = m_target_path; m_output_file.setFileName(filename); @@ -58,11 +58,20 @@ void FileDownload::start() return; } - QLOG_INFO() << "Downloading " << m_url.toString(); + QLOG_INFO() << "Downloading " << m_url.toString() << " expecting " << m_expected_md5; QNetworkRequest request(m_url); request.setRawHeader(QString("If-None-Match").toLatin1(), m_expected_md5.toLatin1()); request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); + // Go ahead and try to open the file. + // If we don't do this, empty files won't be created, which breaks the updater. + // Plus, this way, we don't end up starting a download for a file we can't open. + if (!m_output_file.open(QIODevice::WriteOnly)) + { + emit failed(index_within_job); + return; + } + auto worker = MMC->qnam(); QNetworkReply *rep = worker->get(request); @@ -75,19 +84,19 @@ void FileDownload::start() connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead())); } -void FileDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +void MD5EtagDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { emit progress(index_within_job, bytesReceived, bytesTotal); } -void FileDownload::downloadError(QNetworkReply::NetworkError error) +void MD5EtagDownload::downloadError(QNetworkReply::NetworkError error) { // error happened during download. // TODO: log the reason why m_status = Job_Failed; } -void FileDownload::downloadFinished() +void MD5EtagDownload::downloadFinished() { // if the download succeeded if (m_status != Job_Failed) @@ -96,6 +105,7 @@ void FileDownload::downloadFinished() m_status = Job_Finished; m_output_file.close(); + QLOG_INFO() << "Finished " << m_url.toString() << " got " << m_reply->rawHeader("ETag").constData(); m_reply.reset(); emit succeeded(index_within_job); return; @@ -110,7 +120,7 @@ void FileDownload::downloadFinished() } } -void FileDownload::downloadReadyRead() +void MD5EtagDownload::downloadReadyRead() { if (!m_output_file.isOpen()) { diff --git a/logic/net/FileDownload.h b/logic/net/MD5EtagDownload.h index 58380e86..416ab9de 100644 --- a/logic/net/FileDownload.h +++ b/logic/net/MD5EtagDownload.h @@ -18,8 +18,8 @@ #include "NetAction.h" #include <QFile> -typedef std::shared_ptr<class FileDownload> FileDownloadPtr; -class FileDownload : public NetAction +typedef std::shared_ptr<class MD5EtagDownload> Md5EtagDownloadPtr; +class MD5EtagDownload : public NetAction { Q_OBJECT public: @@ -35,10 +35,10 @@ public: QFile m_output_file; public: - explicit FileDownload(QUrl url, QString target_path); - static FileDownloadPtr make(QUrl url, QString target_path) + explicit MD5EtagDownload(QUrl url, QString target_path); + static Md5EtagDownloadPtr make(QUrl url, QString target_path) { - return FileDownloadPtr(new FileDownload(url, target_path)); + return Md5EtagDownloadPtr(new MD5EtagDownload(url, target_path)); } protected slots: diff --git a/logic/net/NetJob.cpp b/logic/net/NetJob.cpp index 333cdcbf..8b79bc54 100644 --- a/logic/net/NetJob.cpp +++ b/logic/net/NetJob.cpp @@ -16,7 +16,7 @@ #include "NetJob.h" #include "pathutils.h" #include "MultiMC.h" -#include "FileDownload.h" +#include "MD5EtagDownload.h" #include "ByteArrayDownload.h" #include "CacheDownload.h" diff --git a/logic/net/NetJob.h b/logic/net/NetJob.h index 021a1550..6e2e7607 100644 --- a/logic/net/NetJob.h +++ b/logic/net/NetJob.h @@ -18,7 +18,7 @@ #include <QLabel> #include "NetAction.h" #include "ByteArrayDownload.h" -#include "FileDownload.h" +#include "MD5EtagDownload.h" #include "CacheDownload.h" #include "HttpMetaCache.h" #include "ForgeXzDownload.h" @@ -94,6 +94,8 @@ signals: public slots: virtual void start(); + // FIXME: implement + virtual void abort() {}; private slots: void partProgress(int index, qint64 bytesReceived, qint64 bytesTotal); diff --git a/logic/net/PasteUpload.cpp b/logic/net/PasteUpload.cpp new file mode 100644 index 00000000..acf09291 --- /dev/null +++ b/logic/net/PasteUpload.cpp @@ -0,0 +1,84 @@ +#include "PasteUpload.h" +#include "MultiMC.h" +#include "logger/QsLog.h" +#include <QJsonObject> +#include <QJsonDocument> +#include "gui/dialogs/CustomMessageBox.h" +#include <QDesktopServices> + +PasteUpload::PasteUpload(QWidget *window, QString text) : m_text(text), m_window(window) +{ +} + +void PasteUpload::executeTask() +{ + QNetworkRequest request(QUrl("http://paste.ee/api")); + request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); + QByteArray content( + "key=public&description=MultiMC5+Log+File&language=plain&format=json&paste=" + + m_text.toUtf8()); + request.setRawHeader("Content-Type", "application/x-www-form-urlencoded"); + request.setRawHeader("Content-Length", QByteArray::number(content.size())); + + auto worker = MMC->qnam(); + QNetworkReply *rep = worker->post(request, content); + + m_reply = std::shared_ptr<QNetworkReply>(rep); + connect(rep, &QNetworkReply::downloadProgress, [&](qint64 value, qint64 max) + { setProgress(value / max * 100); }); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, + SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + +void PasteUpload::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + QLOG_ERROR() << "Network error: " << error; + emitFailed(m_reply->errorString()); +} + +void PasteUpload::downloadFinished() +{ + // if the download succeeded + if (m_reply->error() == QNetworkReply::NetworkError::NoError) + { + QByteArray data = m_reply->readAll(); + m_reply.reset(); + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + emitFailed(jsonError.errorString()); + return; + } + QString error; + if (!parseResult(doc, &error)) + { + emitFailed(error); + return; + } + } + // else the download failed + else + { + emitFailed(QString("Network error: %1").arg(m_reply->errorString())); + m_reply.reset(); + return; + } + emitSucceeded(); +} + +bool PasteUpload::parseResult(QJsonDocument doc, QString *parseError) +{ + auto object = doc.object(); + auto status = object.value("status").toString("error"); + if (status == "error") + { + parseError = new QString(object.value("error").toString()); + return false; + } + QString pasteUrl = object.value("paste").toObject().value("link").toString(); + QDesktopServices::openUrl(pasteUrl); + return true; +} diff --git a/logic/net/PasteUpload.h b/logic/net/PasteUpload.h new file mode 100644 index 00000000..917a0016 --- /dev/null +++ b/logic/net/PasteUpload.h @@ -0,0 +1,26 @@ +#pragma once +#include "logic/tasks/Task.h" +#include <QMessageBox> +#include <QNetworkReply> +#include <memory> + +class PasteUpload : public Task +{ + Q_OBJECT +public: + PasteUpload(QWidget *window, QString text); + +protected: + virtual void executeTask(); + +private: + bool parseResult(QJsonDocument doc, QString *parseError); + QString m_text; + QString m_error; + QWidget *m_window; + std::shared_ptr<QNetworkReply> m_reply; +public +slots: + void downloadError(QNetworkReply::NetworkError); + void downloadFinished(); +}; diff --git a/logic/net/S3ListBucket.cpp b/logic/net/S3ListBucket.cpp index 89dff951..439b7086 100644 --- a/logic/net/S3ListBucket.cpp +++ b/logic/net/S3ListBucket.cpp @@ -102,7 +102,6 @@ void S3ListBucket::processValidReply() }; // nothing went wrong... - QString prefix("http://s3.amazonaws.com/Minecraft.Resources/"); QByteArray ba = m_reply->readAll(); QString xmlErrorMsg; diff --git a/logic/net/URLConstants.h b/logic/net/URLConstants.h new file mode 100644 index 00000000..dcd5c2b1 --- /dev/null +++ b/logic/net/URLConstants.h @@ -0,0 +1,32 @@ +/* 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 <QString> + +namespace URLConstants +{ +const QString AWS_DOWNLOAD_BASE("s3.amazonaws.com/Minecraft.Download/"); +const QString AWS_DOWNLOAD_VERSIONS(AWS_DOWNLOAD_BASE + "versions/"); +const QString AWS_DOWNLOAD_LIBRARIES(AWS_DOWNLOAD_BASE + "libraries/"); +const QString AWS_DOWNLOAD_INDEXES(AWS_DOWNLOAD_BASE + "indexes/"); +const QString ASSETS_BASE("assets.minecraft.net/"); +//const QString MCN_BASE("sonicrules.org/mcnweb.py"); +const QString RESOURCE_BASE("resources.download.minecraft.net/"); +const QString LIBRARY_BASE("libraries.minecraft.net/"); +const QString SKINS_BASE("skins.minecraft.net/MinecraftSkins/"); +const QString AUTH_BASE("authserver.mojang.com/"); +} diff --git a/logic/tasks/ProgressProvider.h b/logic/tasks/ProgressProvider.h index f6f2906a..15e453a3 100644 --- a/logic/tasks/ProgressProvider.h +++ b/logic/tasks/ProgressProvider.h @@ -38,4 +38,5 @@ public: public slots: virtual void start() = 0; + virtual void abort() = 0; }; diff --git a/logic/tasks/Task.h b/logic/tasks/Task.h index d08ef560..80d5e38b 100644 --- a/logic/tasks/Task.h +++ b/logic/tasks/Task.h @@ -44,6 +44,7 @@ public: public slots: virtual void start(); + virtual void abort() {}; protected: virtual void executeTask() = 0; diff --git a/logic/updater/DownloadUpdateTask.cpp b/logic/updater/DownloadUpdateTask.cpp new file mode 100644 index 00000000..d9aab826 --- /dev/null +++ b/logic/updater/DownloadUpdateTask.cpp @@ -0,0 +1,404 @@ +/* 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::findCurrentVersionInfo() +{ + 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()) + { + // 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; + + // 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(); + }; + + 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); + checker->updateChanList(); + } + else + { + processFunc(); + } +} + +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"); + + // 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)); + } + + // 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 lists.")); + + parseVersionInfo(NEW_VERSION, &m_nVersionFileList); + + // 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); + } + + // 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(); +} + +void DownloadUpdateTask::parseVersionInfo(VersionInfoFileEnum vfile, VersionFileList* list) +{ + 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; + } + + QJsonObject json = jsonDoc.object(); + + 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(), + }; + 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); + } +} + +void DownloadUpdateTask::processFileLists() +{ + 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 : m_cVersionFileList) + { + bool keep = false; + for (VersionFileEntry newEntry : m_nVersionFileList) + { + 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) + m_operationList.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) + { + // 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)) + { + QCryptographicHash hash(QCryptographicHash::Md5); + hash.addData(entryFile.readAll()); + fileMD5 = hash.result().toHex(); + } + + if (!entryFile.exists() || fileMD5.isEmpty() || fileMD5 != entry.md5) + { + QLOG_DEBUG() << "Found file" << entry.path << "that needs updating."; + + // 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("/", "_")); + + // 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)); + } + } + } + } + + // 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")); +} + +void 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; + } + 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.")); + } +} + +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..f5b23d12 --- /dev/null +++ b/logic/updater/DownloadUpdateTask.h @@ -0,0 +1,192 @@ +/* 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(); + +protected: + // 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; + + /*! + * 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(); + + /*! + * 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 void parseVersionInfo(VersionInfoFileEnum vfile, VersionFileList* list); + + /*! + * 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 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); + + VersionFileList m_downloadList; + 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; + +protected slots: + void vinfoDownloadFinished(); + void vinfoDownloadFailed(); + + void fileDownloadFinished(); + void fileDownloadFailed(); + void fileDownloadProgressChanged(qint64 current, qint64 total); +}; + diff --git a/logic/updater/UpdateChecker.cpp b/logic/updater/UpdateChecker.cpp new file mode 100644 index 00000000..5ff1898e --- /dev/null +++ b/logic/updater/UpdateChecker.cpp @@ -0,0 +1,247 @@ +/* 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 "config.h" +#include "logger/QsLog.h" + +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonValue> + +#define API_VERSION 0 +#define CHANLIST_FORMAT 0 + +UpdateChecker::UpdateChecker() +{ + m_currentChannel = VERSION_CHANNEL; + 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() +{ + 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 URL for the channel we're using. + // 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. + 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, SIGNAL(succeeded()), SLOT(updateCheckFinished())); + connect(job, SIGNAL(failed()), SLOT(updateCheckFailed())); + indexJob.reset(job); + job->start(); +} + +void UpdateChecker::updateCheckFinished() +{ + 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()) + { + 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. + int newBuildNumber = newestVersion.value("Id").toVariant().toInt(); + if (newBuildNumber != MMC->version().build) + { + // Update! + emit updateAvailable(m_repoUrl, newestVersion.value("Name").toVariant().toString(), newBuildNumber); + } + + 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(); + + 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..59fb8e47 --- /dev/null +++ b/logic/updater/UpdateChecker.h @@ -0,0 +1,106 @@ +/* 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(); + + /*! + * 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 true 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(); + +private slots: + void updateCheckFinished(); + void updateCheckFailed(); + + void chanListDownloadFinished(); + void chanListDownloadFailed(); + +private: + NetJobPtr indexJob; + NetJobPtr chanListJob; + + QString m_repoUrl; + + QString m_channelListUrl; + QString m_currentChannel; + + 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; +}; + |