Add update checker (#3855)

Co-authored-by: MD <1917406+mdcfe@users.noreply.github.com>

Adds an update checker to Essentials that will check for the latest version on startup, on player join (permission is `essentials.updatecheck`), or manually with `/ess version`.

On startup, the update checker will fetch build information from a resource generated at compile time and determine if the build is a dev or stable build. If it's a stable build, the update checker will only check for a new release; while a dev build will check for any new commits.

There are 6 different types of messages the update checker will return;
* Identical: The current build is the latest stable release or latest dev build. This message is only shown in the `/ess version` command.
* Behind: If the current build is stable, it's an entire stable build behind, otherwise it's one or more dev builds behind.
* Diverged: The current build was made from a branch other than `2.x` and is also one or more dev builds behind the latest commit on `2.x`.
* Diverged Latest: The current build was made from a branch other than `2.x` but is based on the latest commit from `2.x`.
* Unknown: The current build either has invalid build information or was customly built. This message is show everywhere but on player join.
* Error: There was an error while fetching the latest version information.

Update checks can be disabled using the `update-check` option in `config.yml`.
This commit is contained in:
Josh Roy 2021-03-06 11:29:42 -05:00 committed by GitHub
parent b43790e9d2
commit 10fa3b5a31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 342 additions and 3 deletions

View File

@ -36,6 +36,7 @@ import com.earth2me.essentials.signs.SignPlayerListener;
import com.earth2me.essentials.textreader.IText;
import com.earth2me.essentials.textreader.KeywordReplacer;
import com.earth2me.essentials.textreader.SimpleTextInput;
import com.earth2me.essentials.updatecheck.UpdateChecker;
import com.earth2me.essentials.utils.VersionUtil;
import io.papermc.lib.PaperLib;
import net.ess3.api.Economy;
@ -147,6 +148,7 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
private transient MaterialTagProvider materialTagProvider;
private transient Kits kits;
private transient RandomTeleport randomTeleport;
private transient UpdateChecker updateChecker;
static {
// TODO: improve legacy code
@ -390,6 +392,16 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
metrics = new MetricsWrapper(this, 858, true);
updateChecker = new UpdateChecker(this);
runTaskAsynchronously(() -> {
LOGGER.log(Level.INFO, tl("versionFetching"));
for (String str : updateChecker.getVersionMessages(false, true)) {
LOGGER.log(Level.WARNING, str);
}
});
execTimer.mark("Init(External)");
final String timeroutput = execTimer.end();
if (getSettings().isDebug()) {
LOGGER.log(Level.INFO, "Essentials load {0}", timeroutput);
@ -776,6 +788,11 @@ public class Essentials extends JavaPlugin implements net.ess3.api.IEssentials {
return randomTeleport;
}
@Override
public UpdateChecker getUpdateChecker() {
return updateChecker;
}
@Deprecated
@Override
public User getUser(final Object base) {

View File

@ -422,6 +422,14 @@ public class EssentialsPlayerListener implements Listener {
final TextPager pager = new TextPager(output, true);
pager.showPage("1", null, "motd", user.getSource());
}
if (user.isAuthorized("essentials.updatecheck")) {
ess.runTaskAsynchronously(() -> {
for (String str : ess.getUpdateChecker().getVersionMessages(false, false)) {
user.sendMessage(str);
}
});
}
}
}
}

View File

@ -4,6 +4,7 @@ import com.earth2me.essentials.api.IItemDb;
import com.earth2me.essentials.api.IJails;
import com.earth2me.essentials.api.IWarps;
import com.earth2me.essentials.perm.PermissionsHandler;
import com.earth2me.essentials.updatecheck.UpdateChecker;
import net.ess3.provider.MaterialTagProvider;
import net.ess3.provider.ContainerProvider;
import net.ess3.provider.FormattedCommandAliasProvider;
@ -73,6 +74,8 @@ public interface IEssentials extends Plugin {
RandomTeleport getRandomTeleport();
UpdateChecker getUpdateChecker();
BukkitTask runTaskAsynchronously(Runnable run);
BukkitTask runTaskLaterAsynchronously(Runnable run, long delay);

View File

@ -391,6 +391,8 @@ public interface ISettings extends IConf {
boolean isRespawnAtBed();
boolean isUpdateCheckEnabled();
enum KeepInvPolicy {
KEEP,
DELETE,

View File

@ -1767,4 +1767,9 @@ public class Settings implements net.ess3.api.ISettings {
public boolean isRespawnAtBed() {
return config.getBoolean("respawn-at-home-bed", true);
}
@Override
public boolean isUpdateCheckEnabled() {
return config.getBoolean("update-check", true);
}
}

View File

@ -388,10 +388,16 @@ public class Commandessentials extends EssentialsCommand {
sender.sendMessage(tl("serverUnsupportedLimitedApi"));
break;
}
if (VersionUtil.getSupportStatusClass() != null) {
sender.sendMessage(tl("serverUnsupportedClass", VersionUtil.getSupportStatusClass()));
}
sender.sendMessage(tl("versionFetching"));
ess.runTaskAsynchronously(() -> {
for (String str : ess.getUpdateChecker().getVersionMessages(true, true)) {
sender.sendMessage(str);
}
});
}
@Override

View File

@ -38,6 +38,7 @@ public class MetricsWrapper {
checkForcedMetrics();
addPermsChart();
addEconomyChart();
addReleaseBranchChart();
// bStats' backend currently doesn't support multi-line charts or advanced bar charts
// These are included for when bStats is ready to accept this data
@ -87,6 +88,10 @@ public class MetricsWrapper {
}));
}
private void addReleaseBranchChart() {
metrics.addCustomChart(new Metrics.SimplePie("releaseBranch", ess.getUpdateChecker()::getVersionBranch));
}
private void addCommandsChart() {
for (final String command : plugin.getDescription().getCommands().keySet()) {
markCommand(command, false);

View File

@ -0,0 +1,269 @@
package com.earth2me.essentials.updatecheck;
import com.earth2me.essentials.Essentials;
import com.google.common.base.Charsets;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import static com.earth2me.essentials.I18n.tl;
public final class UpdateChecker {
private static final String REPO = "EssentialsX/Essentials";
private static final String BRANCH = "2.x";
private final Essentials ess;
private final String versionIdentifier;
private final String versionBranch;
private final boolean devBuild;
private long lastFetchTime = 0;
private CompletableFuture<RemoteVersion> pendingDevFuture;
private CompletableFuture<RemoteVersion> pendingReleaseFuture;
private String latestRelease = null;
private RemoteVersion cachedDev = null;
private RemoteVersion cachedRelease = null;
public UpdateChecker(Essentials ess) {
String identifier = "INVALID";
String branch = "INVALID";
boolean dev = false;
final InputStream inputStream = UpdateChecker.class.getClassLoader().getResourceAsStream("release");
if (inputStream != null) {
final List<String> versionStr = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.toList());
if (versionStr.size() == 2) {
if (versionStr.get(0).matches("\\d+\\.\\d+\\.\\d+-dev\\+\\d\\d-[0-9a-f]{7,40}")) {
identifier = versionStr.get(0).split("-")[2];
dev = true;
} else {
identifier = versionStr.get(0);
}
branch = versionStr.get(1);
}
}
this.ess = ess;
this.versionIdentifier = identifier;
this.versionBranch = branch;
this.devBuild = dev;
}
public boolean isDevBuild() {
return devBuild;
}
public CompletableFuture<RemoteVersion> fetchLatestDev() {
if (cachedDev == null || ((System.currentTimeMillis() - lastFetchTime) > 1800000L)) {
if (pendingDevFuture != null) {
return pendingDevFuture;
}
pendingDevFuture = new CompletableFuture<>();
new Thread(() -> {
pendingDevFuture.complete(cachedDev = fetchDistance(BRANCH, getVersionIdentifier()));
pendingDevFuture = null;
lastFetchTime = System.currentTimeMillis();
}).start();
return pendingDevFuture;
}
return CompletableFuture.completedFuture(cachedDev);
}
public CompletableFuture<RemoteVersion> fetchLatestRelease() {
if (cachedRelease == null || ((System.currentTimeMillis() - lastFetchTime) > 1800000L)) {
if (pendingReleaseFuture != null) {
return pendingReleaseFuture;
}
pendingReleaseFuture = new CompletableFuture<>();
new Thread(() -> {
catchBlock:
try {
final HttpURLConnection connection = (HttpURLConnection) new URL("https://api.github.com/repos/" + REPO + "/releases/latest").openConnection();
connection.connect();
if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
// Locally built?
pendingReleaseFuture.complete(cachedRelease = new RemoteVersion(BranchStatus.UNKNOWN));
break catchBlock;
}
if (connection.getResponseCode() == HttpURLConnection.HTTP_INTERNAL_ERROR) {
// Github is down
pendingReleaseFuture.complete(new RemoteVersion(BranchStatus.ERROR));
break catchBlock;
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8))) {
latestRelease = new Gson().fromJson(reader, JsonObject.class).get("tag_name").getAsString();
pendingReleaseFuture.complete(cachedRelease = fetchDistance(latestRelease, getVersionIdentifier()));
} catch (JsonSyntaxException | NumberFormatException e) {
e.printStackTrace();
pendingReleaseFuture.complete(new RemoteVersion(BranchStatus.ERROR));
}
} catch (IOException e) {
e.printStackTrace();
pendingReleaseFuture.complete(new RemoteVersion(BranchStatus.ERROR));
}
pendingReleaseFuture = null;
lastFetchTime = System.currentTimeMillis();
}).start();
return pendingReleaseFuture;
}
return CompletableFuture.completedFuture(cachedRelease);
}
public String getVersionIdentifier() {
return versionIdentifier;
}
public String getVersionBranch() {
return versionBranch;
}
public String getBuildInfo() {
return "id:'" + getVersionIdentifier() + "' branch:'" + getVersionBranch() + "' isDev:" + isDevBuild();
}
public String getLatestRelease() {
return latestRelease;
}
private RemoteVersion fetchDistance(final String head, final String hash) {
try {
final HttpURLConnection connection = (HttpURLConnection) new URL("https://api.github.com/repos/" + REPO + "/compare/" + head + "..." + hash).openConnection();
connection.connect();
if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
// Locally built?
return new RemoteVersion(BranchStatus.UNKNOWN);
}
if (connection.getResponseCode() == HttpURLConnection.HTTP_INTERNAL_ERROR) {
// Github is down
return new RemoteVersion(BranchStatus.ERROR);
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8))) {
final JsonObject obj = new Gson().fromJson(reader, JsonObject.class);
switch (obj.get("status").getAsString()) {
case "identical": {
return new RemoteVersion(BranchStatus.IDENTICAL, 0);
}
case "ahead": {
return new RemoteVersion(BranchStatus.AHEAD, 0);
}
case "behind": {
return new RemoteVersion(BranchStatus.BEHIND, obj.get("behind_by").getAsInt());
}
case "diverged": {
return new RemoteVersion(BranchStatus.DIVERGED, obj.get("behind_by").getAsInt());
}
default: {
return new RemoteVersion(BranchStatus.UNKNOWN);
}
}
} catch (JsonSyntaxException | NumberFormatException e) {
e.printStackTrace();
return new RemoteVersion(BranchStatus.ERROR);
}
} catch (IOException e) {
e.printStackTrace();
return new RemoteVersion(BranchStatus.ERROR);
}
}
public String[] getVersionMessages(final boolean sendLatestMessage, final boolean verboseErrors) {
if (!ess.getSettings().isUpdateCheckEnabled()) {
return new String[] {tl("versionCheckDisabled")};
}
if (this.isDevBuild()) {
final RemoteVersion latestDev = this.fetchLatestDev().join();
switch (latestDev.getBranchStatus()) {
case IDENTICAL: {
return sendLatestMessage ? new String[] {tl("versionDevLatest")} : new String[] {};
}
case BEHIND: {
return new String[] {tl("versionDevBehind", latestDev.getDistance()),
tl("versionReleaseNewLink", "https://essentialsx.net/downloads.html")};
}
case AHEAD:
case DIVERGED: {
return new String[] {tl(latestDev.getDistance() == 0 ? "versionDevDivergedLatest" : "versionDevDiverged", latestDev.getDistance()),
tl("versionDevDivergedBranch", this.getVersionBranch()) };
}
case UNKNOWN: {
return verboseErrors ? new String[] {tl("versionCustom", this.getBuildInfo())} : new String[] {};
}
case ERROR: {
return new String[] {tl(verboseErrors ? "versionError" : "versionErrorPlayer", this.getBuildInfo())};
}
default: {
return new String[] {};
}
}
} else {
final RemoteVersion latestRelease = this.fetchLatestRelease().join();
switch (latestRelease.getBranchStatus()) {
case IDENTICAL: {
return sendLatestMessage ? new String[] {tl("versionReleaseLatest")} : new String[] {};
}
case BEHIND: {
return new String[] {tl("versionReleaseNew", this.getLatestRelease()),
tl("versionReleaseNewLink", "https://essentialsx.net/downloads.html?branch=stable")};
}
case DIVERGED: //WhatChamp
case AHEAD: //monkaW?
case UNKNOWN: {
return verboseErrors ? new String[] {tl("versionCustom", this.getBuildInfo())} : new String[] {};
}
case ERROR: {
return new String[] {tl(verboseErrors ? "versionError" : "versionErrorPlayer", this.getBuildInfo())};
}
default: {
return new String[] {};
}
}
}
}
private static class RemoteVersion {
private final BranchStatus branchStatus;
private final int distance;
RemoteVersion(BranchStatus branchStatus) {
this(branchStatus, 0);
}
RemoteVersion(BranchStatus branchStatus, int distance) {
this.branchStatus = branchStatus;
this.distance = distance;
}
public BranchStatus getBranchStatus() {
return branchStatus;
}
public int getDistance() {
return distance;
}
}
private enum BranchStatus {
IDENTICAL,
AHEAD,
BEHIND,
DIVERGED,
ERROR,
UNKNOWN
}
}

