#include "javaruntimeservice.h" #include "localization.h" #include "launcherpaths.h" #include #include #include #include #include #include #include #include #include #include #include #include #include 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 JavaRuntimeService::entries(JavaRuntimeKind kind) const { QList result; for (const JavaRuntimeEntry &entry : m_entries) { if (entry.kind == kind) result << entry; } return result; } std::optional 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 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(Loc::text("java.error.versionsFetchFailed") .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 majors; for (const QJsonValue &value : available) { const int major = value.toInt(); if (major > 0) majors << major; } if (majors.isEmpty()) { requestFinished(Loc::text("java.error.versionsUnparsable")); 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 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 = Loc::text("java.error.buildsFetchFailed") .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(Loc::text("java.error.mojangRuntimesFailed") .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 pending = std::exchange(m_pending, {}); for (const Callback &callback : pending) { // Сеть подвела, но старые данные есть — работаем на них. if (!ok && hasData()) callback(true, warning.isEmpty() ? Loc::text("java.status.catalogFromCache") : warning); else callback(ok, warning); } } QList JavaRuntimeService::parseAdoptium(const QByteArray &payload, const QSet <sVersions) { QList 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(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 JavaRuntimeService::parseMojang(const QByteArray &payload) { QList 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 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 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(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(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(); }