784 lines
26 KiB
C++
784 lines
26 KiB
C++
|
|
#include "javainstaller.h"
|
||
|
|
|
||
|
|
#include "launcherpaths.h"
|
||
|
|
|
||
|
|
#include <QDir>
|
||
|
|
#include <QFile>
|
||
|
|
#include <QFileInfo>
|
||
|
|
#include <QJsonDocument>
|
||
|
|
#include <QJsonObject>
|
||
|
|
#include <QNetworkAccessManager>
|
||
|
|
#include <QNetworkReply>
|
||
|
|
#include <QNetworkRequest>
|
||
|
|
#include <QProcess>
|
||
|
|
#include <QSaveFile>
|
||
|
|
#include <QTimer>
|
||
|
|
#include <QtCore/private/qzipreader_p.h>
|
||
|
|
|
||
|
|
#include <utility>
|
||
|
|
|
||
|
|
// Читатель архива вместе с разобранным оглавлением: fileInfoList() каждый раз
|
||
|
|
// заново разбирает центральный каталог zip, а записей в JDK десятки тысяч.
|
||
|
|
struct ZipExtraction
|
||
|
|
{
|
||
|
|
explicit ZipExtraction(const QString &path)
|
||
|
|
: reader(path)
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
QZipReader reader;
|
||
|
|
QList<QZipReader::FileInfo> entries;
|
||
|
|
int index = 0;
|
||
|
|
};
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
constexpr int kTransferTimeoutMs = 30000;
|
||
|
|
constexpr int kMaxParallel = 4;
|
||
|
|
constexpr int kMaxAttempts = 3;
|
||
|
|
constexpr int kProgressIntervalMs = 100;
|
||
|
|
|
||
|
|
// Записей zip за один проход таймера. Сотня файлов по паре мегабайт — это
|
||
|
|
// заметная, но ещё не видимая глазом задержка кадра.
|
||
|
|
constexpr int kZipChunk = 24;
|
||
|
|
|
||
|
|
bool isTransient(QNetworkReply::NetworkError error, int status)
|
||
|
|
{
|
||
|
|
if (status >= 500)
|
||
|
|
return true;
|
||
|
|
switch (error) {
|
||
|
|
case QNetworkReply::TimeoutError:
|
||
|
|
case QNetworkReply::TemporaryNetworkFailureError:
|
||
|
|
case QNetworkReply::NetworkSessionFailedError:
|
||
|
|
case QNetworkReply::ProxyTimeoutError:
|
||
|
|
case QNetworkReply::UnknownNetworkError:
|
||
|
|
return true;
|
||
|
|
default:
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Путь из манифеста или архива не должен уводить за пределы папки сборки:
|
||
|
|
// «../../» в имени записи — известный способ подменить чужой файл.
|
||
|
|
bool isInside(const QString &root, const QString &path)
|
||
|
|
{
|
||
|
|
const QString normalizedRoot = QDir::cleanPath(root) + u'/';
|
||
|
|
const QString normalized = QDir::cleanPath(path);
|
||
|
|
return normalized.startsWith(normalizedRoot);
|
||
|
|
}
|
||
|
|
|
||
|
|
void makeExecutable(const QString &path)
|
||
|
|
{
|
||
|
|
const QFileDevice::Permissions current = QFile::permissions(path);
|
||
|
|
QFile::setPermissions(path,
|
||
|
|
current | QFileDevice::ExeOwner | QFileDevice::ExeGroup
|
||
|
|
| QFileDevice::ExeOther);
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
JavaInstaller::JavaInstaller(QObject *parent)
|
||
|
|
: QObject(parent)
|
||
|
|
, m_network(new QNetworkAccessManager(this))
|
||
|
|
, m_extractTimer(new QTimer(this))
|
||
|
|
, m_progressTimer(new QTimer(this))
|
||
|
|
{
|
||
|
|
m_extractTimer->setInterval(0);
|
||
|
|
connect(m_extractTimer, &QTimer::timeout, this, &JavaInstaller::extractZipChunk);
|
||
|
|
|
||
|
|
m_progressTimer->setInterval(kProgressIntervalMs);
|
||
|
|
connect(m_progressTimer, &QTimer::timeout, this, &JavaInstaller::flushProgress);
|
||
|
|
}
|
||
|
|
|
||
|
|
JavaInstaller::~JavaInstaller() = default;
|
||
|
|
|
||
|
|
qint64 JavaInstaller::bytesDone() const
|
||
|
|
{
|
||
|
|
qint64 total = m_bytesSettled;
|
||
|
|
for (const auto &context : m_active)
|
||
|
|
total += context->received;
|
||
|
|
return total;
|
||
|
|
}
|
||
|
|
|
||
|
|
double JavaInstaller::fraction() const
|
||
|
|
{
|
||
|
|
if (m_bytesTotal > 0)
|
||
|
|
return qBound(0.0, double(bytesDone()) / double(m_bytesTotal), 1.0);
|
||
|
|
if (m_stepsTotal > 0)
|
||
|
|
return qBound(0.0, double(m_stepsDone) / double(m_stepsTotal), 1.0);
|
||
|
|
return -1.0;
|
||
|
|
}
|
||
|
|
|
||
|
|
void JavaInstaller::install(const JavaRuntimeEntry &entry)
|
||
|
|
{
|
||
|
|
if (m_running)
|
||
|
|
return;
|
||
|
|
if (!entry.isValid()) {
|
||
|
|
emit failed(entry.version, tr("Сборка Java недоступна для загрузки"));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
QString error;
|
||
|
|
if (!LauncherPaths::ensureRootExists(&error)) {
|
||
|
|
emit failed(entry.version, tr("Папка лаунчера недоступна: %1").arg(error));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
m_entry = entry;
|
||
|
|
m_label = tr("%1 %2").arg(javaKindTitle(entry.kind), entry.version);
|
||
|
|
m_targetDir = JavaRuntimeStore::dirFor(entry.id);
|
||
|
|
if (m_targetDir.isEmpty()) {
|
||
|
|
emit failed(m_label, tr("Недопустимый идентификатор сборки Java"));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Переустановка поверх недокачанной прошлой попытки: старое дерево мешает
|
||
|
|
// и размером, и тем, что locateBinary нашла бы в нём чужой java.
|
||
|
|
if (!JavaRuntimeStore::remove(entry.id, &error)) {
|
||
|
|
emit failed(m_label, error);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!QDir().mkpath(m_targetDir)) {
|
||
|
|
emit failed(m_label, tr("Не удалось создать %1").arg(QDir::toNativeSeparators(m_targetDir)));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
m_running = true;
|
||
|
|
m_canceling = false;
|
||
|
|
m_bytesSettled = 0;
|
||
|
|
m_bytesTotal = 0;
|
||
|
|
m_stepsDone = 0;
|
||
|
|
m_stepsTotal = 0;
|
||
|
|
m_currentFile.clear();
|
||
|
|
m_links.clear();
|
||
|
|
m_queue.clear();
|
||
|
|
|
||
|
|
emit started(m_label);
|
||
|
|
if (entry.archive == QLatin1String("mojang"))
|
||
|
|
beginMojang();
|
||
|
|
else
|
||
|
|
beginArchive();
|
||
|
|
}
|
||
|
|
|
||
|
|
void JavaInstaller::cancel()
|
||
|
|
{
|
||
|
|
if (!m_running || m_canceling)
|
||
|
|
return;
|
||
|
|
|
||
|
|
m_canceling = true;
|
||
|
|
m_extractTimer->stop();
|
||
|
|
|
||
|
|
if (m_reply)
|
||
|
|
m_reply->abort();
|
||
|
|
for (const auto &context : std::as_const(m_active))
|
||
|
|
context->reply->abort();
|
||
|
|
if (m_tar)
|
||
|
|
m_tar->kill();
|
||
|
|
|
||
|
|
// Прерванная установка оставляет дерево наполовину собранным: запускать из
|
||
|
|
// него нечего, а locateBinary могла бы найти в нём java.
|
||
|
|
if (m_active.isEmpty() && !m_reply && !m_tar) {
|
||
|
|
const QString label = m_label;
|
||
|
|
JavaRuntimeStore::remove(m_entry.id, nullptr);
|
||
|
|
reset();
|
||
|
|
emit canceled(label);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Temurin: один архив ─────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
void JavaInstaller::beginArchive()
|
||
|
|
{
|
||
|
|
if (m_entry.archive != QLatin1String("zip")
|
||
|
|
&& m_entry.archive != QLatin1String("tar.gz")) {
|
||
|
|
fail(tr("Неизвестный формат архива: %1").arg(m_entry.archive));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const QString name = m_entry.url.fileName();
|
||
|
|
m_archivePath = LauncherPaths::javaDownloadDir() + u'/'
|
||
|
|
+ (name.isEmpty() ? m_entry.id + QStringLiteral(".archive") : name);
|
||
|
|
m_bytesTotal = m_entry.size;
|
||
|
|
setStage(tr("Загрузка Java %1").arg(m_entry.version));
|
||
|
|
downloadArchive();
|
||
|
|
}
|
||
|
|
|
||
|
|
void JavaInstaller::downloadArchive()
|
||
|
|
{
|
||
|
|
m_archiveFile = std::make_unique<QSaveFile>(m_archivePath);
|
||
|
|
if (!m_archiveFile->open(QIODevice::WriteOnly)) {
|
||
|
|
fail(tr("Не удалось открыть на запись %1").arg(QDir::toNativeSeparators(m_archivePath)));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
m_archiveHash.reset();
|
||
|
|
m_currentFile = QFileInfo(m_archivePath).fileName();
|
||
|
|
|
||
|
|
QNetworkRequest request{m_entry.url};
|
||
|
|
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
||
|
|
QNetworkRequest::NoLessSafeRedirectPolicy);
|
||
|
|
request.setTransferTimeout(kTransferTimeoutMs);
|
||
|
|
|
||
|
|
m_reply = m_network->get(request);
|
||
|
|
connect(m_reply, &QNetworkReply::readyRead, this, [this] {
|
||
|
|
const QByteArray chunk = m_reply->readAll();
|
||
|
|
if (chunk.isEmpty())
|
||
|
|
return;
|
||
|
|
m_archiveHash.addData(chunk);
|
||
|
|
if (m_archiveFile->write(chunk) != chunk.size())
|
||
|
|
m_reply->abort();
|
||
|
|
});
|
||
|
|
connect(m_reply, &QNetworkReply::downloadProgress, this,
|
||
|
|
[this](qint64 received, qint64 total) {
|
||
|
|
m_bytesSettled = received;
|
||
|
|
if (m_bytesTotal <= 0 && total > 0)
|
||
|
|
m_bytesTotal = total;
|
||
|
|
scheduleProgress();
|
||
|
|
});
|
||
|
|
connect(m_reply, &QNetworkReply::finished, this,
|
||
|
|
[this] { handleArchiveFinished(m_reply); });
|
||
|
|
|
||
|
|
scheduleProgress();
|
||
|
|
}
|
||
|
|
|
||
|
|
void JavaInstaller::handleArchiveFinished(QNetworkReply *reply)
|
||
|
|
{
|
||
|
|
reply->deleteLater();
|
||
|
|
m_reply = nullptr;
|
||
|
|
|
||
|
|
if (m_canceling) {
|
||
|
|
m_archiveFile->cancelWriting();
|
||
|
|
m_archiveFile.reset();
|
||
|
|
const QString label = m_label;
|
||
|
|
JavaRuntimeStore::remove(m_entry.id, nullptr);
|
||
|
|
reset();
|
||
|
|
emit canceled(label);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!m_running)
|
||
|
|
return;
|
||
|
|
|
||
|
|
if (reply->error() != QNetworkReply::NoError) {
|
||
|
|
m_archiveFile->cancelWriting();
|
||
|
|
m_archiveFile.reset();
|
||
|
|
fail(tr("Не удалось скачать Java %1: %2").arg(m_entry.version, reply->errorString()));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!m_entry.checksum.isEmpty()) {
|
||
|
|
const QString actual = QString::fromLatin1(m_archiveHash.result().toHex());
|
||
|
|
if (actual.compare(m_entry.checksum, Qt::CaseInsensitive) != 0) {
|
||
|
|
m_archiveFile->cancelWriting();
|
||
|
|
m_archiveFile.reset();
|
||
|
|
fail(tr("Контрольная сумма архива Java не совпала — загрузка повреждена"));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!m_archiveFile->commit()) {
|
||
|
|
m_archiveFile.reset();
|
||
|
|
fail(tr("Не удалось сохранить %1").arg(QDir::toNativeSeparators(m_archivePath)));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
m_archiveFile.reset();
|
||
|
|
|
||
|
|
startExtraction();
|
||
|
|
}
|
||
|
|
|
||
|
|
void JavaInstaller::startExtraction()
|
||
|
|
{
|
||
|
|
// Байты кончились, дальше считаем шаги: панель по нулевому итогу перестанет
|
||
|
|
// показывать мегабайты, а проценты продолжит.
|
||
|
|
m_bytesTotal = 0;
|
||
|
|
m_bytesSettled = 0;
|
||
|
|
setStage(tr("Распаковка Java %1").arg(m_entry.version));
|
||
|
|
|
||
|
|
if (m_entry.archive == QLatin1String("tar.gz")) {
|
||
|
|
startTarExtraction();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
m_zip = std::make_unique<ZipExtraction>(m_archivePath);
|
||
|
|
if (!m_zip->reader.isReadable()) {
|
||
|
|
m_zip.reset();
|
||
|
|
fail(tr("Не удалось прочитать архив %1").arg(QDir::toNativeSeparators(m_archivePath)));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
m_zip->entries = m_zip->reader.fileInfoList();
|
||
|
|
m_stepsDone = 0;
|
||
|
|
m_stepsTotal = m_zip->entries.size();
|
||
|
|
m_extractTimer->start();
|
||
|
|
}
|
||
|
|
|
||
|
|
void JavaInstaller::extractZipChunk()
|
||
|
|
{
|
||
|
|
if (m_canceling || !m_running || !m_zip) {
|
||
|
|
m_extractTimer->stop();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const QList<QZipReader::FileInfo> &entries = m_zip->entries;
|
||
|
|
const int end = qMin(m_zip->index + kZipChunk, entries.size());
|
||
|
|
for (; m_zip->index < end; ++m_zip->index) {
|
||
|
|
const QZipReader::FileInfo &info = entries.at(m_zip->index);
|
||
|
|
const QString target = m_targetDir + u'/' + info.filePath;
|
||
|
|
if (!isInside(m_targetDir, target)) {
|
||
|
|
m_extractTimer->stop();
|
||
|
|
fail(tr("Архив Java содержит недопустимый путь: %1").arg(info.filePath));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (info.isDir) {
|
||
|
|
QDir().mkpath(target);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
// Ссылок в zip-сборках Temurin не бывает — под Unix они отдают tar.gz.
|
||
|
|
if (!info.isFile)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
if (!QDir().mkpath(QFileInfo(target).absolutePath())) {
|
||
|
|
m_extractTimer->stop();
|
||
|
|
fail(tr("Не удалось создать %1")
|
||
|
|
.arg(QDir::toNativeSeparators(QFileInfo(target).absolutePath())));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const QByteArray data = m_zip->reader.fileData(info.filePath);
|
||
|
|
QFile file(target);
|
||
|
|
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size()) {
|
||
|
|
m_extractTimer->stop();
|
||
|
|
fail(tr("Не удалось распаковать %1").arg(info.filePath));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
file.close();
|
||
|
|
if (info.permissions != QFileDevice::Permissions())
|
||
|
|
file.setPermissions(info.permissions);
|
||
|
|
}
|
||
|
|
|
||
|
|
m_stepsDone = m_zip->index;
|
||
|
|
m_currentFile = m_zip->index > 0 ? entries.at(m_zip->index - 1).filePath : QString();
|
||
|
|
scheduleProgress();
|
||
|
|
|
||
|
|
if (m_zip->index >= entries.size()) {
|
||
|
|
m_extractTimer->stop();
|
||
|
|
m_zip.reset();
|
||
|
|
complete();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void JavaInstaller::startTarExtraction()
|
||
|
|
{
|
||
|
|
// tar есть в любой macOS и практически в любом дистрибутиве Linux; своего
|
||
|
|
// распаковщика gzip-tar в Qt нет, а тянуть ради него зависимость незачем.
|
||
|
|
m_tar = new QProcess(this);
|
||
|
|
m_tar->setWorkingDirectory(m_targetDir);
|
||
|
|
m_tar->setProgram(QStringLiteral("tar"));
|
||
|
|
m_tar->setArguments({QStringLiteral("-xzf"), m_archivePath,
|
||
|
|
QStringLiteral("-C"), m_targetDir});
|
||
|
|
|
||
|
|
connect(m_tar, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) {
|
||
|
|
if (!m_tar)
|
||
|
|
return;
|
||
|
|
const QString message = m_tar->errorString();
|
||
|
|
m_tar->deleteLater();
|
||
|
|
m_tar = nullptr;
|
||
|
|
if (m_canceling) {
|
||
|
|
const QString label = m_label;
|
||
|
|
JavaRuntimeStore::remove(m_entry.id, nullptr);
|
||
|
|
reset();
|
||
|
|
emit canceled(label);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
fail(tr("Не удалось распаковать архив Java: %1").arg(message));
|
||
|
|
});
|
||
|
|
|
||
|
|
connect(m_tar, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus status) {
|
||
|
|
if (!m_tar)
|
||
|
|
return;
|
||
|
|
const QByteArray errorOutput = m_tar->readAllStandardError();
|
||
|
|
m_tar->deleteLater();
|
||
|
|
m_tar = nullptr;
|
||
|
|
|
||
|
|
if (m_canceling) {
|
||
|
|
const QString label = m_label;
|
||
|
|
JavaRuntimeStore::remove(m_entry.id, nullptr);
|
||
|
|
reset();
|
||
|
|
emit canceled(label);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (status != QProcess::NormalExit || exitCode != 0) {
|
||
|
|
fail(tr("Распаковка архива Java не удалась: %1")
|
||
|
|
.arg(QString::fromLocal8Bit(errorOutput).trimmed()));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
complete();
|
||
|
|
});
|
||
|
|
|
||
|
|
m_tar->start();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Mojang: дерево файлов ───────────────────────────────────────────────────
|
||
|
|
|
||
|
|
void JavaInstaller::beginMojang()
|
||
|
|
{
|
||
|
|
setStage(tr("Список файлов Java %1").arg(m_entry.version));
|
||
|
|
m_currentFile = QStringLiteral("manifest.json");
|
||
|
|
|
||
|
|
QNetworkRequest request{m_entry.url};
|
||
|
|
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
||
|
|
QNetworkRequest::NoLessSafeRedirectPolicy);
|
||
|
|
request.setTransferTimeout(kTransferTimeoutMs);
|
||
|
|
|
||
|
|
m_reply = m_network->get(request);
|
||
|
|
connect(m_reply, &QNetworkReply::finished, this, [this] {
|
||
|
|
QNetworkReply *reply = m_reply;
|
||
|
|
reply->deleteLater();
|
||
|
|
m_reply = nullptr;
|
||
|
|
|
||
|
|
if (m_canceling) {
|
||
|
|
const QString label = m_label;
|
||
|
|
JavaRuntimeStore::remove(m_entry.id, nullptr);
|
||
|
|
reset();
|
||
|
|
emit canceled(label);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!m_running)
|
||
|
|
return;
|
||
|
|
if (reply->error() != QNetworkReply::NoError) {
|
||
|
|
fail(tr("Не удалось получить состав рантайма: %1").arg(reply->errorString()));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
handleManifest(reply->readAll());
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
void JavaInstaller::handleManifest(const QByteArray &payload)
|
||
|
|
{
|
||
|
|
const QJsonObject files = QJsonDocument::fromJson(payload)
|
||
|
|
.object()
|
||
|
|
.value(QStringLiteral("files"))
|
||
|
|
.toObject();
|
||
|
|
if (files.isEmpty()) {
|
||
|
|
fail(tr("Состав рантайма Java не удалось разобрать"));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!prepareTree(files))
|
||
|
|
return;
|
||
|
|
|
||
|
|
setStage(tr("Загрузка Java %1").arg(m_entry.version));
|
||
|
|
if (m_queue.isEmpty()) {
|
||
|
|
if (applyLinks())
|
||
|
|
complete();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
pump();
|
||
|
|
}
|
||
|
|
|
||
|
|
bool JavaInstaller::prepareTree(const QJsonObject &files)
|
||
|
|
{
|
||
|
|
m_bytesTotal = 0;
|
||
|
|
|
||
|
|
for (auto it = files.constBegin(); it != files.constEnd(); ++it) {
|
||
|
|
const QString target = m_targetDir + u'/' + it.key();
|
||
|
|
if (!isInside(m_targetDir, target)) {
|
||
|
|
fail(tr("Состав рантайма содержит недопустимый путь: %1").arg(it.key()));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const QJsonObject node = it.value().toObject();
|
||
|
|
const QString type = node.value(QStringLiteral("type")).toString();
|
||
|
|
|
||
|
|
if (type == QLatin1String("directory")) {
|
||
|
|
if (!QDir().mkpath(target)) {
|
||
|
|
fail(tr("Не удалось создать %1").arg(QDir::toNativeSeparators(target)));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (type == QLatin1String("link")) {
|
||
|
|
// Ссылки создаём в конце: их цели ещё не скачаны.
|
||
|
|
m_links << qMakePair(target, node.value(QStringLiteral("target")).toString());
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (type != QLatin1String("file"))
|
||
|
|
continue;
|
||
|
|
|
||
|
|
const QJsonObject raw = node.value(QStringLiteral("downloads"))
|
||
|
|
.toObject()
|
||
|
|
.value(QStringLiteral("raw"))
|
||
|
|
.toObject();
|
||
|
|
const QUrl url(raw.value(QStringLiteral("url")).toString());
|
||
|
|
if (url.scheme() != QLatin1String("https"))
|
||
|
|
continue;
|
||
|
|
|
||
|
|
JavaFileTask task;
|
||
|
|
task.url = url;
|
||
|
|
task.path = target;
|
||
|
|
task.sha1 = raw.value(QStringLiteral("sha1")).toString();
|
||
|
|
task.size = static_cast<qint64>(raw.value(QStringLiteral("size")).toDouble());
|
||
|
|
task.executable = node.value(QStringLiteral("executable")).toBool();
|
||
|
|
|
||
|
|
m_bytesTotal += task.size;
|
||
|
|
m_queue.enqueue(task);
|
||
|
|
}
|
||
|
|
|
||
|
|
m_stepsTotal = m_queue.size();
|
||
|
|
m_stepsDone = 0;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void JavaInstaller::pump()
|
||
|
|
{
|
||
|
|
if (!m_running || m_canceling)
|
||
|
|
return;
|
||
|
|
|
||
|
|
while (m_active.size() < kMaxParallel && !m_queue.isEmpty())
|
||
|
|
startTask(m_queue.dequeue());
|
||
|
|
|
||
|
|
if (m_active.isEmpty() && m_queue.isEmpty()) {
|
||
|
|
if (applyLinks())
|
||
|
|
complete();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void JavaInstaller::startTask(JavaFileTask task)
|
||
|
|
{
|
||
|
|
const QFileInfo info(task.path);
|
||
|
|
if (!QDir().mkpath(info.absolutePath())) {
|
||
|
|
fail(tr("Не удалось создать %1").arg(QDir::toNativeSeparators(info.absolutePath())));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
auto context = std::make_shared<JavaActiveDownload>();
|
||
|
|
context->task = task;
|
||
|
|
context->file = std::make_unique<QSaveFile>(task.path);
|
||
|
|
if (!context->file->open(QIODevice::WriteOnly)) {
|
||
|
|
fail(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_currentFile = QDir(m_targetDir).relativeFilePath(task.path);
|
||
|
|
|
||
|
|
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] { handleFileFinished(context); });
|
||
|
|
|
||
|
|
scheduleProgress();
|
||
|
|
}
|
||
|
|
|
||
|
|
void JavaInstaller::handleFileFinished(const std::shared_ptr<JavaActiveDownload> &context)
|
||
|
|
{
|
||
|
|
context->reply->deleteLater();
|
||
|
|
m_active.removeOne(context);
|
||
|
|
|
||
|
|
if (m_canceling) {
|
||
|
|
context->file->cancelWriting();
|
||
|
|
if (m_active.isEmpty()) {
|
||
|
|
const QString label = m_label;
|
||
|
|
JavaRuntimeStore::remove(m_entry.id, nullptr);
|
||
|
|
reset();
|
||
|
|
emit canceled(label);
|
||
|
|
}
|
||
|
|
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 {
|
||
|
|
fail(tr("Не удалось скачать %1: %2")
|
||
|
|
.arg(QDir(m_targetDir).relativeFilePath(context->task.path), 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()) {
|
||
|
|
fail(tr("Не удалось сохранить %1").arg(QDir::toNativeSeparators(context->task.path)));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (context->task.executable)
|
||
|
|
makeExecutable(context->task.path);
|
||
|
|
|
||
|
|
m_bytesSettled += context->received;
|
||
|
|
++m_stepsDone;
|
||
|
|
scheduleProgress();
|
||
|
|
pump();
|
||
|
|
}
|
||
|
|
|
||
|
|
bool JavaInstaller::retryOrFail(JavaFileTask task, const QString &reason)
|
||
|
|
{
|
||
|
|
if (++task.attempts >= kMaxAttempts) {
|
||
|
|
fail(tr("Не удалось скачать %1: %2")
|
||
|
|
.arg(QDir(m_targetDir).relativeFilePath(task.path), reason));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
m_queue.enqueue(task);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool JavaInstaller::applyLinks()
|
||
|
|
{
|
||
|
|
for (const auto &link : std::as_const(m_links)) {
|
||
|
|
const QString linkPath = link.first;
|
||
|
|
if (QFileInfo::exists(linkPath))
|
||
|
|
continue;
|
||
|
|
if (!QDir().mkpath(QFileInfo(linkPath).absolutePath()))
|
||
|
|
continue;
|
||
|
|
|
||
|
|
// Цель относительна самой ссылке — QFile::link кладёт её как есть.
|
||
|
|
if (!QFile::link(link.second, linkPath)) {
|
||
|
|
// Под Windows символических ссылок в рантаймах Mojang нет, но если
|
||
|
|
// они появятся, копия работает не хуже.
|
||
|
|
const QString source =
|
||
|
|
QDir::cleanPath(QFileInfo(linkPath).absolutePath() + u'/' + link.second);
|
||
|
|
if (!QFile::copy(source, linkPath)) {
|
||
|
|
fail(tr("Не удалось создать ссылку %1")
|
||
|
|
.arg(QDir(m_targetDir).relativeFilePath(linkPath)));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Завершение ──────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
void JavaInstaller::complete()
|
||
|
|
{
|
||
|
|
setStage(tr("Проверка сборки Java"));
|
||
|
|
|
||
|
|
const QString javaPath = JavaRuntimeStore::locateBinary(m_targetDir);
|
||
|
|
if (javaPath.isEmpty()) {
|
||
|
|
fail(tr("В скачанной сборке не нашлось исполняемого файла java"));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
#if !defined(Q_OS_WIN)
|
||
|
|
// tar сохраняет права, а вот zip и файлы Mojang без флага executable — нет.
|
||
|
|
makeExecutable(javaPath);
|
||
|
|
#endif
|
||
|
|
|
||
|
|
InstalledJavaRuntime runtime;
|
||
|
|
runtime.id = m_entry.id;
|
||
|
|
runtime.kind = javaKindKey(m_entry.kind);
|
||
|
|
runtime.major = m_entry.major;
|
||
|
|
runtime.version = m_entry.version;
|
||
|
|
runtime.javaPath = javaPath;
|
||
|
|
runtime.size = m_entry.size > 0 ? m_entry.size : m_bytesTotal;
|
||
|
|
|
||
|
|
QString error;
|
||
|
|
if (!JavaRuntimeStore::writeMeta(runtime, &error)) {
|
||
|
|
fail(error);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Архив нужен был только на распаковку — двести мегабайт в кэше ни к чему.
|
||
|
|
if (!m_archivePath.isEmpty())
|
||
|
|
QFile::remove(m_archivePath);
|
||
|
|
|
||
|
|
const QString id = m_entry.id;
|
||
|
|
reset();
|
||
|
|
emit finished(id, javaPath);
|
||
|
|
}
|
||
|
|
|
||
|
|
void JavaInstaller::fail(const QString &message)
|
||
|
|
{
|
||
|
|
const QString label = m_label;
|
||
|
|
m_extractTimer->stop();
|
||
|
|
m_zip.reset();
|
||
|
|
if (!m_archivePath.isEmpty())
|
||
|
|
QFile::remove(m_archivePath);
|
||
|
|
// Половина рантайма хуже, чем его отсутствие: следующая попытка начнётся с чистой папки.
|
||
|
|
JavaRuntimeStore::remove(m_entry.id, nullptr);
|
||
|
|
reset();
|
||
|
|
emit failed(label, message);
|
||
|
|
}
|
||
|
|
|
||
|
|
void JavaInstaller::reset()
|
||
|
|
{
|
||
|
|
m_running = false;
|
||
|
|
m_canceling = false;
|
||
|
|
m_progressTimer->stop();
|
||
|
|
m_extractTimer->stop();
|
||
|
|
m_zip.reset();
|
||
|
|
m_queue.clear();
|
||
|
|
m_active.clear();
|
||
|
|
m_links.clear();
|
||
|
|
m_archiveFile.reset();
|
||
|
|
m_archivePath.clear();
|
||
|
|
m_bytesSettled = 0;
|
||
|
|
m_bytesTotal = 0;
|
||
|
|
m_stepsDone = 0;
|
||
|
|
m_stepsTotal = 0;
|
||
|
|
m_stage.clear();
|
||
|
|
m_currentFile.clear();
|
||
|
|
m_entry = {};
|
||
|
|
m_label.clear();
|
||
|
|
emit progressChanged();
|
||
|
|
}
|
||
|
|
|
||
|
|
void JavaInstaller::setStage(const QString &stage)
|
||
|
|
{
|
||
|
|
if (m_stage == stage)
|
||
|
|
return;
|
||
|
|
m_stage = stage;
|
||
|
|
emit progressChanged();
|
||
|
|
}
|
||
|
|
|
||
|
|
void JavaInstaller::scheduleProgress()
|
||
|
|
{
|
||
|
|
m_progressDirty = true;
|
||
|
|
if (!m_progressTimer->isActive())
|
||
|
|
m_progressTimer->start();
|
||
|
|
}
|
||
|
|
|
||
|
|
void JavaInstaller::flushProgress()
|
||
|
|
{
|
||
|
|
if (!m_progressDirty) {
|
||
|
|
m_progressTimer->stop();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
m_progressDirty = false;
|
||
|
|
emit progressChanged();
|
||
|
|
}
|