Expose server build information

Co-authored-by: Zach Brown <1254957+zachbr@users.noreply.github.com>
Co-authored-by: Kyle Wood <kyle@denwav.dev>
Co-authored-by: Mark Vainomaa <mikroskeem@mikroskeem.eu>
Co-authored-by: Riley Park <rileysebastianpark@gmail.com>
Co-authored-by: Jake Potrebic <jake.m.potrebic@gmail.com>
Co-authored-by: masmc05 <masmc05@gmail.com>
This commit is contained in:
Zach Brown
2016-03-01 14:32:43 -06:00
parent 560b13ca08
commit f41436f787
13 changed files with 581 additions and 100 deletions

View File

@@ -0,0 +1,146 @@
package com.destroystokyo.paper;
import com.destroystokyo.paper.util.VersionFetcher;
import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import com.mojang.logging.LogUtils;
import io.papermc.paper.ServerBuildInfo;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.stream.StreamSupport;
import net.kyori.adventure.text.Component;
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.slf4j.Logger;
import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.format.TextColor.color;
@DefaultQualifier(NonNull.class)
public class PaperVersionFetcher implements VersionFetcher {
private static final Logger LOGGER = LogUtils.getClassLogger();
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";
@Override
public long getCacheTime() {
return 720000;
}
@Override
public Component getVersionMessage(final String serverVersion) {
final Component updateMessage;
final ServerBuildInfo build = ServerBuildInfo.buildInfo();
if (build.buildNumber().isEmpty() && build.gitCommit().isEmpty()) {
updateMessage = text("You are running a development version without access to version information", color(0xFF5300));
} else {
updateMessage = getUpdateStatusMessage("PaperMC/Paper", build);
}
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) {
int distance = DISTANCE_ERROR;
final OptionalInt buildNumber = build.buildNumber();
if (buildNumber.isPresent()) {
distance = fetchDistanceFromSiteApi(build, buildNumber.getAsInt());
} else {
final Optional<String> gitBranch = build.gitBranch();
final Optional<String> gitCommit = build.gitCommit();
if (gitBranch.isPresent() && gitCommit.isPresent()) {
distance = fetchDistanceFromGitHub(repo, gitBranch.get(), gitCommit.get());
}
}
return switch (distance) {
case DISTANCE_ERROR -> text("Error obtaining version information", NamedTextColor.YELLOW);
case 0 -> text("You are running the latest version", NamedTextColor.GREEN);
case DISTANCE_UNKNOWN -> text("Unknown version", NamedTextColor.YELLOW);
default -> text("You are " + distance + " version(s) behind", NamedTextColor.YELLOW)
.append(Component.newline())
.append(text("Download the new version at: ")
.append(text(DOWNLOAD_PAGE, NamedTextColor.GOLD)
.hoverEvent(text("Click to open", NamedTextColor.WHITE))
.clickEvent(ClickEvent.openUrl(DOWNLOAD_PAGE))));
};
}
private static int fetchDistanceFromSiteApi(final ServerBuildInfo build, final int jenkinsBuild) {
try {
try (final BufferedReader reader = Resources.asCharSource(
URI.create("https://api.papermc.io/v2/projects/paper/versions/" + build.minecraftVersionId()).toURL(),
Charsets.UTF_8
).openBufferedStream()) {
final JsonObject json = new Gson().fromJson(reader, JsonObject.class);
final JsonArray builds = json.getAsJsonArray("builds");
final int latest = StreamSupport.stream(builds.spliterator(), false)
.mapToInt(JsonElement::getAsInt)
.max()
.orElseThrow();
return latest - jenkinsBuild;
} catch (final JsonSyntaxException ex) {
LOGGER.error("Error parsing json from Paper's downloads API", ex);
return DISTANCE_ERROR;
}
} catch (final IOException e) {
LOGGER.error("Error while parsing version", e);
return DISTANCE_ERROR;
}
}
// Contributed by Techcable <Techcable@outlook.com> in GH-65
private static int fetchDistanceFromGitHub(final String repo, 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();
connection.connect();
if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) return DISTANCE_UNKNOWN; // Unknown commit
try (final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8))) {
final JsonObject obj = new Gson().fromJson(reader, JsonObject.class);
final String status = obj.get("status").getAsString();
return switch (status) {
case "identical" -> 0;
case "behind" -> obj.get("behind_by").getAsInt();
default -> DISTANCE_ERROR;
};
} catch (final JsonSyntaxException | NumberFormatException e) {
LOGGER.error("Error parsing json from GitHub's API", e);
return DISTANCE_ERROR;
}
} catch (final IOException e) {
LOGGER.error("Error while parsing version", e);
return DISTANCE_ERROR;
}
}
private @Nullable Component getHistory() {
final VersionHistoryManager.@Nullable VersionData data = VersionHistoryManager.INSTANCE.getVersionData();
if (data == null) {
return null;
}
final @Nullable String oldVersion = data.getOldVersion();
if (oldVersion == null) {
return null;
}
return text("Previous version: " + oldVersion, NamedTextColor.GRAY, TextDecoration.ITALIC);
}
}

