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"