From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Mon, 27 May 2019 01:10:06 -0500 Subject: [PATCH] Expose server build information Co-authored-by: Professor Bloodstone Co-authored-by: Mark Vainomaa Co-authored-by: masmc05 Co-authored-by: Riley Park diff --git a/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java b/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java new file mode 100644 index 0000000000000000000000000000000000000000..023cc52a9e28e1238c7452c0f3f577f2850fd861 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java @@ -0,0 +1,47 @@ +package com.destroystokyo.paper.util; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public interface VersionFetcher { + + /** + * Amount of time to cache results for in milliseconds + *

+ * Negative values will never cache. + * + * @return cache time + */ + long getCacheTime(); + + /** + * Gets the version message to cache and show to command senders. + * + *

NOTE: This is run in a new thread separate from that of the command processing thread

+ * + * @param serverVersion the current version of the server (will match {@link Bukkit#getVersion()}) + * @return the message to show when requesting a version + */ + Component getVersionMessage(String serverVersion); + + @ApiStatus.Internal + class DummyVersionFetcher implements VersionFetcher { + + @Override + public long getCacheTime() { + return -1; + } + + @Override + public Component getVersionMessage(final String serverVersion) { + Bukkit.getLogger().warning("Version provider has not been set, cannot check for updates!"); + Bukkit.getLogger().info("Override the default implementation of org.bukkit.UnsafeValues#getVersionFetcher()"); + new Throwable().printStackTrace(); + return Component.text("Unable to check for updates. No version provider set.", NamedTextColor.RED); + } + } +} diff --git a/src/main/java/io/papermc/paper/ServerBuildInfo.java b/src/main/java/io/papermc/paper/ServerBuildInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..652ff54e7c50412503725d628bfe72ed03059790 --- /dev/null +++ b/src/main/java/io/papermc/paper/ServerBuildInfo.java @@ -0,0 +1,122 @@ +package io.papermc.paper; + +import java.time.Instant; +import java.util.Optional; +import java.util.OptionalInt; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.util.Services; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; + +/** + * Information about the current server build. + */ +@NullMarked +@ApiStatus.NonExtendable +public interface ServerBuildInfo { + /** + * The brand id for Paper. + */ + Key BRAND_PAPER_ID = Key.key("papermc", "paper"); + + /** + * Gets the {@code ServerBuildInfo}. + * + * @return the {@code ServerBuildInfo} + */ + static ServerBuildInfo buildInfo() { + // + final class Holder { + static final Optional INSTANCE = Services.service(ServerBuildInfo.class); + } + // + return Holder.INSTANCE.orElseThrow(); + } + + /** + * Gets the brand id of the server. + * + * @return the brand id of the server (e.g. "papermc:paper") + */ + Key brandId(); + + /** + * Checks if the current server supports the specified brand. + * + * @param brandId the brand to check (e.g. "papermc:folia") + * @return {@code true} if the server supports the specified brand + */ + @ApiStatus.Experimental + boolean isBrandCompatible(final Key brandId); + + /** + * Gets the brand name of the server. + * + * @return the brand name of the server (e.g. "Paper") + */ + String brandName(); + + /** + * Gets the Minecraft version id. + * + * @return the Minecraft version id (e.g. "1.20.4", "1.20.2-pre2", "23w31a") + */ + String minecraftVersionId(); + + /** + * Gets the Minecraft version name. + * + * @return the Minecraft version name (e.g. "1.20.4", "1.20.2 Pre-release 2", "23w31a") + */ + String minecraftVersionName(); + + /** + * Gets the build number. + * + * @return the build number + */ + OptionalInt buildNumber(); + + /** + * Gets the build time. + * + * @return the build time + */ + Instant buildTime(); + + /** + * Gets the git commit branch. + * + * @return the git commit branch + */ + Optional gitBranch(); + + /** + * Gets the git commit hash. + * + * @return the git commit hash + */ + Optional gitCommit(); + + /** + * Creates a string representation of the server build information. + * + * @param representation the type of representation + * @return a string + */ + String asString(final StringRepresentation representation); + + /** + * String representation types. + */ + enum StringRepresentation { + /** + * A simple version string, in format {@code --}. + */ + VERSION_SIMPLE, + /** + * A simple version string, in format {@code --@ ()}. + */ + VERSION_FULL, + } +} diff --git a/src/main/java/io/papermc/paper/util/JarManifests.java b/src/main/java/io/papermc/paper/util/JarManifests.java new file mode 100644 index 0000000000000000000000000000000000000000..7915a70d676b1205dcae39259f670af258a1ab9b --- /dev/null +++ b/src/main/java/io/papermc/paper/util/JarManifests.java @@ -0,0 +1,38 @@ +package io.papermc.paper.util; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.jar.Manifest; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +@ApiStatus.Internal +public final class JarManifests { + private JarManifests() { + } + + private static final Map MANIFESTS = Collections.synchronizedMap(new WeakHashMap<>()); + + public static @Nullable Manifest manifest(final Class clazz) { + return MANIFESTS.computeIfAbsent(clazz.getClassLoader(), classLoader -> { + final String classLocation = "/" + clazz.getName().replace(".", "/") + ".class"; + final URL resource = clazz.getResource(classLocation); + if (resource == null) { + return null; + } + final String classFilePath = resource.toString().replace("\\", "/"); + final String archivePath = classFilePath.substring(0, classFilePath.length() - classLocation.length()); + try (final InputStream stream = new URL(archivePath + "/META-INF/MANIFEST.MF").openStream()) { + return new Manifest(stream); + } catch (final IOException ex) { + return null; + } + }); + } +} diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java index 5780003631beae773231afe3afc6b834bfe1f84d..a005d5f8879262c763d8c4fbd09b9a99810d5e8c 100644 --- a/src/main/java/org/bukkit/Bukkit.java +++ b/src/main/java/org/bukkit/Bukkit.java @@ -110,13 +110,26 @@ public final class Bukkit { } Bukkit.server = server; - server.getLogger().info("This server is running " + getName() + " version " + getVersion() + " (Implementing API version " + getBukkitVersion() + ")"); + // Paper start - add git information + server.getLogger().info(getVersionMessage()); + } + /** + * Gets message describing the version server is running. + * + * @return message describing the version server is running + */ + @NotNull + public static String getVersionMessage() { + final io.papermc.paper.ServerBuildInfo version = io.papermc.paper.ServerBuildInfo.buildInfo(); + return "This server is running " + getName() + " version " + version.asString(io.papermc.paper.ServerBuildInfo.StringRepresentation.VERSION_FULL) + " (Implementing API version " + getBukkitVersion() + ")"; + // Paper end } /** * Gets the name of this server implementation. * * @return name of this server implementation + * @see io.papermc.paper.ServerBuildInfo#brandName() */ @NotNull public static String getName() { @@ -127,6 +140,7 @@ public final class Bukkit { * Gets the version string of this server implementation. * * @return version of this server implementation + * @see io.papermc.paper.ServerBuildInfo */ @NotNull public static String getVersion() { @@ -143,6 +157,20 @@ public final class Bukkit { return server.getBukkitVersion(); } + // Paper start - expose game version + /** + * Gets the version of game this server implements + * + * @return version of game + * @see io.papermc.paper.ServerBuildInfo#minecraftVersionId() + * @see io.papermc.paper.ServerBuildInfo#minecraftVersionName() + */ + @NotNull + public static String getMinecraftVersion() { + return server.getMinecraftVersion(); + } + // Paper end + /** * Gets a view of all currently logged in players. This {@linkplain * Collections#unmodifiableCollection(Collection) view} is a reused diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java index ba28d9f3213ca4b5f15178dc637bff37a8896edc..8a07f21eeb04fb54032ce377a1478f60303e1824 100644 --- a/src/main/java/org/bukkit/Server.java +++ b/src/main/java/org/bukkit/Server.java @@ -120,6 +120,16 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi @NotNull public String getBukkitVersion(); + // Paper start - expose game version + /** + * Gets the version of game this server implements + * + * @return version of game + */ + @NotNull + String getMinecraftVersion(); + // Paper end + /** * Gets a view of all currently logged in players. This {@linkplain * Collections#unmodifiableCollection(Collection) view} is a reused diff --git a/src/main/java/org/bukkit/UnsafeValues.java b/src/main/java/org/bukkit/UnsafeValues.java index 12ef99f1c91b92a133611c5f5aeaaeebd02ce232..6e67fdb091a006d2d13bc2d93db4d55348af4c8f 100644 --- a/src/main/java/org/bukkit/UnsafeValues.java +++ b/src/main/java/org/bukkit/UnsafeValues.java @@ -163,5 +163,12 @@ public interface UnsafeValues { */ @Deprecated(forRemoval = true) String getTimingsServerName(); + + /** + * Called once by the version command on first use, then cached. + */ + default com.destroystokyo.paper.util.VersionFetcher getVersionFetcher() { + return new com.destroystokyo.paper.util.VersionFetcher.DummyVersionFetcher(); + } // Paper end } diff --git a/src/main/java/org/bukkit/command/defaults/VersionCommand.java b/src/main/java/org/bukkit/command/defaults/VersionCommand.java index 263208d3cba36cb80c9ee4e3022ef702ea113df2..e64bb57f74e6d6f78927be228825b3e0bdf41f48 100644 --- a/src/main/java/org/bukkit/command/defaults/VersionCommand.java +++ b/src/main/java/org/bukkit/command/defaults/VersionCommand.java @@ -25,8 +25,25 @@ import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.util.StringUtil; import org.jetbrains.annotations.NotNull; +// Paper start - version command 2.0 +import com.destroystokyo.paper.util.VersionFetcher; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +// Paper end - version command 2.0 public class VersionCommand extends BukkitCommand { + private VersionFetcher versionFetcher; // Paper - version command 2.0 + private VersionFetcher getVersionFetcher() { // lazy load because unsafe isn't available at command registration + if (versionFetcher == null) { + versionFetcher = Bukkit.getUnsafe().getVersionFetcher(); + } + + return versionFetcher; + } + public VersionCommand(@NotNull String name) { super(name); @@ -41,7 +58,7 @@ public class VersionCommand extends BukkitCommand { if (!testPermission(sender)) return true; if (args.length == 0) { - sender.sendMessage("This server is running " + Bukkit.getName() + " version " + Bukkit.getVersion() + " (Implementing API version " + Bukkit.getBukkitVersion() + ")"); + //sender.sendMessage("This server is running " + Bukkit.getName() + " version " + Bukkit.getVersion() + " (Implementing API version " + Bukkit.getBukkitVersion() + ")"); // Paper - moved to setVersionMessage sendVersion(sender); } else { StringBuilder name = new StringBuilder(); @@ -80,8 +97,17 @@ public class VersionCommand extends BukkitCommand { private void describeToSender(@NotNull Plugin plugin, @NotNull CommandSender sender) { PluginDescriptionFile desc = plugin.getDescription(); - sender.sendMessage(ChatColor.GREEN + desc.getName() + ChatColor.WHITE + " version " + ChatColor.GREEN + desc.getVersion()); - + // Paper start - version command 2.0 + sender.sendMessage( + Component.text() + .append(Component.text(desc.getName(), NamedTextColor.GREEN)) + .append(Component.text(" version ")) + .append(Component.text(desc.getVersion(), NamedTextColor.GREEN) + .hoverEvent(Component.text("Click to copy to clipboard", NamedTextColor.WHITE)) + .clickEvent(ClickEvent.copyToClipboard(desc.getVersion())) + ) + ); + // Paper end - version command 2.0 if (desc.getDescription() != null) { sender.sendMessage(desc.getDescription()); } @@ -147,14 +173,14 @@ public class VersionCommand extends BukkitCommand { private final ReentrantLock versionLock = new ReentrantLock(); private boolean hasVersion = false; - private String versionMessage = null; + private Component versionMessage = null; // Paper private final Set versionWaiters = new HashSet(); private boolean versionTaskStarted = false; private long lastCheck = 0; private void sendVersion(@NotNull CommandSender sender) { if (hasVersion) { - if (System.currentTimeMillis() - lastCheck > 21600000) { + if (System.currentTimeMillis() - lastCheck > getVersionFetcher().getCacheTime()) { // Paper - use version supplier lastCheck = System.currentTimeMillis(); hasVersion = false; } else { @@ -169,7 +195,7 @@ public class VersionCommand extends BukkitCommand { return; } versionWaiters.add(sender); - sender.sendMessage("Checking version, please wait..."); + sender.sendMessage(Component.text("Checking version, please wait...", NamedTextColor.WHITE, TextDecoration.ITALIC)); // Paper if (!versionTaskStarted) { versionTaskStarted = true; new Thread(new Runnable() { @@ -187,6 +213,13 @@ public class VersionCommand extends BukkitCommand { private void obtainVersion() { String version = Bukkit.getVersion(); + // Paper start + if (version.startsWith("null")) { // running from ide? + setVersionMessage(Component.text("Unknown version, custom build?", NamedTextColor.YELLOW)); + return; + } + setVersionMessage(getVersionFetcher().getVersionMessage(version)); + /* if (version == null) version = "Custom"; String[] parts = version.substring(0, version.indexOf(' ')).split("-"); if (parts.length == 4) { @@ -216,11 +249,24 @@ public class VersionCommand extends BukkitCommand { } else { setVersionMessage("Unknown version, custom build?"); } + */ + // Paper end } - private void setVersionMessage(@NotNull String msg) { + // Paper start + private void setVersionMessage(final @NotNull Component msg) { lastCheck = System.currentTimeMillis(); - versionMessage = msg; + final Component message = Component.textOfChildren( + Component.text(Bukkit.getVersionMessage(), NamedTextColor.WHITE), + Component.newline(), + msg + ); + this.versionMessage = Component.text() + .append(message) + .hoverEvent(Component.text("Click to copy to clipboard", NamedTextColor.WHITE)) + .clickEvent(ClickEvent.copyToClipboard(PlainTextComponentSerializer.plainText().serialize(message))) + .build(); + // Paper end versionLock.lock(); try { hasVersion = true; diff --git a/src/test/java/io/papermc/paper/TestServerBuildInfo.java b/src/test/java/io/papermc/paper/TestServerBuildInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..17be27a869c1047a7a9440fb8f3717260d4abbd0 --- /dev/null +++ b/src/test/java/io/papermc/paper/TestServerBuildInfo.java @@ -0,0 +1,59 @@ +package io.papermc.paper; + +import java.time.Instant; +import java.util.Optional; +import java.util.OptionalInt; +import net.kyori.adventure.key.Key; +import org.jetbrains.annotations.NotNull; + +public class TestServerBuildInfo implements ServerBuildInfo { + @Override + public @NotNull Key brandId() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isBrandCompatible(final @NotNull Key brandId) { + throw new UnsupportedOperationException(); + } + + @Override + public @NotNull String brandName() { + throw new UnsupportedOperationException(); + } + + @Override + public @NotNull String minecraftVersionId() { + throw new UnsupportedOperationException(); + } + + @Override + public @NotNull String minecraftVersionName() { + throw new UnsupportedOperationException(); + } + + @Override + public @NotNull OptionalInt buildNumber() { + throw new UnsupportedOperationException(); + } + + @Override + public @NotNull Instant buildTime() { + throw new UnsupportedOperationException(); + } + + @Override + public @NotNull Optional gitBranch() { + throw new UnsupportedOperationException(); + } + + @Override + public @NotNull Optional gitCommit() { + throw new UnsupportedOperationException(); + } + + @Override + public @NotNull String asString(final @NotNull StringRepresentation representation) { + return ""; + } +} diff --git a/src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo b/src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo new file mode 100644 index 0000000000000000000000000000000000000000..64e2f8559b9c5a52e0a3229d3d12f65e9af145b3 --- /dev/null +++ b/src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo @@ -0,0 +1 @@ +io.papermc.paper.TestServerBuildInfo