View File

@@ -0,0 +1,153 @@
package com.destroystokyo.paper;
import com.google.common.base.MoreObjects;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bukkit.Bukkit;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public enum VersionHistoryManager {
INSTANCE;
private final Gson gson = new Gson();
private final Logger logger = Bukkit.getLogger();
private VersionData currentData = null;
VersionHistoryManager() {
final Path path = Paths.get("version_history.json");
if (Files.exists(path)) {
// Basic file santiy checks
if (!Files.isRegularFile(path)) {
if (Files.isDirectory(path)) {
logger.severe(path + " is a directory, cannot be used for version history");
} else {
logger.severe(path + " is not a regular file, cannot be used for version history");
}
// We can't continue
return;
}
try (final BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
currentData = gson.fromJson(reader, VersionData.class);
} catch (final IOException e) {
logger.log(Level.SEVERE, "Failed to read version history file '" + path + "'", e);
return;
} catch (final JsonSyntaxException e) {
logger.log(Level.SEVERE, "Invalid json syntax for file '" + path + "'", e);
return;
}
final String version = Bukkit.getVersion();
if (version == null) {
logger.severe("Failed to retrieve current version");
return;
}
if (currentData == null) {
// Empty file
currentData = new VersionData();
currentData.setCurrentVersion(version);
writeFile(path);
return;
}
if (!version.equals(currentData.getCurrentVersion())) {
// The version appears to have changed
currentData.setOldVersion(currentData.getCurrentVersion());
currentData.setCurrentVersion(version);
writeFile(path);
}
} else {
// File doesn't exist, start fresh
currentData = new VersionData();
// oldVersion is null
currentData.setCurrentVersion(Bukkit.getVersion());
writeFile(path);
}
}
private void writeFile(@Nonnull final Path path) {
try (final BufferedWriter writer = Files.newBufferedWriter(
path,
StandardCharsets.UTF_8,
StandardOpenOption.WRITE,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
)) {
gson.toJson(currentData, writer);
} catch (final IOException e) {
logger.log(Level.SEVERE, "Failed to write to version history file", e);
}
}
@Nullable
public VersionData getVersionData() {
return currentData;
}
public static class VersionData {
private String oldVersion;
private String currentVersion;
@Nullable
public String getOldVersion() {
return oldVersion;
}
public void setOldVersion(@Nullable String oldVersion) {
this.oldVersion = oldVersion;
}
@Nullable
public String getCurrentVersion() {
return currentVersion;
}
public void setCurrentVersion(@Nullable String currentVersion) {
this.currentVersion = currentVersion;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("oldVersion", oldVersion)
.add("currentVersion", currentVersion)
.toString();
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final VersionData versionData = (VersionData) o;
return Objects.equals(oldVersion, versionData.oldVersion) &&
Objects.equals(currentVersion, versionData.currentVersion);
}
@Override
public int hashCode() {
return Objects.hash(oldVersion, currentVersion);
}
}
}