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