1 #include "ComponentUpdateTask.h"
2 
3 #include "ComponentList_p.h"
4 #include "ComponentList.h"
5 #include "Component.h"
6 #include <Env.h>
7 #include <meta/Index.h>
8 #include <meta/VersionList.h>
9 #include <meta/Version.h>
10 #include "ComponentUpdateTask_p.h"
11 #include <cassert>
12 #include <Version.h>
13 #include "net/Mode.h"
14 #include "OneSixVersionFormat.h"
15 
16 /*
17  * This is responsible for loading the components of a component list AND resolving dependency issues between them
18  */
19 
20 /*
21  * FIXME: the 'one shot async task' nature of this does not fit the intended usage
22  * Really, it should be a reactor/state machine that receives input from the application
23  * and dynamically adapts to changing requirements...
24  *
25  * The reactor should be the only entry into manipulating the ComponentList.
26  * See: https://en.wikipedia.org/wiki/Reactor_pattern
27  */
28 
29 /*
30  * 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?
31  * If the component list changes, start over.
32  */
33 
ComponentUpdateTask(Mode mode,Net::Mode netmode,ComponentList * list,QObject * parent)34 ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, ComponentList* list, QObject* parent)
35     : Task(parent)
36 {
37     d.reset(new ComponentUpdateTaskData);
38     d->m_list = list;
39     d->mode = mode;
40     d->netmode = netmode;
41 }
42 
~ComponentUpdateTask()43 ComponentUpdateTask::~ComponentUpdateTask()
44 {
45 }
46 
executeTask()47 void ComponentUpdateTask::executeTask()
48 {
49     qDebug() << "Loading components";
50     loadComponents();
51 }
52 
53 namespace
54 {
55 enum class LoadResult
56 {
57     LoadedLocal,
58     RequiresRemote,
59     Failed
60 };
61 
composeLoadResult(LoadResult a,LoadResult b)62 LoadResult composeLoadResult(LoadResult a, LoadResult b)
63 {
64     if (a < b)
65     {
66         return b;
67     }
68     return a;
69 }
70 
loadComponent(ComponentPtr component,shared_qobject_ptr<Task> & loadTask,Net::Mode netmode)71 static LoadResult loadComponent(ComponentPtr component, shared_qobject_ptr<Task>& loadTask, Net::Mode netmode)
72 {
73     if(component->m_loaded)
74     {
75         qDebug() << component->getName() << "is already loaded";
76         return LoadResult::LoadedLocal;
77     }
78 
79     LoadResult result = LoadResult::Failed;
80     auto customPatchFilename = component->getFilename();
81     if(QFile::exists(customPatchFilename))
82     {
83         // if local file exists...
84 
85         // check for uid problems inside...
86         bool fileChanged = false;
87         auto file = ProfileUtils::parseJsonFile(QFileInfo(customPatchFilename), false);
88         if(file->uid != component->m_uid)
89         {
90             file->uid = component->m_uid;
91             fileChanged = true;
92         }
93         if(fileChanged)
94         {
95             // FIXME: @QUALITY do not ignore return value
96             ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), customPatchFilename);
97         }
98 
99         component->m_file = file;
100         component->m_loaded = true;
101         result = LoadResult::LoadedLocal;
102     }
103     else
104     {
105         auto metaVersion = ENV.metadataIndex()->get(component->m_uid, component->m_version);
106         component->m_metaVersion = metaVersion;
107         if(metaVersion->isLoaded())
108         {
109             component->m_loaded = true;
110             result = LoadResult::LoadedLocal;
111         }
112         else
113         {
114             metaVersion->load(netmode);
115             loadTask = metaVersion->getCurrentTask();
116             if(loadTask)
117                 result = LoadResult::RequiresRemote;
118             else if (metaVersion->isLoaded())
119                 result = LoadResult::LoadedLocal;
120             else
121                 result = LoadResult::Failed;
122         }
123     }
124     return result;
125 }
126 
127 // FIXME: dead code. determine if this can still be useful?
128 /*
129 static LoadResult loadComponentList(ComponentPtr component, shared_qobject_ptr<Task>& loadTask, Net::Mode netmode)
130 {
131     if(component->m_loaded)
132     {
133         qDebug() << component->getName() << "is already loaded";
134         return LoadResult::LoadedLocal;
135     }
136 
137     LoadResult result = LoadResult::Failed;
138     auto metaList = ENV.metadataIndex()->get(component->m_uid);
139     if(metaList->isLoaded())
140     {
141         component->m_loaded = true;
142         result = LoadResult::LoadedLocal;
143     }
144     else
145     {
146         metaList->load(netmode);
147         loadTask = metaList->getCurrentTask();
148         result = LoadResult::RequiresRemote;
149     }
150     return result;
151 }
152 */
153 
loadIndex(shared_qobject_ptr<Task> & loadTask,Net::Mode netmode)154 static LoadResult loadIndex(shared_qobject_ptr<Task>& loadTask, Net::Mode netmode)
155 {
156     // FIXME: DECIDE. do we want to run the update task anyway?
157     if(ENV.metadataIndex()->isLoaded())
158     {
159         qDebug() << "Index is already loaded";
160         return LoadResult::LoadedLocal;
161     }
162     ENV.metadataIndex()->load(netmode);
163     loadTask = ENV.metadataIndex()->getCurrentTask();
164     if(loadTask)
165     {
166         return LoadResult::RequiresRemote;
167     }
168     // FIXME: this is assuming the load succeeded... did it really?
169     return LoadResult::LoadedLocal;
170 }
171 }
172 
loadComponents()173 void ComponentUpdateTask::loadComponents()
174 {
175     LoadResult result = LoadResult::LoadedLocal;
176     size_t taskIndex = 0;
177     size_t componentIndex = 0;
178     d->remoteLoadSuccessful = true;
179     // load the main index (it is needed to determine if components can revert)
180     {
181         // FIXME: tear out as a method? or lambda?
182         shared_qobject_ptr<Task> indexLoadTask;
183         auto singleResult = loadIndex(indexLoadTask, d->netmode);
184         result = composeLoadResult(result, singleResult);
185         if(indexLoadTask)
186         {
187             qDebug() << "Remote loading is being run for metadata index";
188             RemoteLoadStatus status;
189             status.type = RemoteLoadStatus::Type::Index;
190             d->remoteLoadStatusList.append(status);
191             connect(indexLoadTask.get(), &Task::succeeded, [=]()
192             {
193                 remoteLoadSucceeded(taskIndex);
194             });
195             connect(indexLoadTask.get(), &Task::failed, [=](const QString & error)
196             {
197                 remoteLoadFailed(taskIndex, error);
198             });
199             taskIndex++;
200         }
201     }
202     // load all the components OR their lists...
203     for (auto component: d->m_list->d->components)
204     {
205         shared_qobject_ptr<Task> loadTask;
206         LoadResult singleResult;
207         RemoteLoadStatus::Type loadType;
208         // 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...
209 #if 0
210         switch(d->mode)
211         {
212             case Mode::Launch:
213             {
214                 singleResult = loadComponent(component, loadTask, d->netmode);
215                 loadType = RemoteLoadStatus::Type::Version;
216                 break;
217             }
218             case Mode::Resolution:
219             {
220                 singleResult = loadComponentList(component, loadTask, d->netmode);
221                 loadType = RemoteLoadStatus::Type::List;
222                 break;
223             }
224         }
225 #else
226         singleResult = loadComponent(component, loadTask, d->netmode);
227         loadType = RemoteLoadStatus::Type::Version;
228 #endif
229         if(singleResult == LoadResult::LoadedLocal)
230         {
231             component->updateCachedData();
232         }
233         result = composeLoadResult(result, singleResult);
234         if (loadTask)
235         {
236             qDebug() << "Remote loading is being run for" << component->getName();
237             connect(loadTask.get(), &Task::succeeded, [=]()
238             {
239                 remoteLoadSucceeded(taskIndex);
240             });
241             connect(loadTask.get(), &Task::failed, [=](const QString & error)
242             {
243                 remoteLoadFailed(taskIndex, error);
244             });
245             RemoteLoadStatus status;
246             status.type = loadType;
247             status.componentListIndex = componentIndex;
248             d->remoteLoadStatusList.append(status);
249             taskIndex++;
250         }
251         componentIndex++;
252     }
253     d->remoteTasksInProgress = taskIndex;
254     switch(result)
255     {
256         case LoadResult::LoadedLocal:
257         {
258             // Everything got loaded. Advance to dependency resolution.
259             resolveDependencies(d->mode == Mode::Launch || d->netmode == Net::Mode::Offline);
260             break;
261         }
262         case LoadResult::RequiresRemote:
263         {
264             // we wait for signals.
265             break;
266         }
267         case LoadResult::Failed:
268         {
269             emitFailed(tr("Some component metadata load tasks failed."));
270             break;
271         }
272     }
273 }
274 
275 namespace
276 {
277     struct RequireEx : public Meta::Require
278     {
279         size_t indexOfFirstDependee = 0;
280     };
281     struct RequireCompositionResult
282     {
283         bool ok;
284         RequireEx outcome;
285     };
286     using RequireExSet = std::set<RequireEx>;
287 }
288 
composeRequirement(const RequireEx & a,const RequireEx & b)289 static RequireCompositionResult composeRequirement(const RequireEx & a, const RequireEx & b)
290 {
291     assert(a.uid == b.uid);
292     RequireEx out;
293     out.uid = a.uid;
294     out.indexOfFirstDependee = std::min(a.indexOfFirstDependee, b.indexOfFirstDependee);
295     if(a.equalsVersion.isEmpty())
296     {
297         out.equalsVersion = b.equalsVersion;
298     }
299     else if (b.equalsVersion.isEmpty())
300     {
301         out.equalsVersion = a.equalsVersion;
302     }
303     else if (a.equalsVersion == b.equalsVersion)
304     {
305         out.equalsVersion = a.equalsVersion;
306     }
307     else
308     {
309         // FIXME: mark error as explicit version conflict
310         return {false, out};
311     }
312 
313     if(a.suggests.isEmpty())
314     {
315         out.suggests = b.suggests;
316     }
317     else if (b.suggests.isEmpty())
318     {
319         out.suggests = a.suggests;
320     }
321     else
322     {
323         Version aVer(a.suggests);
324         Version bVer(b.suggests);
325         out.suggests = (aVer < bVer ? b.suggests : a.suggests);
326     }
327     return {true, out};
328 }
329 
330 // gather the requirements from all components, finding any obvious conflicts
gatherRequirementsFromComponents(const ComponentContainer & input,RequireExSet & output)331 static bool gatherRequirementsFromComponents(const ComponentContainer & input, RequireExSet & output)
332 {
333     bool succeeded = true;
334     size_t componentNum = 0;
335     for(auto component: input)
336     {
337         auto &componentRequires = component->m_cachedRequires;
338         for(const auto & componentRequire: componentRequires)
339         {
340             auto found = std::find_if(output.cbegin(), output.cend(), [componentRequire](const Meta::Require & req){
341                 return req.uid == componentRequire.uid;
342             });
343 
344             RequireEx componenRequireEx;
345             componenRequireEx.uid = componentRequire.uid;
346             componenRequireEx.suggests = componentRequire.suggests;
347             componenRequireEx.equalsVersion = componentRequire.equalsVersion;
348             componenRequireEx.indexOfFirstDependee = componentNum;
349 
350             if(found != output.cend())
351             {
352                 // found... process it further
353                 auto result = composeRequirement(componenRequireEx, *found);
354                 if(result.ok)
355                 {
356                     output.erase(componenRequireEx);
357                     output.insert(result.outcome);
358                 }
359                 else
360                 {
361                     qCritical()
362                         << "Conflicting requirements:"
363                         << componentRequire.uid
364                         << "versions:"
365                         << componentRequire.equalsVersion
366                         << ";"
367                         << (*found).equalsVersion;
368                 }
369                 succeeded &= result.ok;
370             }
371             else
372             {
373                 // not found, accumulate
374                 output.insert(componenRequireEx);
375             }
376         }
377         componentNum++;
378     }
379     return succeeded;
380 }
381 
382 /// Get list of uids that can be trivially removed because nothing is depending on them anymore (and they are installed as deps)
getTrivialRemovals(const ComponentContainer & components,const RequireExSet & reqs,QStringList & toRemove)383 static void getTrivialRemovals(const ComponentContainer & components, const RequireExSet & reqs, QStringList &toRemove)
384 {
385     for(const auto & component: components)
386     {
387         if(!component->m_dependencyOnly)
388             continue;
389         if(!component->m_cachedVolatile)
390             continue;
391         RequireEx reqNeedle;
392         reqNeedle.uid = component->m_uid;
393         const auto iter = reqs.find(reqNeedle);
394         if(iter == reqs.cend())
395         {
396             toRemove.append(component->m_uid);
397         }
398     }
399 }
400 
401 /**
402  * handles:
403  * - trivial addition (there is an unmet requirement and it can be trivially met by adding something)
404  * - trivial version conflict of dependencies == explicit version required and installed is different
405  *
406  * toAdd - set of requirements than mean adding a new component
407  * toChange - set of requirements that mean changing version of an existing component
408  */
getTrivialComponentChanges(const ComponentIndex & index,const RequireExSet & input,RequireExSet & toAdd,RequireExSet & toChange)409 static bool getTrivialComponentChanges(const ComponentIndex & index, const RequireExSet & input, RequireExSet & toAdd, RequireExSet & toChange)
410 {
411     enum class Decision
412     {
413         Undetermined,
414         Met,
415         Missing,
416         VersionNotSame,
417         LockedVersionNotSame
418     } decision = Decision::Undetermined;
419 
420     QString reqStr;
421     bool succeeded = true;
422     // list the composed requirements and say if they are met or unmet
423     for(auto & req: input)
424     {
425         do
426         {
427             if(req.equalsVersion.isEmpty())
428             {
429                 reqStr = QString("Req: %1").arg(req.uid);
430                 if(index.contains(req.uid))
431                 {
432                     decision = Decision::Met;
433                 }
434                 else
435                 {
436                     toAdd.insert(req);
437                     decision = Decision::Missing;
438                 }
439                 break;
440             }
441             else
442             {
443                 reqStr = QString("Req: %1 == %2").arg(req.uid, req.equalsVersion);
444                 const auto & compIter = index.find(req.uid);
445                 if(compIter == index.cend())
446                 {
447                     toAdd.insert(req);
448                     decision = Decision::Missing;
449                     break;
450                 }
451                 auto & comp = (*compIter);
452                 if(comp->getVersion() != req.equalsVersion)
453                 {
454                     if(comp->isCustom()) {
455                         decision = Decision::LockedVersionNotSame;
456                     } else {
457                         if(comp->m_dependencyOnly)
458                         {
459                             decision = Decision::VersionNotSame;
460                         }
461                         else
462                         {
463                             decision = Decision::LockedVersionNotSame;
464                         }
465                     }
466                     break;
467                 }
468                 decision = Decision::Met;
469             }
470         } while(false);
471         switch(decision)
472         {
473             case Decision::Undetermined:
474                 qCritical() << "No decision for" << reqStr;
475                 succeeded = false;
476                 break;
477             case Decision::Met:
478                 qDebug() << reqStr << "Is met.";
479                 break;
480             case Decision::Missing:
481                 qDebug() << reqStr << "Is missing and should be added at" << req.indexOfFirstDependee;
482                 toAdd.insert(req);
483                 break;
484             case Decision::VersionNotSame:
485                 qDebug() << reqStr << "already has different version that can be changed.";
486                 toChange.insert(req);
487                 break;
488             case Decision::LockedVersionNotSame:
489                 qDebug() << reqStr << "already has different version that cannot be changed.";
490                 succeeded = false;
491                 break;
492         }
493     }
494     return succeeded;
495 }
496 
497 // FIXME, TODO: decouple dependency resolution from loading
498 // FIXME: This works directly with the ComponentList internals. It shouldn't! It needs richer data types than ComponentList uses.
499 // FIXME: throw all this away and use a graph
resolveDependencies(bool checkOnly)500 void ComponentUpdateTask::resolveDependencies(bool checkOnly)
501 {
502     qDebug() << "Resolving dependencies";
503     /*
504      * this is a naive dependency resolving algorithm. all it does is check for following conditions and react in simple ways:
505      * 1. There are conflicting dependencies on the same uid with different exact version numbers
506      *    -> hard error
507      * 2. A dependency has non-matching exact version number
508      *    -> hard error
509      * 3. A dependency is entirely missing and needs to be injected before the dependee(s)
510      *    -> requirements are injected
511      *
512      * NOTE: this is a placeholder and should eventually be replaced with something 'serious'
513      */
514     auto & components = d->m_list->d->components;
515     auto & componentIndex = d->m_list->d->componentIndex;
516 
517     RequireExSet allRequires;
518     QStringList toRemove;
519     do
520     {
521         allRequires.clear();
522         toRemove.clear();
523         if(!gatherRequirementsFromComponents(components, allRequires))
524         {
525             emitFailed(tr("Conflicting requirements detected during dependency checking!"));
526             return;
527         }
528         getTrivialRemovals(components, allRequires, toRemove);
529         if(!toRemove.isEmpty())
530         {
531             qDebug() << "Removing obsolete components...";
532             for(auto & remove : toRemove)
533             {
534                 qDebug() << "Removing" << remove;
535                 d->m_list->remove(remove);
536             }
537         }
538     } while (!toRemove.isEmpty());
539     RequireExSet toAdd;
540     RequireExSet toChange;
541     bool succeeded = getTrivialComponentChanges(componentIndex, allRequires, toAdd, toChange);
542     if(!succeeded)
543     {
544         emitFailed(tr("Instance has conflicting dependencies."));
545         return;
546     }
547     if(checkOnly)
548     {
549         if(toAdd.size() || toChange.size())
550         {
551             emitFailed(tr("Instance has unresolved dependencies while loading/checking for launch."));
552         }
553         else
554         {
555             emitSucceeded();
556         }
557         return;
558     }
559 
560     bool recursionNeeded = false;
561     if(toAdd.size())
562     {
563         // add stuff...
564         for(auto &add: toAdd)
565         {
566             ComponentPtr component = new Component(d->m_list, add.uid);
567             if(!add.equalsVersion.isEmpty())
568             {
569                 // exact version
570                 qDebug() << "Adding" << add.uid << "version" << add.equalsVersion << "at position" << add.indexOfFirstDependee;
571                 component->m_version = add.equalsVersion;
572             }
573             else
574             {
575                 // version needs to be decided
576                 qDebug() << "Adding" << add.uid << "at position" << add.indexOfFirstDependee;
577 // ############################################################################################################
578 // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded.
579                 if(!add.suggests.isEmpty())
580                 {
581                     component->m_version = add.suggests;
582                 }
583                 else
584                 {
585                     if(add.uid == "org.lwjgl")
586                     {
587                         component->m_version = "2.9.1";
588                     }
589                     else if (add.uid == "org.lwjgl3")
590                     {
591                         component->m_version = "3.1.2";
592                     }
593                     else if (add.uid == "net.fabricmc.intermediary")
594                     {
595                         auto minecraft = std::find_if(components.begin(), components.end(), [](ComponentPtr & cmp){
596                             return cmp->getID() == "net.minecraft";
597                         });
598                         if(minecraft != components.end()) {
599                             component->m_version = (*minecraft)->getVersion();
600                         }
601                     }
602                 }
603 // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded.
604 // ############################################################################################################
605             }
606             component->m_dependencyOnly = true;
607             // FIXME: this should not work directly with the component list
608             d->m_list->insertComponent(add.indexOfFirstDependee, component);
609             componentIndex[add.uid] = component;
610         }
611         recursionNeeded = true;
612     }
613     if(toChange.size())
614     {
615         // change a version of something that exists
616         for(auto &change: toChange)
617         {
618             // FIXME: this should not work directly with the component list
619             qDebug() << "Setting version of " << change.uid << "to" << change.equalsVersion;
620             auto component = componentIndex[change.uid];
621             component->setVersion(change.equalsVersion);
622         }
623         recursionNeeded = true;
624     }
625 
626     if(recursionNeeded)
627     {
628         loadComponents();
629     }
630     else
631     {
632         emitSucceeded();
633     }
634 }
635 
remoteLoadSucceeded(size_t taskIndex)636 void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex)
637 {
638     auto &taskSlot = d->remoteLoadStatusList[taskIndex];
639     if(taskSlot.finished)
640     {
641         qWarning() << "Got multiple results from remote load task" << taskIndex;
642         return;
643     }
644     qDebug() << "Remote task" << taskIndex << "succeeded";
645     taskSlot.succeeded = false;
646     taskSlot.finished = true;
647     d->remoteTasksInProgress --;
648     // update the cached data of the component from the downloaded version file.
649     if (taskSlot.type == RemoteLoadStatus::Type::Version)
650     {
651         auto component = d->m_list->getComponent(taskSlot.componentListIndex);
652         component->m_loaded = true;
653         component->updateCachedData();
654     }
655     checkIfAllFinished();
656 }
657 
658 
remoteLoadFailed(size_t taskIndex,const QString & msg)659 void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg)
660 {
661     auto &taskSlot = d->remoteLoadStatusList[taskIndex];
662     if(taskSlot.finished)
663     {
664         qWarning() << "Got multiple results from remote load task" << taskIndex;
665         return;
666     }
667     qDebug() << "Remote task" << taskIndex << "failed: " << msg;
668     d->remoteLoadSuccessful = false;
669     taskSlot.succeeded = false;
670     taskSlot.finished = true;
671     taskSlot.error = msg;
672     d->remoteTasksInProgress --;
673     checkIfAllFinished();
674 }
675 
checkIfAllFinished()676 void ComponentUpdateTask::checkIfAllFinished()
677 {
678     if(d->remoteTasksInProgress)
679     {
680         // not yet...
681         return;
682     }
683     if(d->remoteLoadSuccessful)
684     {
685         // nothing bad happened... clear the temp load status and proceed with looking at dependencies
686         d->remoteLoadStatusList.clear();
687         resolveDependencies(d->mode == Mode::Launch);
688     }
689     else
690     {
691         // remote load failed... report error and bail
692         QStringList allErrorsList;
693         for(auto & item: d->remoteLoadStatusList)
694         {
695             if(!item.succeeded)
696             {
697                 allErrorsList.append(item.error);
698             }
699         }
700         auto allErrors = allErrorsList.join("\n");
701         emitFailed(tr("Component metadata update task failed while downloading from remote server:\n%1").arg(allErrors));
702         d->remoteLoadStatusList.clear();
703     }
704 }
705