1 /* Copyright 2013-2019 MultiMC Contributors
2  *
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 
16 #include <QDir>
17 #include <QDirIterator>
18 #include <QSet>
19 #include <QFile>
20 #include <QThread>
21 #include <QTextStream>
22 #include <QXmlStreamReader>
23 #include <QTimer>
24 #include <QDebug>
25 #include <QFileSystemWatcher>
26 #include <QUuid>
27 #include <QJsonArray>
28 #include <QJsonDocument>
29 
30 #include "InstanceList.h"
31 #include "BaseInstance.h"
32 #include "InstanceTask.h"
33 #include "settings/INISettingsObject.h"
34 #include "minecraft/legacy/LegacyInstance.h"
35 #include "NullInstance.h"
36 #include "minecraft/MinecraftInstance.h"
37 #include "FileSystem.h"
38 #include "ExponentialSeries.h"
39 #include "WatchLock.h"
40 
41 const static int GROUP_FILE_FORMAT_VERSION = 1;
42 
InstanceList(SettingsObjectPtr settings,const QString & instDir,QObject * parent)43 InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir, QObject *parent)
44     : QAbstractListModel(parent), m_globalSettings(settings)
45 {
46     resumeWatch();
47     // Create aand normalize path
48     if (!QDir::current().exists(instDir))
49     {
50         QDir::current().mkpath(instDir);
51     }
52 
53     connect(this, &InstanceList::instancesChanged, this, &InstanceList::providerUpdated);
54 
55     // NOTE: canonicalPath requires the path to exist. Do not move this above the creation block!
56     m_instDir = QDir(instDir).canonicalPath();
57     m_watcher = new QFileSystemWatcher(this);
58     connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &InstanceList::instanceDirContentsChanged);
59     m_watcher->addPath(m_instDir);
60 }
61 
~InstanceList()62 InstanceList::~InstanceList()
63 {
64 }
65 
rowCount(const QModelIndex & parent) const66 int InstanceList::rowCount(const QModelIndex &parent) const
67 {
68     Q_UNUSED(parent);
69     return m_instances.count();
70 }
71 
index(int row,int column,const QModelIndex & parent) const72 QModelIndex InstanceList::index(int row, int column, const QModelIndex &parent) const
73 {
74     Q_UNUSED(parent);
75     if (row < 0 || row >= m_instances.size())
76         return QModelIndex();
77     return createIndex(row, column, (void *)m_instances.at(row).get());
78 }
79 
data(const QModelIndex & index,int role) const80 QVariant InstanceList::data(const QModelIndex &index, int role) const
81 {
82     if (!index.isValid())
83     {
84         return QVariant();
85     }
86     BaseInstance *pdata = static_cast<BaseInstance *>(index.internalPointer());
87     switch (role)
88     {
89     case InstancePointerRole:
90     {
91         QVariant v = qVariantFromValue((void *)pdata);
92         return v;
93     }
94     case InstanceIDRole:
95     {
96         return pdata->id();
97     }
98     case Qt::EditRole:
99     case Qt::DisplayRole:
100     {
101         return pdata->name();
102     }
103     case Qt::AccessibleTextRole:
104     {
105         return tr("%1 Instance").arg(pdata->name());
106     }
107     case Qt::ToolTipRole:
108     {
109         return pdata->instanceRoot();
110     }
111     case Qt::DecorationRole:
112     {
113         return pdata->iconKey();
114     }
115     // HACK: see GroupView.h in gui!
116     case GroupRole:
117     {
118         return getInstanceGroup(pdata->id());
119     }
120     default:
121         break;
122     }
123     return QVariant();
124 }
125 
setData(const QModelIndex & index,const QVariant & value,int role)126 bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int role)
127 {
128     if (!index.isValid())
129     {
130         return false;
131     }
132     if(role != Qt::EditRole)
133     {
134         return false;
135     }
136     BaseInstance *pdata = static_cast<BaseInstance *>(index.internalPointer());
137     auto newName = value.toString();
138     if(pdata->name() == newName)
139     {
140         return true;
141     }
142     pdata->setName(newName);
143     return true;
144 }
145 
flags(const QModelIndex & index) const146 Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const
147 {
148     Qt::ItemFlags f;
149     if (index.isValid())
150     {
151         f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
152     }
153     return f;
154 }
155 
getInstanceGroup(const InstanceId & id) const156 GroupId InstanceList::getInstanceGroup(const InstanceId& id) const
157 {
158     auto inst = getInstanceById(id);
159     if(!inst)
160     {
161         return GroupId();
162     }
163     auto iter = m_instanceGroupIndex.find(inst->id());
164     if(iter != m_instanceGroupIndex.end())
165     {
166         return *iter;
167     }
168     return GroupId();
169 }
170 
setInstanceGroup(const InstanceId & id,const GroupId & name)171 void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name)
172 {
173     auto inst = getInstanceById(id);
174     if(!inst)
175     {
176         qDebug() << "Attempt to set a null instance's group";
177         return;
178     }
179 
180     bool changed = false;
181     auto iter = m_instanceGroupIndex.find(inst->id());
182     if(iter != m_instanceGroupIndex.end())
183     {
184         if(*iter != name)
185         {
186             *iter = name;
187             changed = true;
188         }
189     }
190     else
191     {
192         changed = true;
193         m_instanceGroupIndex[id] = name;
194     }
195 
196     if(changed)
197     {
198         m_groupNameCache.insert(name);
199         auto idx = getInstIndex(inst.get());
200         emit dataChanged(index(idx), index(idx), {GroupRole});
201         saveGroupList();
202     }
203 }
204 
getGroups()205 QStringList InstanceList::getGroups()
206 {
207     return m_groupNameCache.toList();
208 }
209 
deleteGroup(const QString & name)210 void InstanceList::deleteGroup(const QString& name)
211 {
212     bool removed = false;
213     qDebug() << "Delete group" << name;
214     for(auto & instance: m_instances)
215     {
216         const auto & instID = instance->id();
217         auto instGroupName = getInstanceGroup(instID);
218         if(instGroupName == name)
219         {
220             m_instanceGroupIndex.remove(instID);
221             qDebug() << "Remove" << instID << "from group" << name;
222             removed = true;
223             auto idx = getInstIndex(instance.get());
224             if(idx > 0)
225             {
226                 emit dataChanged(index(idx), index(idx), {GroupRole});
227             }
228         }
229     }
230     if(removed)
231     {
232         saveGroupList();
233     }
234 }
235 
isGroupCollapsed(const QString & group)236 bool InstanceList::isGroupCollapsed(const QString& group)
237 {
238     return m_collapsedGroups.contains(group);
239 }
240 
deleteInstance(const InstanceId & id)241 void InstanceList::deleteInstance(const InstanceId& id)
242 {
243     auto inst = getInstanceById(id);
244     if(!inst)
245     {
246         qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?).";
247         return;
248     }
249 
250     if(m_instanceGroupIndex.remove(id))
251     {
252         saveGroupList();
253     }
254 
255     qDebug() << "Will delete instance" << id;
256     if(!FS::deletePath(inst->instanceRoot()))
257     {
258         qWarning() << "Deletion of instance" << id << "has not been completely successful ...";
259         return;
260     }
261 
262     qDebug() << "Instance" << id << "has been deleted by MultiMC.";
263 }
264 
getIdMapping(const QList<InstancePtr> & list)265 static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr> &list)
266 {
267     QMap<InstanceId, InstanceLocator> out;
268     int i = 0;
269     for(auto & item: list)
270     {
271         auto id = item->id();
272         if(out.contains(id))
273         {
274             qWarning() << "Duplicate ID" << id << "in instance list";
275         }
276         out[id] = std::make_pair(item, i);
277         i++;
278     }
279     return out;
280 }
281 
discoverInstances()282 QList< InstanceId > InstanceList::discoverInstances()
283 {
284     qDebug() << "Discovering instances in" << m_instDir;
285     QList<InstanceId> out;
286     QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks);
287     while (iter.hasNext())
288     {
289         QString subDir = iter.next();
290         QFileInfo dirInfo(subDir);
291         if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists())
292             continue;
293         // if it is a symlink, ignore it if it goes to the instance folder
294         if(dirInfo.isSymLink())
295         {
296             QFileInfo targetInfo(dirInfo.symLinkTarget());
297             QFileInfo instDirInfo(m_instDir);
298             if(targetInfo.canonicalPath() == instDirInfo.canonicalFilePath())
299             {
300                 qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder";
301                 continue;
302             }
303         }
304         auto id = dirInfo.fileName();
305         out.append(id);
306         qDebug() << "Found instance ID" << id;
307     }
308     instanceSet = out.toSet();
309     m_instancesProbed = true;
310     return out;
311 }
312 
loadList()313 InstanceList::InstListError InstanceList::loadList()
314 {
315     auto existingIds = getIdMapping(m_instances);
316 
317     QList<InstancePtr> newList;
318 
319     for(auto & id: discoverInstances())
320     {
321         if(existingIds.contains(id))
322         {
323             auto instPair = existingIds[id];
324             existingIds.remove(id);
325             qDebug() << "Should keep and soft-reload" << id;
326         }
327         else
328         {
329             InstancePtr instPtr = loadInstance(id);
330             if(instPtr)
331             {
332                 newList.append(instPtr);
333             }
334         }
335     }
336 
337     // TODO: looks like a general algorithm with a few specifics inserted. Do something about it.
338     if(!existingIds.isEmpty())
339     {
340         // get the list of removed instances and sort it by their original index, from last to first
341         auto deadList = existingIds.values();
342         auto orderSortPredicate = [](const InstanceLocator & a, const InstanceLocator & b) -> bool
343         {
344             return a.second > b.second;
345         };
346         std::sort(deadList.begin(), deadList.end(), orderSortPredicate);
347         // remove the contiguous ranges of rows
348         int front_bookmark = -1;
349         int back_bookmark = -1;
350         int currentItem = -1;
351         auto removeNow = [&]()
352         {
353             beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark);
354             m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1);
355             endRemoveRows();
356             front_bookmark = -1;
357             back_bookmark = currentItem;
358         };
359         for(auto & removedItem: deadList)
360         {
361             auto instPtr = removedItem.first;
362             instPtr->invalidate();
363             currentItem = removedItem.second;
364             if(back_bookmark == -1)
365             {
366                 // no bookmark yet
367                 back_bookmark = currentItem;
368             }
369             else if(currentItem == front_bookmark - 1)
370             {
371                 // part of contiguous sequence, continue
372             }
373             else
374             {
375                 // seam between previous and current item
376                 removeNow();
377             }
378             front_bookmark = currentItem;
379         }
380         if(back_bookmark != -1)
381         {
382             removeNow();
383         }
384     }
385     if(newList.size())
386     {
387         add(newList);
388     }
389     m_dirty = false;
390     return NoError;
391 }
392 
saveNow()393 void InstanceList::saveNow()
394 {
395     for(auto & item: m_instances)
396     {
397         item->saveNow();
398     }
399 }
400 
add(const QList<InstancePtr> & t)401 void InstanceList::add(const QList<InstancePtr> &t)
402 {
403     beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1);
404     m_instances.append(t);
405     for(auto & ptr : t)
406     {
407         connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged);
408     }
409     endInsertRows();
410 }
411 
resumeWatch()412 void InstanceList::resumeWatch()
413 {
414     if(m_watchLevel > 0)
415     {
416         qWarning() << "Bad suspend level resume in instance list";
417         return;
418     }
419     m_watchLevel++;
420     if(m_watchLevel > 0 && m_dirty)
421     {
422         loadList();
423     }
424 }
425 
suspendWatch()426 void InstanceList::suspendWatch()
427 {
428     m_watchLevel --;
429 }
430 
providerUpdated()431 void InstanceList::providerUpdated()
432 {
433     m_dirty = true;
434     if(m_watchLevel == 1)
435     {
436         loadList();
437     }
438 }
439 
getInstanceById(QString instId) const440 InstancePtr InstanceList::getInstanceById(QString instId) const
441 {
442     if(instId.isEmpty())
443         return InstancePtr();
444     for(auto & inst: m_instances)
445     {
446         if (inst->id() == instId)
447         {
448             return inst;
449         }
450     }
451     return InstancePtr();
452 }
453 
getInstanceIndexById(const QString & id) const454 QModelIndex InstanceList::getInstanceIndexById(const QString &id) const
455 {
456     return index(getInstIndex(getInstanceById(id).get()));
457 }
458 
getInstIndex(BaseInstance * inst) const459 int InstanceList::getInstIndex(BaseInstance *inst) const
460 {
461     int count = m_instances.count();
462     for (int i = 0; i < count; i++)
463     {
464         if (inst == m_instances[i].get())
465         {
466             return i;
467         }
468     }
469     return -1;
470 }
471 
propertiesChanged(BaseInstance * inst)472 void InstanceList::propertiesChanged(BaseInstance *inst)
473 {
474     int i = getInstIndex(inst);
475     if (i != -1)
476     {
477         emit dataChanged(index(i), index(i));
478     }
479 }
480 
loadInstance(const InstanceId & id)481 InstancePtr InstanceList::loadInstance(const InstanceId& id)
482 {
483     if(!m_groupsLoaded)
484     {
485         loadGroupList();
486     }
487 
488     auto instanceRoot = FS::PathCombine(m_instDir, id);
489     auto instanceSettings = std::make_shared<INISettingsObject>(FS::PathCombine(instanceRoot, "instance.cfg"));
490     InstancePtr inst;
491 
492     instanceSettings->registerSetting("InstanceType", "Legacy");
493 
494     QString inst_type = instanceSettings->get("InstanceType").toString();
495 
496     if (inst_type == "OneSix" || inst_type == "Nostalgia")
497     {
498         inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot));
499     }
500     else if (inst_type == "Legacy")
501     {
502         inst.reset(new LegacyInstance(m_globalSettings, instanceSettings, instanceRoot));
503     }
504     else
505     {
506         inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot));
507     }
508     qDebug() << "Loaded instance " << inst->name() << " from " << inst->instanceRoot();
509     return inst;
510 }
511 
saveGroupList()512 void InstanceList::saveGroupList()
513 {
514     qDebug() << "Will save group list now.";
515     if(!m_instancesProbed)
516     {
517         qDebug() << "Group saving prevented because we don't know the full list of instances yet.";
518         return;
519     }
520     WatchLock foo(m_watcher, m_instDir);
521     QString groupFileName = m_instDir + "/instgroups.json";
522     QMap<QString, QSet<QString>> reverseGroupMap;
523     for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++)
524     {
525         QString id = iter.key();
526         QString group = iter.value();
527         if (group.isEmpty())
528             continue;
529         if(!instanceSet.contains(id))
530         {
531             qDebug() << "Skipping saving missing instance" << id << "to groups list.";
532             continue;
533         }
534 
535         if (!reverseGroupMap.count(group))
536         {
537             QSet<QString> set;
538             set.insert(id);
539             reverseGroupMap[group] = set;
540         }
541         else
542         {
543             QSet<QString> &set = reverseGroupMap[group];
544             set.insert(id);
545         }
546     }
547     QJsonObject toplevel;
548     toplevel.insert("formatVersion", QJsonValue(QString("1")));
549     QJsonObject groupsArr;
550     for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++)
551     {
552         auto list = iter.value();
553         auto name = iter.key();
554         QJsonObject groupObj;
555         QJsonArray instanceArr;
556         groupObj.insert("hidden", QJsonValue(m_collapsedGroups.contains(name)));
557         for (auto item : list)
558         {
559             instanceArr.append(QJsonValue(item));
560         }
561         groupObj.insert("instances", instanceArr);
562         groupsArr.insert(name, groupObj);
563     }
564     toplevel.insert("groups", groupsArr);
565     QJsonDocument doc(toplevel);
566     try
567     {
568         FS::write(groupFileName, doc.toJson());
569         qDebug() << "Group list saved.";
570     }
571     catch (const FS::FileSystemException &e)
572     {
573         qCritical() << "Failed to write instance group file :" << e.cause();
574     }
575 }
576 
loadGroupList()577 void InstanceList::loadGroupList()
578 {
579     qDebug() << "Will load group list now.";
580 
581     QString groupFileName = m_instDir + "/instgroups.json";
582 
583     // if there's no group file, fail
584     if (!QFileInfo(groupFileName).exists())
585         return;
586 
587     QByteArray jsonData;
588     try
589     {
590         jsonData = FS::read(groupFileName);
591     }
592     catch (const FS::FileSystemException &e)
593     {
594         qCritical() << "Failed to read instance group file :" << e.cause();
595         return;
596     }
597 
598     QJsonParseError error;
599     QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error);
600 
601     // if the json was bad, fail
602     if (error.error != QJsonParseError::NoError)
603     {
604         qCritical() << QString("Failed to parse instance group file: %1 at offset %2")
605                             .arg(error.errorString(), QString::number(error.offset))
606                             .toUtf8();
607         return;
608     }
609 
610     // if the root of the json wasn't an object, fail
611     if (!jsonDoc.isObject())
612     {
613         qWarning() << "Invalid group file. Root entry should be an object.";
614         return;
615     }
616 
617     QJsonObject rootObj = jsonDoc.object();
618 
619     // Make sure the format version matches, otherwise fail.
620     if (rootObj.value("formatVersion").toVariant().toInt() != GROUP_FILE_FORMAT_VERSION)
621         return;
622 
623     // Get the groups. if it's not an object, fail
624     if (!rootObj.value("groups").isObject())
625     {
626         qWarning() << "Invalid group list JSON: 'groups' should be an object.";
627         return;
628     }
629 
630     QSet<QString> groupSet;
631     m_instanceGroupIndex.clear();
632 
633     // Iterate through all the groups.
634     QJsonObject groupMapping = rootObj.value("groups").toObject();
635     for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++)
636     {
637         QString groupName = iter.key();
638 
639         // If not an object, complain and skip to the next one.
640         if (!iter.value().isObject())
641         {
642             qWarning() << QString("Group '%1' in the group list should be an object.").arg(groupName).toUtf8();
643             continue;
644         }
645 
646         QJsonObject groupObj = iter.value().toObject();
647         if (!groupObj.value("instances").isArray())
648         {
649             qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.").arg(groupName).toUtf8();
650             continue;
651         }
652 
653         // keep a list/set of groups for choosing
654         groupSet.insert(groupName);
655 
656         auto hidden = groupObj.value("hidden").toBool(false);
657         if(hidden) {
658             m_collapsedGroups.insert(groupName);
659         }
660 
661         // Iterate through the list of instances in the group.
662         QJsonArray instancesArray = groupObj.value("instances").toArray();
663 
664         for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end(); iter2++)
665         {
666             m_instanceGroupIndex[(*iter2).toString()] = groupName;
667         }
668     }
669     m_groupsLoaded = true;
670     m_groupNameCache.unite(groupSet);
671     qDebug() << "Group list loaded.";
672 }
673 
instanceDirContentsChanged(const QString & path)674 void InstanceList::instanceDirContentsChanged(const QString& path)
675 {
676     Q_UNUSED(path);
677     emit instancesChanged();
678 }
679 
on_InstFolderChanged(const Setting & setting,QVariant value)680 void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value)
681 {
682     QString newInstDir = QDir(value.toString()).canonicalPath();
683     if(newInstDir != m_instDir)
684     {
685         if(m_groupsLoaded)
686         {
687             saveGroupList();
688         }
689         m_instDir = newInstDir;
690         m_groupsLoaded = false;
691         emit instancesChanged();
692     }
693 }
694 
on_GroupStateChanged(const QString & group,bool collapsed)695 void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed)
696 {
697     qDebug() << "Group" << group << (collapsed ? "collapsed" : "expanded");
698     if(collapsed) {
699         m_collapsedGroups.insert(group);
700     } else {
701         m_collapsedGroups.remove(group);
702     }
703     saveGroupList();
704 }
705 
706 class InstanceStaging : public Task
707 {
708 Q_OBJECT
709     const unsigned minBackoff = 1;
710     const unsigned maxBackoff = 16;
711 public:
InstanceStaging(InstanceList * parent,Task * child,const QString & stagingPath,const QString & instanceName,const QString & groupName)712     InstanceStaging (
713         InstanceList * parent,
714         Task * child,
715         const QString & stagingPath,
716         const QString& instanceName,
717         const QString& groupName )
718     : backoff(minBackoff, maxBackoff)
719     {
720         m_parent = parent;
721         m_child.reset(child);
722         connect(child, &Task::succeeded, this, &InstanceStaging::childSucceded);
723         connect(child, &Task::failed, this, &InstanceStaging::childFailed);
724         connect(child, &Task::status, this, &InstanceStaging::setStatus);
725         connect(child, &Task::progress, this, &InstanceStaging::setProgress);
726         m_instanceName = instanceName;
727         m_groupName = groupName;
728         m_stagingPath = stagingPath;
729         m_backoffTimer.setSingleShot(true);
730         connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded);
731     }
732 
~InstanceStaging()733     virtual ~InstanceStaging() {};
734 
735 
736     // FIXME/TODO: add ability to abort during instance commit retries
abort()737     bool abort() override
738     {
739         if(m_child && m_child->canAbort())
740         {
741             return m_child->abort();
742         }
743         return false;
744     }
canAbort() const745     bool canAbort() const override
746     {
747         if(m_child && m_child->canAbort())
748         {
749             return true;
750         }
751         return false;
752     }
753 
754 protected:
executeTask()755     virtual void executeTask() override
756     {
757         m_child->start();
758     }
warnings() const759     QStringList warnings() const override
760     {
761         return m_child->warnings();
762     }
763 
764 private slots:
childSucceded()765     void childSucceded()
766     {
767         unsigned sleepTime = backoff();
768         if(m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName))
769         {
770             emitSucceeded();
771             return;
772         }
773         // we actually failed, retry?
774         if(sleepTime == maxBackoff)
775         {
776             emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something."));
777             return;
778         }
779         qDebug() << "Failed to commit instance" << m_instanceName << "Initiating backoff:" << sleepTime;
780         m_backoffTimer.start(sleepTime * 500);
781     }
childFailed(const QString & reason)782     void childFailed(const QString & reason)
783     {
784         m_parent->destroyStagingPath(m_stagingPath);
785         emitFailed(reason);
786     }
787 
788 private:
789     /*
790      * WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows.
791      * Basically, it starts messing things up while MultiMC is extracting/creating instances
792      * and causes that horrible failure that is NTFS to lock files in place because they are open.
793      */
794     ExponentialSeries backoff;
795     QString m_stagingPath;
796     InstanceList * m_parent;
797     unique_qobject_ptr<Task> m_child;
798     QString m_instanceName;
799     QString m_groupName;
800     QTimer m_backoffTimer;
801 };
802 
wrapInstanceTask(InstanceTask * task)803 Task * InstanceList::wrapInstanceTask(InstanceTask * task)
804 {
805     auto stagingPath = getStagedInstancePath();
806     task->setStagingPath(stagingPath);
807     task->setParentSettings(m_globalSettings);
808     return new InstanceStaging(this, task, stagingPath, task->name(), task->group());
809 }
810 
getStagedInstancePath()811 QString InstanceList::getStagedInstancePath()
812 {
813     QString key = QUuid::createUuid().toString();
814     QString relPath = FS::PathCombine("_MMC_TEMP/" , key);
815     QDir rootPath(m_instDir);
816     auto path = FS::PathCombine(m_instDir, relPath);
817     if(!rootPath.mkpath(relPath))
818     {
819         return QString();
820     }
821     return path;
822 }
823 
commitStagedInstance(const QString & path,const QString & instanceName,const QString & groupName)824 bool InstanceList::commitStagedInstance(const QString& path, const QString& instanceName, const QString& groupName)
825 {
826     QDir dir;
827     QString instID = FS::DirNameFromString(instanceName, m_instDir);
828     {
829         WatchLock lock(m_watcher, m_instDir);
830         QString destination = FS::PathCombine(m_instDir, instID);
831         if(!dir.rename(path, destination))
832         {
833             qWarning() << "Failed to move" << path << "to" << destination;
834             return false;
835         }
836         m_instanceGroupIndex[instID] = groupName;
837         instanceSet.insert(instID);
838         m_groupNameCache.insert(groupName);
839         emit instancesChanged();
840         emit instanceSelectRequest(instID);
841     }
842     saveGroupList();
843     return true;
844 }
845 
destroyStagingPath(const QString & keyPath)846 bool InstanceList::destroyStagingPath(const QString& keyPath)
847 {
848     return FS::deletePath(keyPath);
849 }
850 
851 #include "InstanceList.moc"