This commit is contained in:
2026-09-29 09:40:37 +03:00
parent 441021b9a6
commit 6567e731d1
34 changed files with 57 additions and 709 deletions
+1 -59
View File
@@ -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();
}