Improve outdated version check (#12589)

Co-authored-by: MiniDigger | Martin <admin@minidigger.dev>
This commit is contained in:
tofik.07 2026-01-02 12:57:12 +01:00 committed by GitHub
parent cd445fdb26
commit 8f736dbf07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 153 additions and 36 deletions

View file

@ -66,4 +66,5 @@ Lukas Planz <lukas.planz@web.de>
granny <contact@granny.dev>
mja00 <me@mja00.dev>
roro1506HD <16260475+roro1506HD@users.noreply.github.com>
Toffikk <slodkitofik@gmail.com>
```

View file

@ -759,7 +759,7 @@
protected void runServer() {
try {
if (!this.initServer()) {
@@ -739,26 +_,68 @@
@@ -739,26 +_,73 @@
}
this.nextTickTimeNanos = Util.getNanos();
@ -791,6 +791,11 @@
+ LOGGER.info("*************************************************************************************");
+ }
+ // Paper end - Add onboarding message for initial server start
+ // Paper start - Improve outdated version checking
+ if (System.getProperty("paper.disableStartupVersionCheck") == null && io.papermc.paper.configuration.GlobalConfiguration.get().updateChecker.enabled) {
+ CompletableFuture.runAsync(com.destroystokyo.paper.PaperVersionFetcher::getUpdateStatusStartupMessage, io.papermc.paper.util.MCUtil.ASYNC_EXECUTOR);
+ }
+ // Paper end - Improve outdated version checking
+
while (this.running) {
- long l;

View file

@ -1,7 +1,6 @@
package com.destroystokyo.paper;
import com.destroystokyo.paper.util.VersionFetcher;
import com.google.common.io.Resources;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
@ -14,28 +13,43 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.stream.StreamSupport;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultQualifier;
import org.apache.logging.log4j.LogManager;
import org.slf4j.Logger;
import java.util.List;
import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.format.TextColor.color;
import static io.papermc.paper.ServerBuildInfo.StringRepresentation.VERSION_SIMPLE;
@DefaultQualifier(NonNull.class)
public class PaperVersionFetcher implements VersionFetcher {
private static final Logger LOGGER = LogUtils.getClassLogger();
private static final ComponentLogger COMPONENT_LOGGER = ComponentLogger.logger(LogManager.getRootLogger().getName());
private static final int DISTANCE_ERROR = -1;
private static final int DISTANCE_UNKNOWN = -2;
private static final String DOWNLOAD_PAGE = "https://papermc.io/downloads/paper";
private static final String REPOSITORY = "PaperMC/Paper";
private static final ServerBuildInfo BUILD_INFO;
private static final String USER_AGENT;
private static final Gson GSON = new Gson();
static {
BUILD_INFO = ServerBuildInfo.buildInfo();
USER_AGENT = BUILD_INFO.brandName() + "/" + BUILD_INFO.asString(VERSION_SIMPLE) + " (https://papermc.io)";
}
@Override
public long getCacheTime() {
@ -45,28 +59,69 @@ public class PaperVersionFetcher implements VersionFetcher {
@Override
public Component getVersionMessage() {
final Component updateMessage;
final ServerBuildInfo build = ServerBuildInfo.buildInfo();
if (build.buildNumber().isEmpty() && build.gitCommit().isEmpty()) {
if (BUILD_INFO.buildNumber().isEmpty() && BUILD_INFO.gitCommit().isEmpty()) {
updateMessage = text("You are running a development version without access to version information", color(0xFF5300));
} else {
updateMessage = getUpdateStatusMessage("PaperMC/Paper", build);
updateMessage = getUpdateStatusMessage();
}
final @Nullable Component history = this.getHistory();
return history != null ? Component.textOfChildren(updateMessage, Component.newline(), history) : updateMessage;
}
private static Component getUpdateStatusMessage(final String repo, final ServerBuildInfo build) {
public static void getUpdateStatusStartupMessage() {
int distance = DISTANCE_ERROR;
final OptionalInt buildNumber = build.buildNumber();
if (buildNumber.isPresent()) {
distance = fetchDistanceFromSiteApi(build, buildNumber.getAsInt());
final OptionalInt buildNumber = BUILD_INFO.buildNumber();
if (buildNumber.isEmpty() && BUILD_INFO.gitCommit().isEmpty()) {
COMPONENT_LOGGER.warn(text("*** You are running a development version without access to version information ***"));
} else {
final Optional<String> gitBranch = build.gitBranch();
final Optional<String> gitCommit = build.gitCommit();
final Optional<MinecraftVersionFetcher> apiResult = fetchMinecraftVersionList();
if (buildNumber.isPresent()) {
distance = fetchDistanceFromSiteApi(buildNumber.getAsInt());
} else {
final Optional<String> gitBranch = BUILD_INFO.gitBranch();
final Optional<String> gitCommit = BUILD_INFO.gitCommit();
if (gitBranch.isPresent() && gitCommit.isPresent()) {
distance = fetchDistanceFromGitHub(gitBranch.get(), gitCommit.get());
}
}
switch (distance) {
case DISTANCE_ERROR -> COMPONENT_LOGGER.error(text("*** Error obtaining version information! Cannot fetch version info ***"));
case 0 -> apiResult.ifPresent(result -> {
COMPONENT_LOGGER.warn(text("*************************************************************************************"));
COMPONENT_LOGGER.warn(text("You are running the latest build for your Minecraft version (" + BUILD_INFO.minecraftVersionId() + ")"));
COMPONENT_LOGGER.warn(text("However, you are " + result.distance() + " release(s) behind the latest stable release (" + result.latestVersion() + ")!"));
COMPONENT_LOGGER.warn(text("It is recommended that you update as soon as possible"));
COMPONENT_LOGGER.warn(text(DOWNLOAD_PAGE));
COMPONENT_LOGGER.warn(text("*************************************************************************************"));
});
case DISTANCE_UNKNOWN -> COMPONENT_LOGGER.warn(text("*** You are running an unknown version! Cannot fetch version info ***"));
default -> {
if (apiResult.isPresent()) {
COMPONENT_LOGGER.warn(text("*** You are running an outdated version of Minecraft, which is " + apiResult.get().distance() + " release(s) and " + distance + " build(s) behind!"));
COMPONENT_LOGGER.warn(text("*** Please update to the latest stable version on " + DOWNLOAD_PAGE + " ***"));
} else {
COMPONENT_LOGGER.info(text("*** Currently you are " + distance + " build(s) behind ***"));
COMPONENT_LOGGER.info(text("*** It is highly recommended to download the latest build from " + DOWNLOAD_PAGE + " ***"));
}
}
}
}
}
private static Component getUpdateStatusMessage() {
int distance = DISTANCE_ERROR;
final OptionalInt buildNumber = PaperVersionFetcher.BUILD_INFO.buildNumber();
if (buildNumber.isPresent()) {
distance = fetchDistanceFromSiteApi(buildNumber.getAsInt());
} else {
final Optional<String> gitBranch = PaperVersionFetcher.BUILD_INFO.gitBranch();
final Optional<String> gitCommit = PaperVersionFetcher.BUILD_INFO.gitCommit();
if (gitBranch.isPresent() && gitCommit.isPresent()) {
distance = fetchDistanceFromGitHub(repo, gitBranch.get(), gitCommit.get());
distance = fetchDistanceFromGitHub(gitBranch.get(), gitCommit.get());
}
}
@ -83,13 +138,75 @@ public class PaperVersionFetcher implements VersionFetcher {
};
}
private static int fetchDistanceFromSiteApi(final ServerBuildInfo build, final int jenkinsBuild) {
private record MinecraftVersionFetcher(String latestVersion, int distance) {}
private static Optional<MinecraftVersionFetcher> fetchMinecraftVersionList() {
final String currentVersion = PaperVersionFetcher.BUILD_INFO.minecraftVersionId();
try {
try (final BufferedReader reader = Resources.asCharSource(
URI.create("https://api.papermc.io/v2/projects/paper/versions/" + build.minecraftVersionId()).toURL(),
StandardCharsets.UTF_8
).openBufferedStream()) {
final JsonObject json = new Gson().fromJson(reader, JsonObject.class);
final URL versionsUrl = URI.create("https://fill.papermc.io/v3/projects/paper").toURL();
final HttpURLConnection connection = (HttpURLConnection) versionsUrl.openConnection();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.setRequestProperty("User-Agent", PaperVersionFetcher.USER_AGENT);
connection.setRequestProperty("Accept", "application/json");
try (final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
final JsonObject json = GSON.fromJson(reader, JsonObject.class);
final JsonObject versions = json.getAsJsonObject("versions");
final List<String> versionList = versions.keySet().stream()
.map(versions::getAsJsonArray)
.flatMap(array -> StreamSupport.stream(array.spliterator(), false))
.map(JsonElement::getAsString)
.toList();
for (final String latestVersion : versionList) {
if (latestVersion.equals(currentVersion)) {
return Optional.empty();
}
try {
final URL buildsUrl = URI.create("https://fill.papermc.io/v3/projects/paper/versions/" + latestVersion + "/builds/latest").toURL();
final HttpURLConnection connection2 = (HttpURLConnection) buildsUrl.openConnection();
connection2.setConnectTimeout(5000);
connection2.setReadTimeout(5000);
connection2.setRequestProperty("User-Agent", PaperVersionFetcher.USER_AGENT);
connection2.setRequestProperty("Accept", "application/json");
try (final BufferedReader buildReader = new BufferedReader(new InputStreamReader(connection2.getInputStream(), StandardCharsets.UTF_8))) {
final JsonObject buildJson = GSON.fromJson(buildReader, JsonObject.class);
if ("STABLE".equals(buildJson.get("channel").getAsString())) {
final int currentIndex = versionList.indexOf(currentVersion);
final int latestIndex = versionList.indexOf(latestVersion);
final int distance = currentIndex - latestIndex;
return Optional.of(new MinecraftVersionFetcher(latestVersion, distance));
}
} catch (final JsonSyntaxException ex) {
LOGGER.error("Error parsing json from Paper's downloads API", ex);
}
} catch (final IOException e) {
LOGGER.error("Error while parsing latest build", e);
}
}
} catch (final JsonSyntaxException ex) {
LOGGER.error("Error parsing json from Paper's downloads API", ex);
}
} catch (final IOException e) {
LOGGER.error("Error while parsing version list", e);
}
return Optional.empty();
}
private static int fetchDistanceFromSiteApi(final int jenkinsBuild) {
try {
final URL buildsUrl = URI.create("https://fill.papermc.io/v3/projects/paper/versions/" + PaperVersionFetcher.BUILD_INFO.minecraftVersionId()).toURL();
final HttpURLConnection connection = (HttpURLConnection) buildsUrl.openConnection();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.setRequestProperty("User-Agent", PaperVersionFetcher.USER_AGENT);
connection.setRequestProperty("Accept", "application/json");
try (final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
final JsonObject json = GSON.fromJson(reader, JsonObject.class);
final JsonArray builds = json.getAsJsonArray("builds");
final int latest = StreamSupport.stream(builds.spliterator(), false)
.mapToInt(JsonElement::getAsInt)
@ -107,13 +224,16 @@ public class PaperVersionFetcher implements VersionFetcher {
}
// Contributed by Techcable <Techcable@outlook.com> in GH-65
private static int fetchDistanceFromGitHub(final String repo, final String branch, final String hash) {
private static int fetchDistanceFromGitHub(final String branch, final String hash) {
try {
final HttpURLConnection connection = (HttpURLConnection) URI.create("https://api.github.com/repos/%s/compare/%s...%s".formatted(repo, branch, hash)).toURL().openConnection();
final HttpURLConnection connection = (HttpURLConnection) URI.create("https://api.github.com/repos/%s/compare/%s...%s".formatted(PaperVersionFetcher.REPOSITORY, branch, hash)).toURL().openConnection();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.setRequestProperty("User-Agent", PaperVersionFetcher.USER_AGENT);
connection.connect();
if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) return DISTANCE_UNKNOWN; // Unknown commit
try (final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
final JsonObject obj = new Gson().fromJson(reader, JsonObject.class);
final JsonObject obj = GSON.fromJson(reader, JsonObject.class);
final String status = obj.get("status").getAsString();
return switch (status) {
case "identical" -> 0;

View file

@ -394,4 +394,10 @@ public class GlobalConfiguration extends ConfigurationPart {
}
}
}
public UpdateChecker updateChecker;
public class UpdateChecker extends ConfigurationPart {
public boolean enabled = true;
}
}

View file

@ -3,8 +3,6 @@ package org.bukkit.craftbukkit;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import joptsimple.OptionParser;
@ -223,19 +221,6 @@ public class Main {
System.setProperty(net.minecrell.terminalconsole.TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY, "false"); // Paper
}
if (Main.class.getPackage().getImplementationVendor() != null && System.getProperty("IReallyKnowWhatIAmDoingISwear") == null) {
Date buildDate = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse(Main.class.getPackage().getImplementationVendor()); // Paper
Calendar deadline = Calendar.getInstance();
deadline.add(Calendar.DAY_OF_YEAR, -14);
if (buildDate.before(deadline.getTime())) {
// Paper start - This is some stupid bullshit
System.err.println("*** Warning, you've not updated in a while! ***");
System.err.println("*** Please download a new build from https://papermc.io/downloads/paper ***");
// Paper end
}
}
System.setProperty("library.jansi.version", "Paper"); // Paper - set meaningless jansi version to prevent git builds from crashing on Windows
System.setProperty("jdk.console", "java.base"); // Paper - revert default console provider back to java.base so we can have our own jline