View File

@ -674,6 +674,11 @@ log-command-block-commands: true
# Set the maximum speed for projectiles spawned with /fireball.
max-projectile-speed: 8
# Should EssentialsX check for updates?
# If set to true, EssentialsX will show notifications when a new version is available.
# This uses the public GitHub API and no identifying information is sent or stored.
update-check: true
############################################################
# +------------------------------------------------------+ #
# | Homes | #

View File

@ -936,6 +936,16 @@ vanish=\u00a76Vanish for {0}\u00a76\: {1}
vanishCommandDescription=Hide yourself from other players.
vanishCommandUsage=/<command> [player] [on|off]
vanished=\u00a76You are now completely invisible to normal users, and hidden from in-game commands.
versionCheckDisabled=\u00a76Update checking disabled in config.
versionCustom=\u00a76Unable to check your version! Self-built? Build information: \u00a7c{0}\u00a76.
versionDevBehind=\u00a74You''re \u00a7c{0} \u00a74EssentialsX dev build(s) out of date!
versionDevDiverged=\u00a76You''re running an experimental build of EssentialsX that is \u00a7c{0} \u00a76builds behind the latest dev build!
versionDevDivergedBranch=\u00a76Feature Branch: \u00a7c{0}\u00a76.
versionDevDivergedLatest=\u00a76You''re running an up to date experimental EssentialsX build!
versionDevLatest=\u00a76You''re running the latest EssentialsX dev build!
versionError=\u00a74Error while fetching EssentialsX version information! Build information: \u00a7c{0}\u00a76.
versionErrorPlayer=\u00a76Error while checking EssentialsX version information!
versionFetching=\u00a76Fetching version information...
versionOutputVaultMissing=\u00a74Vault is not installed. Chat and permissions may not work.
versionOutputFine=\u00a76{0} version: \u00a7a{1}
versionOutputWarn=\u00a76{0} version: \u00a7c{1}
@ -943,6 +953,9 @@ versionOutputUnsupported=\u00a7d{0} \u00a76version: \u00a7d{1}
versionOutputUnsupportedPlugins=\u00a76You are running \u00a7dunsupported plugins\u00a76!
versionMismatch=\u00a74Version mismatch\! Please update {0} to the same version.
versionMismatchAll=\u00a74Version mismatch\! Please update all Essentials jars to the same version.
versionReleaseLatest=\u00a76You''re running the latest stable version of EssentialsX!
versionReleaseNew=\u00a74There is a new EssentialsX version available for download: \u00a7c{0}\u00a74.
versionReleaseNewLink=\u00a74Download it here:\u00a7c {0}
voiceSilenced=\u00a76Your voice has been silenced\!
voiceSilencedTime=\u00a76Your voice has been silenced for {0}\!
voiceSilencedReason=\u00a76Your voice has been silenced\! Reason: \u00a7c{0}

View File

@ -0,0 +1,2 @@
${full.version}
${git.branch}

View File

@ -36,9 +36,10 @@ def commitsSinceLastTag() {
ext {
GIT_COMMIT = grgit == null ? "unknown" : grgit.head().abbreviatedId
GIT_BRANCH = grgit == null ? "detached-head" : grgit.branch.current().name
GIT_DEPTH = commitsSinceLastTag()
fullVersion = "${version}-${GIT_COMMIT}".replace("-SNAPSHOT", "-dev+${GIT_DEPTH}")
fullVersion = "${version}".replace("-SNAPSHOT", "-dev+${GIT_DEPTH}-${GIT_COMMIT}")
checkstyleVersion = '8.36.2'
spigotVersion = '1.16.5-R0.1-SNAPSHOT'
@ -92,9 +93,12 @@ subprojects {
// Version Injection
processResources {
// Always process resources if version string or git branch changes
inputs.property('fullVersion', fullVersion)
inputs.property('gitBranch', GIT_BRANCH)
filter(ReplaceTokens, beginToken: '${',
endToken: '}', tokens: ["full.version": fullVersion])
endToken: '}', tokens: ["full.version": fullVersion, "git.branch": GIT_BRANCH])
}
indra {