#include "ComponentUpdateTask.h" #include "ComponentList_p.h" #include "ComponentList.h" #include "Component.h" #include #include #include #include #include "ComponentUpdateTask_p.h" #include #include #include "net/Mode.h" #include "OneSixVersionFormat.h" /* * This is responsible for loading the components of a component list AND resolving dependency issues between them */ /* * FIXME: the 'one shot async task' nature of this does not fit the intended usage * Really, it should be a reactor/state machine that receives input from the application * and dynamically adapts to changing requirements... * * The reactor should be the only entry into manipulating the ComponentList. * See: https://en.wikipedia.org/wiki/Reactor_pattern */ /* * Or make this operate on a snapshot of the ComponentList state, then merge results in as long as the snapshot and ComponentList didn't change? * If the component list changes, start over. */ ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, ComponentList* list, QObject* parent) : Task(parent) { d.reset(new ComponentUpdateTaskData); d->m_list = list; d->mode = mode; d->netmode = netmode; } ComponentUpdateTask::~ComponentUpdateTask() { } void ComponentUpdateTask::executeTask() { qDebug() << "Loading components"; loadComponents(); } namespace { enum class LoadResult { LoadedLocal, RequiresRemote, Failed }; LoadResult composeLoadResult(LoadResult a, LoadResult b) { if (a < b) { return b; } return a; } static LoadResult loadComponent(ComponentPtr component, shared_qobject_ptr& loadTask, Net::Mode netmode) { if(component->m_loaded) { qDebug() << component->getName() << "is already loaded"; return LoadResult::LoadedLocal; } LoadResult result = LoadResult::Failed; auto customPatchFilename = component->getFilename(); if(QFile::exists(customPatchFilename)) { // if local file exists... // check for uid problems inside... bool fileChanged = false; auto file = ProfileUtils::parseJsonFile(QFileInfo(customPatchFilename), false); if(file->uid != component->m_uid) { file->uid = component->m_uid; fileChanged = true; } if(fileChanged) { // FIXME: @QUALITY do not ignore return value ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), customPatchFilename); } component->m_file = file; component->m_loaded = true; result = LoadResult::LoadedLocal; } else { auto metaVersion = ENV.metadataIndex()->get(component->m_uid, component->m_version); component->m_metaVersion = metaVersion; if(metaVersion->isLoaded()) { component->m_loaded = true; result = LoadResult::LoadedLocal; } else { metaVersion->load(netmode); loadTask = metaVersion->getCurrentTask(); if(loadTask) result = LoadResult::RequiresRemote; else if (metaVersion->isLoaded()) result = LoadResult::LoadedLocal; else result = LoadResult::Failed; } } return result; } // FIXME: dead code. determine if this can still be useful? /* static LoadResult loadComponentList(ComponentPtr component, shared_qobject_ptr& loadTask, Net::Mode netmode) { if(component->m_loaded) { qDebug() << component->getName() << "is already loaded"; return LoadResult::LoadedLocal; } LoadResult result = LoadResult::Failed; auto metaList = ENV.metadataIndex()->get(component->m_uid); if(metaList->isLoaded()) { component->m_loaded = true; result = LoadResult::LoadedLocal; } else { metaList->load(netmode); loadTask = metaList->getCurrentTask(); result = LoadResult::RequiresRemote; } return result; } */ static LoadResult loadIndex(shared_qobject_ptr& loadTask, Net::Mode netmode) { // FIXME: DECIDE. do we want to run the update task anyway? if(ENV.metadataIndex()->isLoaded()) { qDebug() << "Index is already loaded"; return LoadResult::LoadedLocal; } ENV.metadataIndex()->load(netmode); loadTask = ENV.metadataIndex()->getCurrentTask(); if(loadTask) { return LoadResult::RequiresRemote; } // FIXME: this is assuming the load succeeded... did it really? return LoadResult::LoadedLocal; } } void ComponentUpdateTask::loadComponents() { LoadResult result = LoadResult::LoadedLocal; size_t taskIndex = 0; size_t componentIndex = 0; d->remoteLoadSuccessful = true; // load the main index (it is needed to determine if components can revert) { // FIXME: tear out as a method? or lambda? shared_qobject_ptr indexLoadTask; auto singleResult = loadIndex(indexLoadTask, d->netmode); result = composeLoadResult(result, singleResult); if(indexLoadTask) { qDebug() << "Remote loading is being run for metadata index"; RemoteLoadStatus status; status.type = RemoteLoadStatus::Type::Index; d->remoteLoadStatusList.append(status); connect(indexLoadTask.get(), &Task::succeeded, [=]() { remoteLoadSucceeded(taskIndex); }); connect(indexLoadTask.get(), &Task::failed, [=](const QString & error) { remoteLoadFailed(taskIndex, error); }); taskIndex++; } } // load all the components OR their lists... for (auto component: d->m_list->d->components) { shared_qobject_ptr loadTask; LoadResult singleResult; RemoteLoadStatus::Type loadType; // FIXME: to do this right, we need to load the lists and decide on which versions to use during dependency resolution. For now, ignore all that... #if 0 switch(d->mode) { case Mode::Launch: { singleResult = loadComponent(component, loadTask, d->netmode); loadType = RemoteLoadStatus::Type::Version; break; } case Mode::Resolution: { singleResult = loadComponentList(component, loadTask, d->netmode); loadType = RemoteLoadStatus::Type::List; break; } } #else singleResult = loadComponent(component, loadTask, d->netmode); loadType = RemoteLoadStatus::Type::Version; #endif if(singleResult == LoadResult::LoadedLocal) { component->updateCachedData(); } result = composeLoadResult(result, singleResult); if (loadTask) { qDebug() << "Remote loading is being run for" << component->getName(); connect(loadTask.get(), &Task::succeeded, [=]() { remoteLoadSucceeded(taskIndex); }); connect(loadTask.get(), &Task::failed, [=](const QString & error) { remoteLoadFailed(taskIndex, error); }); RemoteLoadStatus status; status.type = loadType; status.componentListIndex = componentIndex; d->remoteLoadStatusList.append(status); taskIndex++; } componentIndex++; } d->remoteTasksInProgress = taskIndex; switch(result) { case LoadResult::LoadedLocal: { // Everything got loaded. Advance to dependency resolution. resolveDependencies(d->mode == Mode::Launch || d->netmode == Net::Mode::Offline); break; } case LoadResult::RequiresRemote: { // we wait for signals. break; } case LoadResult::Failed: { emitFailed(tr("Some component metadata load tasks failed.")); break; } } } namespace { struct RequireEx : public Meta::Require { size_t indexOfFirstDependee = 0; }; struct RequireCompositionResult { bool ok; RequireEx outcome; }; using RequireExSet = std::set; } static RequireCompositionResult composeRequirement(const RequireEx & a, const RequireEx & b) { assert(a.uid == b.uid); RequireEx out; out.uid = a.uid; out.indexOfFirstDependee = std::min(a.indexOfFirstDependee, b.indexOfFirstDependee); if(a.equalsVersion.isEmpty()) { out.equalsVersion = b.equalsVersion; } else if (b.equalsVersion.isEmpty()) { out.equalsVersion = a.equalsVersion; } else if (a.equalsVersion == b.equalsVersion) { out.equalsVersion = a.equalsVersion; } else { // FIXME: mark error as explicit version conflict return {false, out}; } if(a.suggests.isEmpty()) { out.suggests = b.suggests; } else if (b.suggests.isEmpty()) { out.suggests = a.suggests; } else { Version aVer(a.suggests); Version bVer(b.suggests); out.suggests = (aVer < bVer ? b.suggests : a.suggests); } return {true, out}; } // gather the requirements from all components, finding any obvious conflicts static bool gatherRequirementsFromComponents(const ComponentContainer & input, RequireExSet & output) { bool succeeded = true; size_t componentNum = 0; for(auto component: input) { auto &componentRequires = component->m_cachedRequires; for(const auto & componentRequire: componentRequires) { auto found = std::find_if(output.cbegin(), output.cend(), [componentRequire](const Meta::Require & req){ return req.uid == componentRequire.uid; }); RequireEx componenRequireEx; componenRequireEx.uid = componentRequire.uid; componenRequireEx.suggests = componentRequire.suggests; componenRequireEx.equalsVersion = componentRequire.equalsVersion; componenRequireEx.indexOfFirstDependee = componentNum; if(found != output.cend()) { // found... process it further auto result = composeRequirement(componenRequireEx, *found); if(result.ok) { output.erase(componenRequireEx); output.insert(result.outcome); } else { qCritical() << "Conflicting requirements:" << componentRequire.uid << "versions:" << componentRequire.equalsVersion << ";" << (*found).equalsVersion; } succeeded &= result.ok; } else { // not found, accumulate output.insert(componenRequireEx); } } componentNum++; } return succeeded; } /// Get list of uids that can be trivially removed because nothing is depending on them anymore (and they are installed as deps) static void getTrivialRemovals(const ComponentContainer & components, const RequireExSet & reqs, QStringList &toRemove) { for(const auto & component: components) { if(!component->m_dependencyOnly) continue; if(!component->m_cachedVolatile) continue; RequireEx reqNeedle; reqNeedle.uid = component->m_uid; const auto iter = reqs.find(reqNeedle); if(iter == reqs.cend()) { toRemove.append(component->m_uid); } } } /** * handles: * - trivial addition (there is an unmet requirement and it can be trivially met by adding something) * - trivial version conflict of dependencies == explicit version required and installed is different * * toAdd - set of requirements than mean adding a new component * toChange - set of requirements that mean changing version of an existing component */ static bool getTrivialComponentChanges(const ComponentIndex & index, const RequireExSet & input, RequireExSet & toAdd, RequireExSet & toChange) { enum class Decision { Undetermined, Met, Missing, VersionNotSame, LockedVersionNotSame } decision = Decision::Undetermined; QString reqStr; bool succeeded = true; // list the composed requirements and say if they are met or unmet for(auto & req: input) { do { if(req.equalsVersion.isEmpty()) { reqStr = QString("Req: %1").arg(req.uid); if(index.contains(req.uid)) { decision = Decision::Met; } else { toAdd.insert(req); decision = Decision::Missing; } break; } else { reqStr = QString("Req: %1 == %2").arg(req.uid, req.equalsVersion); const auto & compIter = index.find(req.uid); if(compIter == index.cend()) { toAdd.insert(req); decision = Decision::Missing; break; } auto & comp = (*compIter); if(comp->getVersion() != req.equalsVersion) { if(comp->isCustom()) { decision = Decision::LockedVersionNotSame; } else { if(comp->m_dependencyOnly) { decision = Decision::VersionNotSame; } else { decision = Decision::LockedVersionNotSame; } } break; } decision = Decision::Met; } } while(false); switch(decision) { case Decision::Undetermined: qCritical() << "No decision for" << reqStr; succeeded = false; break; case Decision::Met: qDebug() << reqStr << "Is met."; break; case Decision::Missing: qDebug() << reqStr << "Is missing and should be added at" << req.indexOfFirstDependee; toAdd.insert(req); break; case Decision::VersionNotSame: qDebug() << reqStr << "already has different version that can be changed."; toChange.insert(req); break; case Decision::LockedVersionNotSame: qDebug() << reqStr << "already has different version that cannot be changed."; succeeded = false; break; } } return succeeded; } // FIXME, TODO: decouple dependency resolution from loading // FIXME: This works directly with the ComponentList internals. It shouldn't! It needs richer data types than ComponentList uses. // FIXME: throw all this away and use a graph void ComponentUpdateTask::resolveDependencies(bool checkOnly) { qDebug() << "Resolving dependencies"; /* * this is a naive dependency resolving algorithm. all it does is check for following conditions and react in simple ways: * 1. There are conflicting dependencies on the same uid with different exact version numbers * -> hard error * 2. A dependency has non-matching exact version number * -> hard error * 3. A dependency is entirely missing and needs to be injected before the dependee(s) * -> requirements are injected * * NOTE: this is a placeholder and should eventually be replaced with something 'serious' */ auto & components = d->m_list->d->components; auto & componentIndex = d->m_list->d->componentIndex; RequireExSet allRequires; QStringList toRemove; do { allRequires.clear(); toRemove.clear(); if(!gatherRequirementsFromComponents(components, allRequires)) { emitFailed(tr("Conflicting requirements detected during dependency checking!")); return; } getTrivialRemovals(components, allRequires, toRemove); if(!toRemove.isEmpty()) { qDebug() << "Removing obsolete components..."; for(auto & remove : toRemove) { qDebug() << "Removing" << remove; d->m_list->remove(remove); } } } while (!toRemove.isEmpty()); RequireExSet toAdd; RequireExSet toChange; bool succeeded = getTrivialComponentChanges(componentIndex, allRequires, toAdd, toChange); if(!succeeded) { emitFailed(tr("Instance has conflicting dependencies.")); return; } if(checkOnly) { if(toAdd.size() || toChange.size()) { emitFailed(tr("Instance has unresolved dependencies while loading/checking for launch.")); } else { emitSucceeded(); } return; } bool recursionNeeded = false; if(toAdd.size()) { // add stuff... for(auto &add: toAdd) { ComponentPtr component = new Component(d->m_list, add.uid); if(!add.equalsVersion.isEmpty()) { // exact version qDebug() << "Adding" << add.uid << "version" << add.equalsVersion << "at position" << add.indexOfFirstDependee; component->m_version = add.equalsVersion; } else { // version needs to be decided qDebug() << "Adding" << add.uid << "at position" << add.indexOfFirstDependee; // ############################################################################################################ // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. if(!add.suggests.isEmpty()) { component->m_version = add.suggests; } else { if(add.uid == "org.lwjgl") { component->m_version = "2.9.1"; } else if (add.uid == "org.lwjgl3") { component->m_version = "3.1.2"; } else if (add.uid == "net.fabricmc.intermediary") { auto minecraft = std::find_if(components.begin(), components.end(), [](ComponentPtr & cmp){ return cmp->getID() == "net.minecraft"; }); if(minecraft != components.end()) { component->m_version = (*minecraft)->getVersion(); } } } // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. // ############################################################################################################ } component->m_dependencyOnly = true; // FIXME: this should not work directly with the component list d->m_list->insertComponent(add.indexOfFirstDependee, component); componentIndex[add.uid] = component; } recursionNeeded = true; } if(toChange.size()) { // change a version of something that exists for(auto &change: toChange) { // FIXME: this should not work directly with the component list qDebug() << "Setting version of " << change.uid << "to" << change.equalsVersion; auto component = componentIndex[change.uid]; component->setVersion(change.equalsVersion); } recursionNeeded = true; } if(recursionNeeded) { loadComponents(); } else { emitSucceeded(); } } void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex) { auto &taskSlot = d->remoteLoadStatusList[taskIndex]; if(taskSlot.finished) { qWarning() << "Got multiple results from remote load task" << taskIndex; return; } qDebug() << "Remote task" << taskIndex << "succeeded"; taskSlot.succeeded = false; taskSlot.finished = true; d->remoteTasksInProgress --; // update the cached data of the component from the downloaded version file. if (taskSlot.type == RemoteLoadStatus::Type::Version) { auto component = d->m_list->getComponent(taskSlot.componentListIndex); component->m_loaded = true; component->updateCachedData(); } checkIfAllFinished(); } void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg) { auto &taskSlot = d->remoteLoadStatusList[taskIndex]; if(taskSlot.finished) { qWarning() << "Got multiple results from remote load task" << taskIndex; return; } qDebug() << "Remote task" << taskIndex << "failed: " << msg; d->remoteLoadSuccessful = false; taskSlot.succeeded = false; taskSlot.finished = true; taskSlot.error = msg; d->remoteTasksInProgress --; checkIfAllFinished(); } void ComponentUpdateTask::checkIfAllFinished() { if(d->remoteTasksInProgress) { // not yet... return; } if(d->remoteLoadSuccessful) { // nothing bad happened... clear the temp load status and proceed with looking at dependencies d->remoteLoadStatusList.clear(); resolveDependencies(d->mode == Mode::Launch); } else { // remote load failed... report error and bail QStringList allErrorsList; for(auto & item: d->remoteLoadStatusList) { if(!item.succeeded) { allErrorsList.append(item.error); } } auto allErrors = allErrorsList.join("\n"); emitFailed(tr("Component metadata update task failed while downloading from remote server:\n%1").arg(allErrors)); d->remoteLoadStatusList.clear(); } }