Files
minecraft-launcher/javaruntimeservice.cpp
T
2026-08-31 00:59:14 +03:00

583 lines
23 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 "javaruntimeservice.h"
#include "launcherpaths.h"
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSaveFile>
#include <QSysInfo>
#include <QTimer>
#include <QUrlQuery>
#include <algorithm>
#include <utility>
namespace {
const QString kAdoptiumApi = QStringLiteral("https://api.adoptium.net/v3");
// Список компонентов рантайма Mojang. Ссылка зашита в официальный лаунчер и
// меняется вместе с ним; сами компоненты обновляются по этому же адресу.
const QString kMojangRuntimesUrl = QStringLiteral(
"https://piston-meta.mojang.com/v1/products/java-runtime/"
"2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json");
// Сборки Java выходят раз в квартал, а не раз в неделю: суток кэша достаточно,
// чтобы новая появилась в тот же день, и хватает на десяток открытий окна.
constexpr qint64 kCacheTtlSeconds = 24 * 60 * 60;
constexpr int kTransferTimeoutMs = 20000;
// Сколько последних сборок каждой мажорной версии показывать. Больше трёх
// смысла нет: старые патчи отличаются только исправлениями безопасности.
constexpr int kReleasesPerMajor = 3;
QString adoptiumOs()
{
#if defined(Q_OS_WIN)
return QStringLiteral("windows");
#elif defined(Q_OS_MACOS)
return QStringLiteral("mac");
#else
return QStringLiteral("linux");
#endif
}
QString adoptiumArch()
{
const QString arch = QSysInfo::currentCpuArchitecture();
if (arch == QLatin1String("arm64"))
return QStringLiteral("aarch64");
if (arch == QLatin1String("x86_64"))
return QStringLiteral("x64");
if (arch == QLatin1String("i386"))
return QStringLiteral("x86");
if (arch == QLatin1String("arm"))
return QStringLiteral("arm");
return arch;
}
// Ключ платформы в манифесте Mojang.
QString mojangPlatform()
{
const QString arch = QSysInfo::currentCpuArchitecture();
#if defined(Q_OS_WIN)
if (arch == QLatin1String("arm64"))
return QStringLiteral("windows-arm64");
return arch == QLatin1String("i386") ? QStringLiteral("windows-x86")
: QStringLiteral("windows-x64");
#elif defined(Q_OS_MACOS)
return arch == QLatin1String("arm64") ? QStringLiteral("mac-os-arm64")
: QStringLiteral("mac-os");
#else
return arch == QLatin1String("i386") ? QStringLiteral("linux-i386")
: QStringLiteral("linux");
#endif
}
QNetworkRequest makeRequest(const QUrl &url)
{
QNetworkRequest request{url};
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
request.setTransferTimeout(kTransferTimeoutMs);
return request;
}
// «1.8.0_504-b01» никто не узнаёт, а «8u504» — общепринятое имя той же сборки.
QString displayVersion(const QJsonObject &versionData)
{
const int major = versionData.value(QStringLiteral("major")).toInt();
if (major == 8) {
return QStringLiteral("8u%1").arg(versionData.value(QStringLiteral("security")).toInt());
}
QString openjdk = versionData.value(QStringLiteral("openjdk_version")).toString();
if (openjdk.endsWith(QLatin1String("-LTS")))
openjdk.chop(4);
if (!openjdk.isEmpty())
return openjdk;
return versionData.value(QStringLiteral("semver")).toString();
}
QString archiveKind(const QString &fileName)
{
if (fileName.endsWith(QLatin1String(".zip"), Qt::CaseInsensitive))
return QStringLiteral("zip");
if (fileName.endsWith(QLatin1String(".tar.gz"), Qt::CaseInsensitive))
return QStringLiteral("tar.gz");
return {};
}
} // namespace
JavaRuntimeService::JavaRuntimeService(QObject *parent)
: QObject(parent)
, m_network(new QNetworkAccessManager(this))
{
}
QList<JavaRuntimeEntry> JavaRuntimeService::entries(JavaRuntimeKind kind) const
{
QList<JavaRuntimeEntry> result;
for (const JavaRuntimeEntry &entry : m_entries) {
if (entry.kind == kind)
result << entry;
}
return result;
}
std::optional<JavaRuntimeEntry> JavaRuntimeService::bestFor(int major) const
{
if (major <= 0)
return std::nullopt;
// Каталог уже отфильтрован по текущей ОС и архитектуре, поэтому здесь
// остаётся выбрать из заведомо запускаемого.
const auto rank = [](JavaRuntimeKind kind) {
switch (kind) {
case JavaRuntimeKind::Jdk:
return 0;
case JavaRuntimeKind::Mojang:
return 1;
case JavaRuntimeKind::Jre:
return 2;
}
return 3;
};
const JavaRuntimeEntry *best = nullptr;
for (const JavaRuntimeEntry &entry : m_entries) {
if (!entry.isValid() || entry.major < major)
continue;
if (!best) {
best = &entry;
continue;
}
// Чем ближе мажорная версия к нужной, тем меньше шансов на сюрприз:
// игра на Java 8 под Java 21 просто не стартует.
if (entry.major != best->major) {
if (entry.major < best->major)
best = &entry;
continue;
}
if (rank(entry.kind) != rank(best->kind)) {
if (rank(entry.kind) < rank(best->kind))
best = &entry;
continue;
}
if (entry.released.isValid() && best->released.isValid()
&& entry.released != best->released) {
if (entry.released > best->released)
best = &entry;
continue;
}
if (entry.version > best->version)
best = &entry;
}
if (!best)
return std::nullopt;
return *best;
}
std::optional<JavaRuntimeEntry> JavaRuntimeService::find(const QString &id) const
{
const auto it = m_indexById.constFind(id);
if (it == m_indexById.constEnd())
return std::nullopt;
return m_entries.at(it.value());
}
void JavaRuntimeService::ensureLoaded(Callback callback, bool forceRefresh)
{
if (!m_cacheRead) {
m_cacheRead = true;
if (readCache())
emit catalogChanged();
}
const bool fresh = hasData() && m_fetchedAt.isValid()
&& m_fetchedAt.secsTo(QDateTime::currentDateTimeUtc()) < kCacheTtlSeconds;
if (!forceRefresh && fresh) {
// Колбэк, который то синхронный, то нет, ломает вызывающий код —
// на попадании в кэш отвечаем через событийный цикл.
if (callback)
QTimer::singleShot(0, this, [callback = std::move(callback)] { callback(true, {}); });
return;
}
if (callback)
m_pending << std::move(callback);
if (isRefreshing())
return;
startRefresh();
}
void JavaRuntimeService::startRefresh()
{
m_incoming.clear();
m_warnings.clear();
m_ltsVersions.clear();
// Счётчик поднимаем на оба стартовых запроса разом: иначе ответ первого
// успел бы обнулить его и завершить обновление раньше времени.
m_outstanding = 2;
emit refreshingChanged();
requestAdoptiumReleases();
requestMojangRuntimes();
}
void JavaRuntimeService::requestAdoptiumReleases()
{
QNetworkReply *reply = m_network->get(
makeRequest(QUrl(kAdoptiumApi + QStringLiteral("/info/available_releases"))));
connect(reply, &QNetworkReply::finished, this, [this, reply] {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
requestFinished(tr("Не удалось получить список версий Java: %1")
.arg(reply->errorString()));
return;
}
const QJsonObject root = QJsonDocument::fromJson(reply->readAll()).object();
const QJsonArray lts = root.value(QStringLiteral("available_lts_releases")).toArray();
for (const QJsonValue &value : lts)
m_ltsVersions.insert(value.toInt());
const QJsonArray available = root.value(QStringLiteral("available_releases")).toArray();
QList<int> majors;
for (const QJsonValue &value : available) {
const int major = value.toInt();
if (major > 0)
majors << major;
}
if (majors.isEmpty()) {
requestFinished(tr("Список версий Java не удалось разобрать"));
return;
}
// Запросы добавляем до того, как отчитается этот: иначе счётчик уйдёт
// в ноль между двумя строчками.
m_outstanding += majors.size();
const QString arch = adoptiumArch();
for (const int major : std::as_const(majors))
requestFeatureRelease(major, arch, arch == QLatin1String("aarch64"));
requestFinished({});
});
}
void JavaRuntimeService::requestFeatureRelease(int major,
const QString &architecture,
bool allowFallback)
{
QUrl url(kAdoptiumApi + QStringLiteral("/assets/feature_releases/%1/ga").arg(major));
QUrlQuery query;
query.addQueryItem(QStringLiteral("architecture"), architecture);
query.addQueryItem(QStringLiteral("heap_size"), QStringLiteral("normal"));
query.addQueryItem(QStringLiteral("jvm_impl"), QStringLiteral("hotspot"));
query.addQueryItem(QStringLiteral("os"), adoptiumOs());
query.addQueryItem(QStringLiteral("page"), QStringLiteral("0"));
query.addQueryItem(QStringLiteral("page_size"), QString::number(kReleasesPerMajor));
query.addQueryItem(QStringLiteral("project"), QStringLiteral("jdk"));
query.addQueryItem(QStringLiteral("sort_method"), QStringLiteral("DEFAULT"));
query.addQueryItem(QStringLiteral("sort_order"), QStringLiteral("DESC"));
query.addQueryItem(QStringLiteral("vendor"), QStringLiteral("eclipse"));
url.setQuery(query);
QNetworkReply *reply = m_network->get(makeRequest(url));
connect(reply, &QNetworkReply::finished, this, [this, reply, major, allowFallback] {
reply->deleteLater();
// 404 значит «под эту платформу такой сборки нет» — это не ошибка сети,
// а нормальный ответ: Java 8 и 16 под Apple Silicon просто не собирали.
const int status =
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QList<JavaRuntimeEntry> parsed;
if (reply->error() == QNetworkReply::NoError)
parsed = parseAdoptium(reply->readAll(), m_ltsVersions);
if (parsed.isEmpty() && allowFallback && (status == 404 || status == 200)) {
// На Apple Silicon старые сборки берём под x64: Rosetta их запускает.
m_outstanding += 1;
requestFeatureRelease(major, QStringLiteral("x64"), false);
requestFinished({});
return;
}
QString warning;
if (reply->error() != QNetworkReply::NoError && status != 404) {
warning = tr("Не удалось получить сборки Java %1: %2")
.arg(major)
.arg(reply->errorString());
}
m_incoming += parsed;
requestFinished(warning);
});
}
void JavaRuntimeService::requestMojangRuntimes()
{
QNetworkReply *reply = m_network->get(makeRequest(QUrl(kMojangRuntimesUrl)));
connect(reply, &QNetworkReply::finished, this, [this, reply] {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
requestFinished(tr("Не удалось получить рантаймы Mojang: %1")
.arg(reply->errorString()));
return;
}
m_incoming += parseMojang(reply->readAll());
requestFinished({});
});
}
void JavaRuntimeService::requestFinished(const QString &warning)
{
if (!warning.isEmpty())
m_warnings << warning;
if (--m_outstanding > 0)
return;
finishRefresh();
}
void JavaRuntimeService::finishRefresh()
{
m_outstanding = 0;
const QString warning = m_warnings.join(QStringLiteral("; "));
// Пустой разбор — испорченные ответы; хороший кэш ими не затираем.
const bool ok = !m_incoming.isEmpty();
if (ok) {
applyEntries(std::exchange(m_incoming, {}));
m_fetchedAt = QDateTime::currentDateTimeUtc();
writeCache();
emit catalogChanged();
}
m_incoming.clear();
m_warnings.clear();
emit refreshingChanged();
// Колбэк может снова позвать ensureLoaded — список забираем себе заранее.
const QList<Callback> pending = std::exchange(m_pending, {});
for (const Callback &callback : pending) {
// Сеть подвела, но старые данные есть — работаем на них.
if (!ok && hasData())
callback(true, warning.isEmpty() ? tr("Список сборок Java взят из кэша") : warning);
else
callback(ok, warning);
}
}
QList<JavaRuntimeEntry> JavaRuntimeService::parseAdoptium(const QByteArray &payload,
const QSet<int> &ltsVersions)
{
QList<JavaRuntimeEntry> result;
const QJsonArray releases = QJsonDocument::fromJson(payload).array();
for (const QJsonValue &releaseValue : releases) {
const QJsonObject release = releaseValue.toObject();
const QJsonObject versionData = release.value(QStringLiteral("version_data")).toObject();
const int major = versionData.value(QStringLiteral("major")).toInt();
const QString releaseName = release.value(QStringLiteral("release_name")).toString();
if (major <= 0 || releaseName.isEmpty())
continue;
const QString version = displayVersion(versionData);
const QDateTime released = QDateTime::fromString(
release.value(QStringLiteral("timestamp")).toString(), Qt::ISODate);
const QJsonArray binaries = release.value(QStringLiteral("binaries")).toArray();
for (const QJsonValue &binaryValue : binaries) {
const QJsonObject binary = binaryValue.toObject();
const auto kind = javaKindFromKey(binary.value(QStringLiteral("image_type")).toString());
if (!kind || *kind == JavaRuntimeKind::Mojang)
continue; // testimage, debugimage, sources и прочее нам не нужно
const QJsonObject package = binary.value(QStringLiteral("package")).toObject();
const QUrl url(package.value(QStringLiteral("link")).toString());
const QString archive = archiveKind(package.value(QStringLiteral("name")).toString());
// Распаковать умеем только zip и tar.gz; https — потому что ссылку
// мы получили из сети и открываем её же.
if (archive.isEmpty() || url.scheme() != QLatin1String("https"))
continue;
JavaRuntimeEntry entry;
entry.kind = *kind;
entry.major = major;
entry.version = version;
entry.released = released;
entry.url = url;
entry.checksum = package.value(QStringLiteral("checksum")).toString();
entry.size = static_cast<qint64>(package.value(QStringLiteral("size")).toDouble());
entry.archive = archive;
entry.architecture = binary.value(QStringLiteral("architecture")).toString();
entry.lts = ltsVersions.contains(major);
entry.id = JavaRuntimeStore::sanitizeId(QStringLiteral("temurin-%1-%2-%3")
.arg(javaKindKey(*kind),
version,
entry.architecture));
result << entry;
}
}
return result;
}
QList<JavaRuntimeEntry> JavaRuntimeService::parseMojang(const QByteArray &payload)
{
QList<JavaRuntimeEntry> result;
const QJsonObject root = QJsonDocument::fromJson(payload).object();
const QJsonObject platform = root.value(mojangPlatform()).toObject();
for (auto it = platform.constBegin(); it != platform.constEnd(); ++it) {
// minecraft-java-exe — это лаунчер игры, а не Java.
if (it.key() == QLatin1String("minecraft-java-exe"))
continue;
const QJsonArray variants = it.value().toArray();
if (variants.isEmpty())
continue; // компонента под эту платформу нет
const QJsonObject variant = variants.first().toObject();
const QJsonObject manifest = variant.value(QStringLiteral("manifest")).toObject();
const QUrl url(manifest.value(QStringLiteral("url")).toString());
if (url.scheme() != QLatin1String("https"))
continue;
const QJsonObject versionObject = variant.value(QStringLiteral("version")).toObject();
const QString version = versionObject.value(QStringLiteral("name")).toString();
if (version.isEmpty())
continue;
JavaRuntimeEntry entry;
entry.kind = JavaRuntimeKind::Mojang;
entry.component = it.key();
entry.version = version;
// «8u51-cacert462b08» у jre-legacy, «21.0.7» у остальных.
entry.major = version.startsWith(QLatin1String("8u"))
? 8
: version.section(u'.', 0, 0).toInt();
entry.released = QDateTime::fromString(
versionObject.value(QStringLiteral("released")).toString(), Qt::ISODate);
entry.url = url;
entry.checksum = manifest.value(QStringLiteral("sha1")).toString();
entry.size = 0; // манифест знает только свой размер, не размер рантайма
entry.archive = QStringLiteral("mojang");
entry.architecture = QSysInfo::currentCpuArchitecture();
entry.lts = false;
entry.id = JavaRuntimeStore::sanitizeId(QStringLiteral("mojang-") + it.key());
if (entry.major > 0)
result << entry;
}
return result;
}
void JavaRuntimeService::applyEntries(QList<JavaRuntimeEntry> list)
{
std::stable_sort(list.begin(), list.end(),
[](const JavaRuntimeEntry &a, const JavaRuntimeEntry &b) {
if (a.major != b.major)
return a.major > b.major;
if (a.released != b.released)
return a.released > b.released;
return a.version > b.version;
});
m_entries = std::move(list);
m_indexById.clear();
m_indexById.reserve(m_entries.size());
for (int i = 0; i < m_entries.size(); ++i)
m_indexById.insert(m_entries.at(i).id, i);
}
bool JavaRuntimeService::readCache()
{
QFile file(LauncherPaths::javaCatalogFile());
if (!file.open(QIODevice::ReadOnly))
return false;
const QJsonObject root = QJsonDocument::fromJson(file.readAll()).object();
// Кэш платформозависим: папку лаунчера могли принести с другой машины.
if (root.value(QStringLiteral("platform")).toString() != mojangPlatform())
return false;
QList<JavaRuntimeEntry> parsed;
const QJsonArray entries = root.value(QStringLiteral("entries")).toArray();
for (const QJsonValue &value : entries) {
const QJsonObject object = value.toObject();
const auto kind = javaKindFromKey(object.value(QStringLiteral("kind")).toString());
if (!kind)
continue;
JavaRuntimeEntry entry;
entry.id = object.value(QStringLiteral("id")).toString();
entry.kind = *kind;
entry.major = object.value(QStringLiteral("major")).toInt();
entry.version = object.value(QStringLiteral("version")).toString();
entry.component = object.value(QStringLiteral("component")).toString();
entry.released = QDateTime::fromString(
object.value(QStringLiteral("released")).toString(), Qt::ISODate);
entry.url = QUrl(object.value(QStringLiteral("url")).toString());
entry.checksum = object.value(QStringLiteral("checksum")).toString();
entry.size = static_cast<qint64>(object.value(QStringLiteral("size")).toDouble());
entry.archive = object.value(QStringLiteral("archive")).toString();
entry.architecture = object.value(QStringLiteral("architecture")).toString();
entry.lts = object.value(QStringLiteral("lts")).toBool();
if (entry.isValid() && entry.url.scheme() == QLatin1String("https"))
parsed << entry;
}
if (parsed.isEmpty())
return false;
applyEntries(std::move(parsed));
m_fetchedAt = QDateTime::fromString(root.value(QStringLiteral("fetchedAt")).toString(),
Qt::ISODate);
return true;
}
void JavaRuntimeService::writeCache()
{
// Папку кэша могли удалить уже после запуска лаунчера.
if (!LauncherPaths::ensureRootExists())
return;
QJsonArray entries;
for (const JavaRuntimeEntry &entry : std::as_const(m_entries)) {
entries << QJsonObject{{QStringLiteral("id"), entry.id},
{QStringLiteral("kind"), javaKindKey(entry.kind)},
{QStringLiteral("major"), entry.major},
{QStringLiteral("version"), entry.version},
{QStringLiteral("component"), entry.component},
{QStringLiteral("released"), entry.released.toString(Qt::ISODate)},
{QStringLiteral("url"), entry.url.toString()},
{QStringLiteral("checksum"), entry.checksum},
{QStringLiteral("size"), static_cast<double>(entry.size)},
{QStringLiteral("archive"), entry.archive},
{QStringLiteral("architecture"), entry.architecture},
{QStringLiteral("lts"), entry.lts}};
}
const QJsonObject root{{QStringLiteral("fetchedAt"), m_fetchedAt.toString(Qt::ISODate)},
{QStringLiteral("platform"), mojangPlatform()},
{QStringLiteral("entries"), entries}};
QSaveFile file(LauncherPaths::javaCatalogFile());
if (!file.open(QIODevice::WriteOnly))
return;
const QByteArray payload = QJsonDocument(root).toJson(QJsonDocument::Compact);
if (file.write(payload) != payload.size()) {
file.cancelWriting();
return;
}
file.commit();
}