Files
BiLauncher/versioninstaller.cpp
T

839 lines
30 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "versioninstaller.h"
#include "gamelauncher.h"
#include "versionmanifestservice.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSaveFile>
#include <QTimer>
#include <algorithm>
#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)
return false;
for (const QChar c : value) {
if (!((c >= u'0' && c <= u'9') || (c >= u'a' && c <= u'f') || (c >= u'A' && c <= u'F')))
return false;
}
return true;
}
// Относительный путь из json нельзя подставлять как есть: «../..» увёл бы
// запись за пределы .minecraft.
bool isSafeRelativePath(const QString &path)
{
if (path.isEmpty() || path.startsWith(u'/') || path.contains(u'\\') || path.contains(u':'))
return false;
const QStringList parts = path.split(u'/');
for (const QString &part : parts) {
if (part.isEmpty() || part == QLatin1String(".") || part == QLatin1String(".."))
return false;
}
return true;
}
// Одиночный сегмент: id версии, id индекса ресурсов, имя конфига логирования.
bool isSafeRelativeSegment(const QString &value)
{
return !value.contains(u'/') && isSafeRelativePath(value);
}
QString versionJsonPath(const QString &gameDir, const QString &id)
{
return gameDir + QStringLiteral("/versions/") + id + u'/' + id + QStringLiteral(".json");
}
bool isTransient(QNetworkReply::NetworkError error, int httpStatus)
{
if (httpStatus == 429 || httpStatus >= 500)
return true;
switch (error) {
case QNetworkReply::RemoteHostClosedError:
case QNetworkReply::TimeoutError:
case QNetworkReply::TemporaryNetworkFailureError:
case QNetworkReply::NetworkSessionFailedError:
case QNetworkReply::UnknownNetworkError:
case QNetworkReply::ProxyTimeoutError:
case QNetworkReply::ContentReSendError:
return true;
default:
return false;
}
}
QString fileNameOf(const QString &path)
{
return QFileInfo(path).fileName();
}
} // namespace
VersionInstaller::VersionInstaller(VersionManifestService *manifest, QObject *parent)
: QObject(parent)
, m_manifest(manifest)
, m_network(new QNetworkAccessManager(this))
, m_progressTimer(new QTimer(this))
, m_legacyTimer(new QTimer(this))
{
// Восемь параллельных ответов шлют downloadProgress на каждый кусок данных:
// без склейки привязки в QML пересчитывались бы сотни раз в секунду.
m_progressTimer->setInterval(100);
connect(m_progressTimer, &QTimer::timeout, this, &VersionInstaller::flushProgress);
m_legacyTimer->setInterval(0);
connect(m_legacyTimer, &QTimer::timeout, this, &VersionInstaller::legacyTick);
}
VersionInstaller::~VersionInstaller()
{
// Из деструктора сигналы уже некому слушать, а QSaveFile сам выбросит
// недописанные временные файлы.
blockSignals(true);
cancel();
}
// ── Прогресс ───────────────────────────────────────────────────────────────
qint64 VersionInstaller::bytesDone() const
{
qint64 total = m_bytesSettled;
for (const auto &context : m_active)
total += context->received;
return total;
}
double VersionInstaller::fraction() const
{
if (m_bytesTotal <= 0)
return -1.0;
const double value = double(bytesDone()) / double(m_bytesTotal);
return std::clamp(value, 0.0, 1.0);
}
void VersionInstaller::setStage(const QString &stage)
{
m_stage = stage;
scheduleProgress();
}
void VersionInstaller::scheduleProgress()
{
m_progressDirty = true;
if (!m_progressTimer->isActive())
m_progressTimer->start();
}
void VersionInstaller::flushProgress()
{
if (!m_progressDirty) {
if (!m_running)
m_progressTimer->stop();
return;
}
m_progressDirty = false;
emit progressChanged();
}
// ── Очередь установок ──────────────────────────────────────────────────────
bool VersionInstaller::isQueued(const QString &versionId) const
{
for (const auto &entry : m_pendingInstalls) {
if (entry.second == versionId)
return true;
}
return false;
}
void VersionInstaller::install(const QString &gameDir, const QString &versionId)
{
const QString id = versionId.trimmed();
if (id.isEmpty())
return;
if (!isSafeRelativeSegment(id)) {
emit failed(id, tr("Недопустимое имя версии «%1»").arg(id));
return;
}
if (m_canceling) {
emit failed(id, tr("Идёт отмена предыдущей загрузки — попробуйте ещё раз"));
return;
}
if (m_versionId == id || isQueued(id))
return;
m_pendingInstalls.enqueue({gameDir, id});
if (!m_running)
startNext();
}
void VersionInstaller::startNext()
{
if (m_pendingInstalls.isEmpty()) {
m_running = false;
m_versionId.clear();
m_stage.clear();
m_currentLabel.clear();
m_progressTimer->stop();
emit progressChanged();
return;
}
const auto entry = m_pendingInstalls.dequeue();
m_gameDir = entry.first;
m_versionId = entry.second;
m_versionJson = {};
m_version = {};
m_queue.clear();
m_warnings.clear();
m_assetObjects = {};
m_assetsVirtual = false;
m_assetsMapToResources = false;
m_legacyCopies.clear();
m_legacyIndex = 0;
m_bytesSettled = 0;
m_bytesTotal = 0;
m_assetsDeclaredTotal = 0;
const bool wasRunning = m_running;
m_running = true;
if (!wasRunning)
emit started(m_versionId);
setStage(tr("Получение списка версий"));
emit progressChanged();
resolveManifestEntry();
}
void VersionInstaller::resolveManifestEntry()
{
const QString id = m_versionId;
m_manifest->ensureLoaded([this, id](bool ok, const QString &warning) {
if (!m_running || m_canceling || m_versionId != id)
return;
if (!ok) {
QFile local(versionJsonPath(m_gameDir, id));
if (local.open(QIODevice::ReadOnly)) {
adoptVersionJson(local.readAll(), /*alreadySaved=*/true);
return;
}
failInstall(warning.isEmpty() ? tr("Список версий Mojang недоступен") : warning);
return;
}
const auto entry = m_manifest->find(id);
if (entry) {
fetchVersionJson(entry->url, entry->sha1);
return;
}
// Версии нет в манифесте, но описание могло уже лечь на диск: так
// ставятся сборки модлоадеров, чьи id Mojang не знает.
QFile local(versionJsonPath(m_gameDir, id));
if (local.open(QIODevice::ReadOnly)) {
adoptVersionJson(local.readAll(), /*alreadySaved=*/true);
return;
}
// Иначе это вручную вписанный id сборки — сообщение не должно
// выглядеть как сетевая ошибка.
failInstall(tr("Версия «%1» отсутствует в манифесте Mojang — "
"установить её автоматически нельзя")
.arg(id));
});
}
void VersionInstaller::fetchVersionJson(const QUrl &url, const QString &expectedSha1)
{
setStage(tr("Описание версии"));
m_currentLabel = m_versionId + QStringLiteral(".json");
QNetworkRequest request{url};
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
request.setTransferTimeout(kTransferTimeoutMs);
const QString id = m_versionId;
QNetworkReply *reply = m_network->get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply, expectedSha1, id] {
reply->deleteLater();
if (!m_running || m_canceling || m_versionId != id)
return;
if (reply->error() != QNetworkReply::NoError) {
failInstall(tr("Не удалось получить описание версии: %1").arg(reply->errorString()));
return;
}
const QByteArray payload = reply->readAll();
if (!expectedSha1.isEmpty()) {
const QString actual = QString::fromLatin1(
QCryptographicHash::hash(payload, QCryptographicHash::Sha1).toHex());
if (actual.compare(expectedSha1, Qt::CaseInsensitive) != 0) {
failInstall(tr("Контрольная сумма описания версии «%1» не совпала").arg(id));
return;
}
}
adoptVersionJson(payload, /*alreadySaved=*/false);
});
}
// Общий хвост для описания версии, откуда бы оно ни пришло: из сети или уже
// лежащим на диске (сборки модлоадеров кладут его туда сами).
void VersionInstaller::adoptVersionJson(const QByteArray &payload, bool alreadySaved)
{
const QString id = m_versionId;
m_versionJson = QJsonDocument::fromJson(payload).object();
if (m_versionJson.isEmpty()) {
failInstall(tr("Описание версии «%1» не удалось разобрать").arg(id));
return;
}
if (!alreadySaved) {
const QString directory = m_gameDir + QStringLiteral("/versions/") + id;
if (!QDir().mkpath(directory)) {
failInstall(tr("Не удалось создать %1").arg(QDir::toNativeSeparators(directory)));
return;
}
QSaveFile file(directory + u'/' + id + QStringLiteral(".json"));
if (!file.open(QIODevice::WriteOnly) || file.write(payload) != payload.size()
|| !file.commit()) {
file.cancelWriting();
failInstall(tr("Не удалось сохранить описание версии «%1»").arg(id));
return;
}
}
// Ванильные версии ничего не наследуют, но сборка могла бы — тогда
// сначала ставим родителя, а потом возвращаемся к этой версии.
const QString parentId = m_versionJson.value(QStringLiteral("inheritsFrom")).toString();
if (!parentId.isEmpty() && isSafeRelativeSegment(parentId)
&& !QFile::exists(versionJsonPath(m_gameDir, parentId))) {
if (m_inheritDepth >= kMaxInheritDepth) {
failInstall(tr("Слишком длинная цепочка inheritsFrom у версии «%1»").arg(id));
return;
}
++m_inheritDepth;
m_pendingInstalls.prepend({m_gameDir, id});
m_pendingInstalls.prepend({m_gameDir, parentId});
abandonCurrent();
return;
}
m_inheritDepth = 0;
if (prepareTasks())
pump();
}
// ── Составление списка файлов ──────────────────────────────────────────────
void VersionInstaller::enqueue(DownloadTask task)
{
if (task.url.scheme() != QLatin1String("https")) {
// Подменённое описание версии не должно уводить загрузку на чужой адрес.
m_warnings << tr("Пропущен %1: недопустимая ссылка").arg(task.label);
return;
}
m_bytesTotal += task.size;
m_queue.enqueue(std::move(task));
}
bool VersionInstaller::prepareTasks()
{
setStage(tr("Подготовка"));
QString error;
// features пустые намеренно: Mojang отбирает библиотеки только по ОС,
// а feature-правила живут в arguments, которые установщику не нужны.
m_version = VersionLoader::load(m_gameDir, m_versionId, {}, &error);
if (!m_version.isValid()) {
failInstall(error.isEmpty() ? tr("Не удалось прочитать версию «%1»").arg(m_versionId)
: error);
return false;
}
const QJsonObject client = m_versionJson.value(QStringLiteral("downloads"))
.toObject()
.value(QStringLiteral("client"))
.toObject();
if (!client.isEmpty()) {
DownloadTask task;
task.kind = DownloadTask::ClientJar;
task.url = QUrl(client.value(QStringLiteral("url")).toString());
task.path = m_version.clientJarPath;
task.sha1 = client.value(QStringLiteral("sha1")).toString();
task.size = client.value(QStringLiteral("size")).toInteger();
task.label = fileNameOf(task.path);
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 << tr("Нет ссылки на библиотеку %1").arg(library.name);
continue;
}
DownloadTask task;
task.kind = DownloadTask::Library;
task.url = QUrl(library.url);
task.path = m_gameDir + QStringLiteral("/libraries/") + library.path;
task.sha1 = library.sha1;
task.size = library.size;
task.label = library.name;
enqueue(std::move(task));
}
const QJsonObject assetIndex = m_versionJson.value(QStringLiteral("assetIndex")).toObject();
if (!assetIndex.isEmpty() && isSafeRelativeSegment(m_version.assetIndexId)) {
DownloadTask task;
task.kind = DownloadTask::AssetIndex;
task.url = QUrl(assetIndex.value(QStringLiteral("url")).toString());
task.path = m_gameDir + QStringLiteral("/assets/indexes/") + m_version.assetIndexId
+ QStringLiteral(".json");
task.sha1 = assetIndex.value(QStringLiteral("sha1")).toString();
task.size = assetIndex.value(QStringLiteral("size")).toInteger();
task.label = tr("индекс ресурсов");
enqueue(std::move(task));
// totalSize — суммарный вес всех ресурсов, известный ещё до скачивания
// индекса: без него полоса первые секунды стояла бы и потом прыгнула.
m_assetsDeclaredTotal = assetIndex.value(QStringLiteral("totalSize")).toInteger();
m_bytesTotal += m_assetsDeclaredTotal;
}
const QJsonObject logging = m_versionJson.value(QStringLiteral("logging"))
.toObject()
.value(QStringLiteral("client"))
.toObject()
.value(QStringLiteral("file"))
.toObject();
const QString loggingId = logging.value(QStringLiteral("id")).toString();
if (!logging.isEmpty() && isSafeRelativeSegment(loggingId)) {
DownloadTask task;
task.kind = DownloadTask::LoggingConfig;
task.url = QUrl(logging.value(QStringLiteral("url")).toString());
task.path = m_gameDir + QStringLiteral("/assets/log_configs/") + loggingId;
task.sha1 = logging.value(QStringLiteral("sha1")).toString();
task.size = logging.value(QStringLiteral("size")).toInteger();
task.label = loggingId;
enqueue(std::move(task));
}
setStage(tr("Файлы игры"));
return true;
}
// ── Движок загрузки ────────────────────────────────────────────────────────
void VersionInstaller::schedulePump()
{
if (m_pumpScheduled)
return;
m_pumpScheduled = true;
QTimer::singleShot(0, this, [this] {
m_pumpScheduled = false;
pump();
});
}
void VersionInstaller::pump()
{
if (!m_running || m_canceling)
return;
int skipped = 0;
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;
}
}
}
if (m_queue.isEmpty() && m_active.isEmpty())
afterDownloads();
}
void VersionInstaller::startTask(DownloadTask task)
{
const QFileInfo info(task.path);
if (info.exists() && !m_verifyExisting && (task.size == 0 || info.size() == task.size)) {
m_bytesSettled += task.size;
if (task.kind == DownloadTask::AssetIndex)
expandAssetIndex(task.path);
scheduleProgress();
return;
}
// Временный файл QSaveFile ложится рядом с целевым, поэтому папка нужна
// даже до открытия.
if (!QDir().mkpath(info.absolutePath())) {
failInstall(tr("Не удалось создать %1").arg(QDir::toNativeSeparators(info.absolutePath())));
return;
}
auto context = std::make_shared<ActiveDownload>();
context->task = task;
context->file = std::make_unique<QSaveFile>(task.path);
if (!context->file->open(QIODevice::WriteOnly)) {
failInstall(tr("Не удалось открыть на запись %1").arg(QDir::toNativeSeparators(task.path)));
return;
}
QNetworkRequest request{task.url};
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
// Без таймаута зависший сокет подвесил бы установку навсегда.
request.setTransferTimeout(kTransferTimeoutMs);
context->reply = m_network->get(request);
m_active.append(context);
m_currentLabel = task.label;
connect(context->reply, &QNetworkReply::readyRead, this, [this, context] {
const QByteArray chunk = context->reply->readAll();
if (chunk.isEmpty())
return;
context->hash.addData(chunk);
if (context->file->write(chunk) != chunk.size())
context->reply->abort();
});
connect(context->reply, &QNetworkReply::downloadProgress, this,
[this, context](qint64 received, qint64) {
context->received = received;
scheduleProgress();
});
connect(context->reply, &QNetworkReply::finished, this,
[this, context] { handleReplyFinished(context); });
scheduleProgress();
}
void VersionInstaller::handleReplyFinished(const std::shared_ptr<ActiveDownload> &context)
{
context->reply->deleteLater();
m_active.removeOne(context);
if (m_canceling) {
context->file->cancelWriting();
if (m_active.isEmpty())
finalizeCancel();
return;
}
if (!m_running)
return;
const int status =
context->reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (context->reply->error() != QNetworkReply::NoError) {
context->file->cancelWriting();
const QString reason = context->reply->errorString();
if (isTransient(context->reply->error(), status)) {
if (!retryOrFail(context->task, reason))
return;
} else {
failInstall(tr("Не удалось скачать %1: %2").arg(context->task.label, reason));
return;
}
pump();
return;
}
if (!context->task.sha1.isEmpty()) {
const QString actual = QString::fromLatin1(context->hash.result().toHex());
if (actual.compare(context->task.sha1, Qt::CaseInsensitive) != 0) {
context->file->cancelWriting();
if (!retryOrFail(context->task, tr("контрольная сумма не совпала")))
return;
pump();
return;
}
}
if (!context->file->commit()) {
failInstall(tr("Не удалось сохранить %1").arg(QDir::toNativeSeparators(context->task.path)));
return;
}
m_bytesSettled += context->received;
if (context->task.kind == DownloadTask::AssetIndex)
expandAssetIndex(context->task.path);
scheduleProgress();
pump();
}
// Возвращает true, если задача поставлена на повтор, и false, если установка
// уже провалена.
bool VersionInstaller::retryOrFail(DownloadTask task, const QString &reason)
{
if (task.attempts + 1 >= kMaxAttempts) {
failInstall(tr("Не удалось скачать %1: %2").arg(task.label, reason));
return false;
}
++task.attempts;
const int delay = 400 * task.attempts;
QTimer::singleShot(delay, this, [this, task] {
if (!m_running || m_canceling)
return;
// В начало очереди: иначе повтор ждал бы за тысячами ресурсов.
m_queue.prepend(task);
pump();
});
return true;
}
void VersionInstaller::expandAssetIndex(const QString &indexPath)
{
QFile file(indexPath);
if (!file.open(QIODevice::ReadOnly)) {
failInstall(tr("Не удалось прочитать индекс ресурсов %1")
.arg(QDir::toNativeSeparators(indexPath)));
return;
}
const QJsonObject root = QJsonDocument::fromJson(file.readAll()).object();
m_assetObjects = root.value(QStringLiteral("objects")).toObject();
m_assetsVirtual = root.value(QStringLiteral("virtual")).toBool();
m_assetsMapToResources = root.value(QStringLiteral("map_to_resources")).toBool();
setStage(tr("Ресурсы"));
qint64 actualTotal = 0;
for (auto it = m_assetObjects.constBegin(); it != m_assetObjects.constEnd(); ++it) {
const QJsonObject object = it.value().toObject();
const QString hash = object.value(QStringLiteral("hash")).toString();
if (!isSha1Hex(hash)) {
m_warnings << tr("Пропущен ресурс %1: некорректный хэш").arg(it.key());
continue;
}
const QString prefix = hash.left(2);
DownloadTask task;
task.kind = DownloadTask::AssetObject;
task.url = QUrl(kResourcesUrl + prefix + u'/' + hash);
task.path = m_gameDir + QStringLiteral("/assets/objects/") + prefix + u'/' + hash;
task.sha1 = hash;
task.size = object.value(QStringLiteral("size")).toInteger();
task.label = it.key();
actualTotal += task.size;
enqueue(std::move(task));
}
// enqueue уже прибавил вес ресурсов к итогу — снимаем заявленный заранее,
// чтобы он не учитывался дважды.
m_bytesTotal -= m_assetsDeclaredTotal;
m_assetsDeclaredTotal = actualTotal;
scheduleProgress();
}
// ── Завершение ─────────────────────────────────────────────────────────────
void VersionInstaller::afterDownloads()
{
const bool legacy = m_assetsVirtual || m_assetsMapToResources
|| m_version.assetsKind == QLatin1String("legacy")
|| m_version.assetsKind == QLatin1String("pre-1.6");
if (legacy && !m_assetObjects.isEmpty() && m_legacyCopies.isEmpty() && m_legacyIndex == 0) {
startLegacyCopy();
return;
}
complete();
}
void VersionInstaller::startLegacyCopy()
{
setStage(tr("Подготовка ресурсов"));
const QString targetRoot = m_assetsMapToResources
? m_gameDir + QStringLiteral("/resources/")
: m_gameDir + QStringLiteral("/assets/virtual/legacy/");
for (auto it = m_assetObjects.constBegin(); it != m_assetObjects.constEnd(); ++it) {
const QString name = it.key();
const QString hash = it.value().toObject().value(QStringLiteral("hash")).toString();
if (!isSha1Hex(hash) || !isSafeRelativePath(name))
continue;
m_legacyCopies.append({m_gameDir + QStringLiteral("/assets/objects/") + hash.left(2) + u'/'
+ hash,
targetRoot + name});
}
m_legacyIndex = 0;
if (m_legacyCopies.isEmpty()) {
complete();
return;
}
m_legacyTimer->start();
}
void VersionInstaller::legacyTick()
{
if (!m_running || m_canceling) {
m_legacyTimer->stop();
return;
}
const int end = std::min(m_legacyIndex + kLegacyCopiesPerTick, int(m_legacyCopies.size()));
for (; m_legacyIndex < end; ++m_legacyIndex) {
const auto &pair = m_legacyCopies.at(m_legacyIndex);
const QFileInfo source(pair.first);
const QFileInfo target(pair.second);
if (!source.exists())
continue;
if (target.exists() && target.size() == source.size())
continue;
QDir().mkpath(target.absolutePath());
QFile::remove(pair.second);
if (!QFile::copy(pair.first, pair.second))
m_warnings << tr("Не удалось подготовить ресурс %1").arg(target.fileName());
}
m_currentLabel = tr("%1 из %2").arg(m_legacyIndex).arg(m_legacyCopies.size());
scheduleProgress();
if (m_legacyIndex >= m_legacyCopies.size()) {
m_legacyTimer->stop();
complete();
}
}
void VersionInstaller::complete()
{
// Приёмка той же проверкой, которой пользуется запуск: успешная установка
// тогда по построению запускаема.
LaunchOptions probe;
probe.gameDir = m_gameDir;
probe.versionId = m_versionId;
const QStringList missing = GameLauncher::missingFiles(probe, m_version);
if (!missing.isEmpty()) {
failInstall(tr("После загрузки не хватает файлов (%1):\n%2")
.arg(missing.size())
.arg(missing.join(u'\n')));
return;
}
const QString id = m_versionId;
resetInstallState();
emit finished(id);
startNext();
}
void VersionInstaller::resetInstallState()
{
m_queue.clear();
m_active.clear();
m_assetObjects = {};
m_legacyCopies.clear();
m_legacyIndex = 0;
m_legacyTimer->stop();
m_versionId.clear();
m_stage.clear();
m_currentLabel.clear();
m_running = false;
m_progressDirty = false;
m_progressTimer->stop();
}
void VersionInstaller::failInstall(const QString &message)
{
const QString id = m_versionId;
QString text = 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)) {
if (context->reply)
context->reply->abort();
context->file->cancelWriting();
}
resetInstallState();
emit failed(id, text);
}
void VersionInstaller::abandonCurrent()
{
// Версия уступает очередь родителю: сигналов нет, установка продолжается.
m_queue.clear();
m_active.clear();
m_versionId.clear();
startNext();
}
void VersionInstaller::cancel()
{
if (!m_running)
return;
m_pendingInstalls.clear();
m_queue.clear();
m_legacyTimer->stop();
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();
}
void VersionInstaller::finalizeCancel()
{
const QString id = m_versionId;
m_canceling = false;
resetInstallState();
emit canceled(id);
}