cleanups
This commit is contained in:
+1
-59
@@ -19,21 +19,15 @@
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
// Своя очередь, а не тысячи QNetworkReply внутри QNetworkAccessManager: там они
|
||||
// встают в невидимую очередь, каждый со своим объектом и без управления отменой.
|
||||
constexpr int kMaxParallel = 8;
|
||||
constexpr int kMaxAttempts = 3;
|
||||
constexpr int kTransferTimeoutMs = 30000;
|
||||
// Копирование тысяч мелких файлов одним циклом заморозило бы окно на секунды.
|
||||
constexpr int kLegacyCopiesPerTick = 64;
|
||||
// Столько уже имеющихся файлов проверяем за один заход, остальное — следующим.
|
||||
constexpr int kSkipsPerTick = 256;
|
||||
constexpr int kMaxInheritDepth = 10;
|
||||
|
||||
const QString kResourcesUrl = QStringLiteral("https://resources.download.minecraft.net/");
|
||||
|
||||
// Хэш из json попадает в путь на диске — принимаем только настоящий sha1.
|
||||
bool isSha1Hex(const QString &value)
|
||||
{
|
||||
if (value.size() != 40)
|
||||
@@ -45,8 +39,6 @@ bool isSha1Hex(const QString &value)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Относительный путь из json нельзя подставлять как есть: «../..» увёл бы
|
||||
// запись за пределы .minecraft.
|
||||
bool isSafeRelativePath(const QString &path)
|
||||
{
|
||||
if (path.isEmpty() || path.startsWith(u'/') || path.contains(u'\\') || path.contains(u':'))
|
||||
@@ -59,7 +51,6 @@ bool isSafeRelativePath(const QString &path)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Одиночный сегмент: id версии, id индекса ресурсов, имя конфига логирования.
|
||||
bool isSafeRelativeSegment(const QString &value)
|
||||
{
|
||||
return !value.contains(u'/') && isSafeRelativePath(value);
|
||||
@@ -93,7 +84,7 @@ QString fileNameOf(const QString &path)
|
||||
return QFileInfo(path).fileName();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
}
|
||||
|
||||
VersionInstaller::VersionInstaller(VersionManifestService *manifest, QObject *parent)
|
||||
: QObject(parent)
|
||||
@@ -102,8 +93,6 @@ VersionInstaller::VersionInstaller(VersionManifestService *manifest, QObject *pa
|
||||
, m_progressTimer(new QTimer(this))
|
||||
, m_legacyTimer(new QTimer(this))
|
||||
{
|
||||
// Восемь параллельных ответов шлют downloadProgress на каждый кусок данных:
|
||||
// без склейки привязки в QML пересчитывались бы сотни раз в секунду.
|
||||
m_progressTimer->setInterval(100);
|
||||
connect(m_progressTimer, &QTimer::timeout, this, &VersionInstaller::flushProgress);
|
||||
|
||||
@@ -113,14 +102,10 @@ VersionInstaller::VersionInstaller(VersionManifestService *manifest, QObject *pa
|
||||
|
||||
VersionInstaller::~VersionInstaller()
|
||||
{
|
||||
// Из деструктора сигналы уже некому слушать, а QSaveFile сам выбросит
|
||||
// недописанные временные файлы.
|
||||
blockSignals(true);
|
||||
cancel();
|
||||
}
|
||||
|
||||
// ── Прогресс ───────────────────────────────────────────────────────────────
|
||||
|
||||
qint64 VersionInstaller::bytesDone() const
|
||||
{
|
||||
qint64 total = m_bytesSettled;
|
||||
@@ -161,8 +146,6 @@ void VersionInstaller::flushProgress()
|
||||
emit progressChanged();
|
||||
}
|
||||
|
||||
// ── Очередь установок ──────────────────────────────────────────────────────
|
||||
|
||||
bool VersionInstaller::isQueued(const QString &versionId) const
|
||||
{
|
||||
for (const auto &entry : m_pendingInstalls) {
|
||||
@@ -255,16 +238,12 @@ void VersionInstaller::resolveManifestEntry()
|
||||
return;
|
||||
}
|
||||
|
||||
// Версии нет в манифесте, но описание могло уже лечь на диск: так
|
||||
// ставятся сборки модлоадеров, чьи id Mojang не знает.
|
||||
QFile local(versionJsonPath(m_gameDir, id));
|
||||
if (local.open(QIODevice::ReadOnly)) {
|
||||
adoptVersionJson(local.readAll(), /*alreadySaved=*/true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Иначе это вручную вписанный id сборки — сообщение не должно
|
||||
// выглядеть как сетевая ошибка.
|
||||
failInstall(Loc::text("version.error.notInManifest")
|
||||
.arg(id));
|
||||
});
|
||||
@@ -306,8 +285,6 @@ void VersionInstaller::fetchVersionJson(const QUrl &url, const QString &expected
|
||||
});
|
||||
}
|
||||
|
||||
// Общий хвост для описания версии, откуда бы оно ни пришло: из сети или уже
|
||||
// лежащим на диске (сборки модлоадеров кладут его туда сами).
|
||||
void VersionInstaller::adoptVersionJson(const QByteArray &payload, bool alreadySaved)
|
||||
{
|
||||
const QString id = m_versionId;
|
||||
@@ -334,8 +311,6 @@ void VersionInstaller::adoptVersionJson(const QByteArray &payload, bool alreadyS
|
||||
}
|
||||
}
|
||||
|
||||
// Ванильные версии ничего не наследуют, но сборка могла бы — тогда
|
||||
// сначала ставим родителя, а потом возвращаемся к этой версии.
|
||||
const QString parentId = m_versionJson.value(QStringLiteral("inheritsFrom")).toString();
|
||||
if (!parentId.isEmpty() && isSafeRelativeSegment(parentId)
|
||||
&& !QFile::exists(versionJsonPath(m_gameDir, parentId))) {
|
||||
@@ -355,12 +330,9 @@ void VersionInstaller::adoptVersionJson(const QByteArray &payload, bool alreadyS
|
||||
pump();
|
||||
}
|
||||
|
||||
// ── Составление списка файлов ──────────────────────────────────────────────
|
||||
|
||||
void VersionInstaller::enqueue(DownloadTask task)
|
||||
{
|
||||
if (task.url.scheme() != QLatin1String("https")) {
|
||||
// Подменённое описание версии не должно уводить загрузку на чужой адрес.
|
||||
m_warnings << Loc::text("version.warning.skippedBadUrl").arg(task.label);
|
||||
return;
|
||||
}
|
||||
@@ -373,8 +345,6 @@ bool VersionInstaller::prepareTasks()
|
||||
setStage(Loc::text("version.progress.preparing"));
|
||||
|
||||
QString error;
|
||||
// features пустые намеренно: Mojang отбирает библиотеки только по ОС,
|
||||
// а feature-правила живут в arguments, которые установщику не нужны.
|
||||
m_version = VersionLoader::load(m_gameDir, m_versionId, {}, &error);
|
||||
if (!m_version.isValid()) {
|
||||
failInstall(error.isEmpty() ? Loc::text("common.error.versionReadFailed").arg(m_versionId)
|
||||
@@ -397,8 +367,6 @@ bool VersionInstaller::prepareTasks()
|
||||
enqueue(std::move(task));
|
||||
}
|
||||
|
||||
// url, sha1 и size у библиотек уже разобраны VersionLoader — это готовые
|
||||
// описания загрузки, разбирать json заново не нужно.
|
||||
for (const MinecraftLibrary &library : std::as_const(m_version.libraries)) {
|
||||
if (library.url.isEmpty() || !isSafeRelativePath(library.path)) {
|
||||
m_warnings << Loc::text("version.error.libraryNoUrl").arg(library.name);
|
||||
@@ -425,9 +393,6 @@ bool VersionInstaller::prepareTasks()
|
||||
task.size = assetIndex.value(QStringLiteral("size")).toInteger();
|
||||
task.label = Loc::text("version.progress.assetIndex");
|
||||
enqueue(std::move(task));
|
||||
|
||||
// totalSize — суммарный вес всех ресурсов, известный ещё до скачивания
|
||||
// индекса: без него полоса первые секунды стояла бы и потом прыгнула.
|
||||
m_assetsDeclaredTotal = assetIndex.value(QStringLiteral("totalSize")).toInteger();
|
||||
m_bytesTotal += m_assetsDeclaredTotal;
|
||||
}
|
||||
@@ -454,8 +419,6 @@ bool VersionInstaller::prepareTasks()
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Движок загрузки ────────────────────────────────────────────────────────
|
||||
|
||||
void VersionInstaller::schedulePump()
|
||||
{
|
||||
if (m_pumpScheduled)
|
||||
@@ -476,12 +439,9 @@ void VersionInstaller::pump()
|
||||
while (m_active.size() < kMaxParallel && !m_queue.isEmpty()) {
|
||||
const int before = m_active.size();
|
||||
startTask(m_queue.dequeue());
|
||||
// startTask мог провалить установку — тогда очередь уже разобрана.
|
||||
if (!m_running || m_canceling)
|
||||
return;
|
||||
if (m_active.size() == before) {
|
||||
// Файл уже был на месте — сотни таких проверок подряд ощутимы,
|
||||
// поэтому остаток берём следующим проходом событийного цикла.
|
||||
if (++skipped >= kSkipsPerTick && !m_queue.isEmpty()) {
|
||||
schedulePump();
|
||||
return;
|
||||
@@ -504,8 +464,6 @@ void VersionInstaller::startTask(DownloadTask task)
|
||||
return;
|
||||
}
|
||||
|
||||
// Временный файл QSaveFile ложится рядом с целевым, поэтому папка нужна
|
||||
// даже до открытия.
|
||||
if (!QDir().mkpath(info.absolutePath())) {
|
||||
failInstall(Loc::text("common.error.createFailed").arg(QDir::toNativeSeparators(info.absolutePath())));
|
||||
return;
|
||||
@@ -522,7 +480,6 @@ void VersionInstaller::startTask(DownloadTask task)
|
||||
QNetworkRequest request{task.url};
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
||||
QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
// Без таймаута зависший сокет подвесил бы установку навсегда.
|
||||
request.setTransferTimeout(kTransferTimeoutMs);
|
||||
|
||||
context->reply = m_network->get(request);
|
||||
@@ -603,8 +560,6 @@ void VersionInstaller::handleReplyFinished(const std::shared_ptr<ActiveDownload>
|
||||
pump();
|
||||
}
|
||||
|
||||
// Возвращает true, если задача поставлена на повтор, и false, если установка
|
||||
// уже провалена.
|
||||
bool VersionInstaller::retryOrFail(DownloadTask task, const QString &reason)
|
||||
{
|
||||
if (task.attempts + 1 >= kMaxAttempts) {
|
||||
@@ -617,7 +572,6 @@ bool VersionInstaller::retryOrFail(DownloadTask task, const QString &reason)
|
||||
QTimer::singleShot(delay, this, [this, task] {
|
||||
if (!m_running || m_canceling)
|
||||
return;
|
||||
// В начало очереди: иначе повтор ждал бы за тысячами ресурсов.
|
||||
m_queue.prepend(task);
|
||||
pump();
|
||||
});
|
||||
@@ -661,15 +615,11 @@ void VersionInstaller::expandAssetIndex(const QString &indexPath)
|
||||
enqueue(std::move(task));
|
||||
}
|
||||
|
||||
// enqueue уже прибавил вес ресурсов к итогу — снимаем заявленный заранее,
|
||||
// чтобы он не учитывался дважды.
|
||||
m_bytesTotal -= m_assetsDeclaredTotal;
|
||||
m_assetsDeclaredTotal = actualTotal;
|
||||
scheduleProgress();
|
||||
}
|
||||
|
||||
// ── Завершение ─────────────────────────────────────────────────────────────
|
||||
|
||||
void VersionInstaller::afterDownloads()
|
||||
{
|
||||
const bool legacy = m_assetsVirtual || m_assetsMapToResources
|
||||
@@ -742,8 +692,6 @@ void VersionInstaller::legacyTick()
|
||||
|
||||
void VersionInstaller::complete()
|
||||
{
|
||||
// Приёмка той же проверкой, которой пользуется запуск: успешная установка
|
||||
// тогда по построению запускаема.
|
||||
LaunchOptions probe;
|
||||
probe.gameDir = m_gameDir;
|
||||
probe.versionId = m_versionId;
|
||||
@@ -784,8 +732,6 @@ void VersionInstaller::failInstall(const QString &message)
|
||||
if (!m_warnings.isEmpty())
|
||||
text += u'\n' + m_warnings.mid(0, 3).join(u'\n');
|
||||
|
||||
// Остальные версии из очереди уже некуда ставить — их пути могли зависеть
|
||||
// от этой, да и пользователю нужен один внятный ответ.
|
||||
m_pendingInstalls.clear();
|
||||
m_inheritDepth = 0;
|
||||
for (const auto &context : std::as_const(m_active)) {
|
||||
@@ -799,7 +745,6 @@ void VersionInstaller::failInstall(const QString &message)
|
||||
|
||||
void VersionInstaller::abandonCurrent()
|
||||
{
|
||||
// Версия уступает очередь родителю: сигналов нет, установка продолжается.
|
||||
m_queue.clear();
|
||||
m_active.clear();
|
||||
m_versionId.clear();
|
||||
@@ -817,14 +762,11 @@ void VersionInstaller::cancel()
|
||||
m_inheritDepth = 0;
|
||||
m_canceling = true;
|
||||
|
||||
// abort() может доставить finished синхронно, а обработчик стирает элемент
|
||||
// из m_active — поэтому обходим копию.
|
||||
const auto active = m_active;
|
||||
for (const auto &context : active) {
|
||||
if (context->reply)
|
||||
context->reply->abort();
|
||||
}
|
||||
// abort() мог довести отмену до конца прямо здесь — тогда m_canceling уже снят.
|
||||
if (m_canceling && m_active.isEmpty())
|
||||
finalizeCancel();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user