Files
minecraft-launcher/versioninstaller.cpp

839 lines
30 KiB
C++

#include "versioninstaller.h"
#include "localization.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, Loc::text("version.error.invalidName").arg(id));
return;
}
if (m_canceling) {
emit failed(id, Loc::text("version.error.cancelPending"));
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(Loc::text("version.progress.fetchingList"));
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() ? Loc::text("version.error.listUnavailable") : 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(Loc::text("version.error.notInManifest")
.arg(id));
});
}
void VersionInstaller::fetchVersionJson(const QUrl &url, const QString &expectedSha1)
{
setStage(Loc::text("version.progress.description"));
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(Loc::text("version.error.descriptionFetchFailed").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(Loc::text("version.error.descriptionChecksum").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(Loc::text("version.error.descriptionUnparsable").arg(id));
return;
}
if (!alreadySaved) {
const QString directory = m_gameDir + QStringLiteral("/versions/") + id;
if (!QDir().mkpath(directory)) {
failInstall(Loc::text("common.error.createFailed").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(Loc::text("common.error.versionSaveFailed").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(Loc::text("common.error.inheritsChainTooLong").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 << Loc::text("version.warning.skippedBadUrl").arg(task.label);
return;
}
m_bytesTotal += task.size;
m_queue.enqueue(std::move(task));
}
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)
: 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 << Loc::text("version.error.libraryNoUrl").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 = Loc::text("version.progress.assetIndex");
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(Loc::text("version.progress.gameFiles"));
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(Loc::text("common.error.createFailed").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(Loc::text("common.error.openForWriteFailed").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(Loc::text("common.error.downloadFailed").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, Loc::text("common.error.checksumMismatch")))
return;
pump();
return;
}
}
if (!context->file->commit()) {
failInstall(Loc::text("common.error.saveFailed").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(Loc::text("common.error.downloadFailed").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(Loc::text("version.error.assetIndexReadFailed")
.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(Loc::text("version.progress.assets"));
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 << Loc::text("version.warning.skippedBadHash").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(Loc::text("version.progress.preparingAssets"));
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 << Loc::text("version.error.assetPrepareFailed").arg(target.fileName());
}
m_currentLabel = Loc::text("version.progress.countOf").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(Loc::text("version.error.filesMissingAfterDownload")
.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);
}