diff --git a/src/main/resources/config.toml b/devconfig.yml similarity index 50% rename from src/main/resources/config.toml rename to devconfig.yml index 24a572d7..4462cd7a 100644 --- a/src/main/resources/config.toml +++ b/devconfig.yml @@ -1,26 +1,38 @@ -# GeyserUpdater +# GeyserUpdater configuration with more settings enabled # Made by Jens & YHDiamond # NOTICE: Please read the README on our github page for full information regarding these options! # https://github.com/ProjectG-Plugins/GeyserUpdater -# If enabled, GeyserUpdater will check for new Geyser builds on server start, and on the interval specified by Auto-Update-Interval. If a new build exists, it will be downloaded. -Auto-Update-Geyser=false +default-updates: + geyser: + enable: true + auto-check: true + auto-update: true + floodgate: + enable: false + auto-check: true + auto-update: false + # The interval in hours between each auto update check. -Auto-Update-Interval=24 +auto-update-interval: 24 +# The maximum amount of seconds that a download is allowed to run for. Increase if you are running into issues on a slow internet connection. +download-time-limit: 300 +# Delete the downloaded file if the file hash of the downloaded file did not match what the download server provided. +# Or if the file hash was not checked and the download time limit was reached, or an exception occurred. +# If the file hash is not correct the downloaded file is likely corrupt or unfinished. +delete-on-fail: true # If enabled, GeyserUpdater will attempt to restart the server 10 seconds after a new version of Geyser has been successfully downloaded. # If you aren't using a hosting provider or a server wrapper, you will need a restart script. -Auto-Restart-Server=false +auto-restart-server: true # When enabled, GeyserUpdater will automatically generate a restart script for you. If you are using CraftBukkit or a proxy # you will need to use the generated script to start your server! If you are using a hosting provider or a server wrapper you probably don't need this. -Auto-Script-Generating=false - +auto-script-generating: true # Configure the message that is sent to all online players warning them that the server will be restarting in 10 seconds. -Restart-Message-Players='&2This server will be restarting in 10 seconds!' +restart-message-players: "§2This server will be restarting in 10 seconds!" # Enable debug logging -Enable-Debug=false - +enable-debug: true # Please do not change this version value! -Config-Version=2 \ No newline at end of file +config-version: 3 diff --git a/guDeploy.sh b/guDeploy.sh index 19ffe375..f6f20b2e 100644 --- a/guDeploy.sh +++ b/guDeploy.sh @@ -14,14 +14,14 @@ waterDir="Waterfall-guDeploy" velocityDir="Velocity-guDeploy" #Links -guLink="https://ci.projectg.dev/job/GeyserUpdater/job/1.5.0/lastSuccessfulBuild/artifact/target/GeyserUpdater-1.5.0.jar" -geyserLink="https://ci.opencollab.dev/job/GeyserMC/job/Geyser/job/master/720/artifact/bootstrap/" +guLink="https://ci.projectg.dev/job/GeyserUpdater/job/1.6.0/lastSuccessfulBuild/artifact/target/GeyserUpdater-1.6.0.jar" +geyserLink="https://ci.opencollab.dev/job/GeyserMC/job/Geyser/job/master/821/artifact/bootstrap/" buildToolsLink="https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar" -paperLink="https://papermc.io/api/v2/projects/paper/versions/1.16.5/builds/750/downloads/paper-1.16.5-750.jar" +paperLink="https://papermc.io/api/v2/projects/paper/versions/1.17.1/builds/209/downloads/paper-1.17.1-209.jar" bungeeLink="https://ci.md-5.net/job/BungeeCord/lastSuccessfulBuild/artifact/bootstrap/target/BungeeCord.jar" -waterLink="https://papermc.io/api/v2/projects/waterfall/versions/1.16/builds/425/downloads/waterfall-1.16-425.jar" -velocityLink="https://versions.velocitypowered.com/download/1.1.5.jar" +waterLink="https://papermc.io/api/v2/projects/waterfall/versions/1.17/builds/448/downloads/waterfall-1.17-448.jar" +velocityLink="https://versions.velocitypowered.com/download/3.0.0.jar" echo "[WARN] This script can generate up to 500MB of data!" @@ -40,12 +40,20 @@ else fi # Download file for a given link +# first arg is link, second arg is output file name (optional) download () { - jarURL="$1" if [[ "$downloadCmd" == "curl" ]]; then - curl "$jarURL" -O + if [[ -z "$2" ]]; then + curl "$1" -O + else + curl "$1" --output "$2" + fi else - wget "$jarURL" + if [[ -z "$2" ]]; then + wget "$1" + else + curl "$1" --output-document "$2" + fi fi } @@ -57,8 +65,14 @@ getAllPlugins () { mkdir "$pluginCache" cd "$pluginCache" || exit - mkdir Common - cd Common || exit + mkdir "Common" + cd "Common" || exit + mkdir "GeyserUpdater" + cd "GeyserUpdater" || exit + echo + echo "[INFO] Downloading GeyserUpdater config" + download "https://raw.githubusercontent.com/ProjectG-Plugins/GeyserUpdater/1.6.0/devconfig.yml" "config.yml" + cd ../ echo echo "[INFO] Downloading GeyserUpdater" download "$guLink" @@ -66,31 +80,13 @@ getAllPlugins () { mkdir "Spigot" cd "Spigot" || exit - mkdir "GeyserUpdater" - cd "GeyserUpdater" || exit - echo "Auto-Update-Geyser: true -Auto-Restart-Server: true -Restart-Message-Players: '&2This server will be restarting in 10 seconds!' -Auto-Script-Generating: true -Enable-Debug: true -Config-Version: 2" > config.yml - cd ../ echo echo "[INFO] Downloading Geyser-Spigot.jar" download "$geyserLink"spigot/target/Geyser-Spigot.jar cd ../ - mkdir BungeeCord - cd BungeeCord || exit - mkdir GeyserUpdater - cd GeyserUpdater || exit - echo "Auto-Update-Geyser: true -Auto-Restart-Server: true -Restart-Message-Players: '&2This server will be restarting in 10 seconds!' -Auto-Script-Generating: true -Enable-Debug: true -Config-Version: 2" > config.yml - cd ../ + mkdir "BungeeCord" + cd "BungeeCord" || exit echo echo "[INFO] Downloading Geyser-BungeeCord.jar" download "$geyserLink"bungeecord/target/Geyser-BungeeCord.jar @@ -98,15 +94,6 @@ Config-Version: 2" > config.yml mkdir "Velocity" cd "Velocity" || exit - mkdir "geyserupdater" - cd "geyserupdater" || exit - echo "Auto-Update-Geyser=true -Auto-Restart-Server=true -Restart-Message-Players='&2This server will be restarting in 10 seconds!' -Auto-Script-Generating=true -Enable-Debug=true -Config-Version=2" > config.toml - cd ../ echo echo "[INFO] Downloading Geyser-Velocity" download "$geyserLink"velocity/target/Geyser-Velocity.jar diff --git a/pom.xml b/pom.xml index bd451a3a..745c0cf7 100644 --- a/pom.xml +++ b/pom.xml @@ -4,90 +4,78 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - com.projectg + dev.projectg GeyserUpdater GeyserUpdater - 1.5.0 + 1.6.0 UTF-8 UTF-8 1.8 1.8 + 2.10.2 - - bungeecord-repo - https://oss.sonatype.org/content/repositories/snapshots - - - velocity - https://nexus.velocitypowered.com/repository/maven-public/ - spigot-repo https://hub.spigotmc.org/nexus/content/repositories/snapshots/ - opencollab-release-repo - https://repo.opencollab.dev/maven-releases/ - - true - - - false - + jitpack.io + https://jitpack.io - opencollab-snapshot-repo - https://repo.opencollab.dev/maven-snapshots/ - - false - - - true - + velocity + https://nexus.velocitypowered.com/repository/maven-public/ - net.md-5 - bungeecord-api - 1.16-R0.5-SNAPSHOT - jar + org.spigotmc + spigot-api + 1.12.2-R0.1-SNAPSHOT provided - net.md-5 - bungeecord-api - 1.16-R0.5-SNAPSHOT - javadoc + com.github.SpigotMC.BungeeCord + bungeecord-proxy + + a7c6ede provided com.velocitypowered velocity-api - 1.1.8 + 3.0.0 provided + org.apache.logging.log4j log4j-core 2.13.2 provided + - org.bukkit - bukkit - 1.8-R0.1-SNAPSHOT - provided + org.spongepowered + configurate-yaml + 4.1.2 + + + + com.google.code.findbugs + jsr305 + 3.0.2 + - org.geysermc - connector - 1.4.0-SNAPSHOT + org.projectlombok + lombok + 1.18.22 provided @@ -109,6 +97,27 @@ 1.8 + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + org.spongepowered.configurate + dev.projectg.geyserupdater.shaded.configurate + + + + + + diff --git a/src/main/java/com/projectg/geyserupdater/bungee/BungeeUpdater.java b/src/main/java/com/projectg/geyserupdater/bungee/BungeeUpdater.java deleted file mode 100644 index e61f738d..00000000 --- a/src/main/java/com/projectg/geyserupdater/bungee/BungeeUpdater.java +++ /dev/null @@ -1,209 +0,0 @@ -package com.projectg.geyserupdater.bungee; - -import com.projectg.geyserupdater.bungee.command.GeyserUpdateCommand; -import com.projectg.geyserupdater.bungee.listeners.BungeeJoinListener; -import com.projectg.geyserupdater.bungee.util.GeyserBungeeDownloader; -import com.projectg.geyserupdater.bungee.util.bstats.Metrics; -import com.projectg.geyserupdater.common.logger.JavaUtilUpdaterLogger; -import com.projectg.geyserupdater.common.logger.UpdaterLogger; -import com.projectg.geyserupdater.common.util.FileUtils; -import com.projectg.geyserupdater.common.util.GeyserProperties; -import com.projectg.geyserupdater.common.util.ScriptCreator; - -import com.projectg.geyserupdater.common.util.SpigotResourceUpdateChecker; -import net.md_5.bungee.api.plugin.Plugin; -import net.md_5.bungee.config.Configuration; -import net.md_5.bungee.config.ConfigurationProvider; -import net.md_5.bungee.config.YamlConfiguration; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.concurrent.TimeUnit; - -public final class BungeeUpdater extends Plugin { - - private static BungeeUpdater plugin; - private Configuration configuration; - private UpdaterLogger logger; - - @Override - public void onEnable() { - plugin = this; - logger = new JavaUtilUpdaterLogger(getLogger()); - new Metrics(this, 10203); - - this.loadConfig(); - if (getConfig().getBoolean("Enable-Debug", false)) { - UpdaterLogger.getLogger().info("Trying to enable debug logging."); - UpdaterLogger.getLogger().enableDebug(); - } - - this.checkConfigVersion(); - // Check GeyserUpdater version - this.checkUpdaterVersion(); - - this.getProxy().getPluginManager().registerCommand(this, new GeyserUpdateCommand()); - // Player alert if a restart is required when they join - getProxy().getPluginManager().registerListener(this, new BungeeJoinListener()); - - // Make startup script - if (configuration.getBoolean("Auto-Script-Generating")) { - try { - // Tell the createScript method that a loop is necessary because bungee has no restart system. - ScriptCreator.createRestartScript(true); - } catch (IOException e) { - e.printStackTrace(); - } - } - // Auto update Geyser if enabled - if (configuration.getBoolean("Auto-Update-Geyser")) { - scheduleAutoUpdate(); - } - // Check if downloaded Geyser file exists periodically - getProxy().getScheduler().schedule(this, () -> { - if (FileUtils.checkFile("plugins/GeyserUpdater/BuildUpdate/Geyser-BungeeCord.jar", true)) { - logger.info("A new Geyser build has been downloaded! Please restart BungeeCord in order to use the updated build!"); - } - }, 30, 720, TimeUnit.MINUTES); - - } - - @Override - public void onDisable() { - // Force Geyser to disable so we can modify the jar in the plugins folder without issue - logger.debug("Forcing Geyser to disable first..."); - getProxy().getPluginManager().getPlugin("Geyser-BungeeCord").onDisable(); - try { - moveGeyserJar(); - for (int i = 0; i <= 2; i++) { - try { - deleteGeyserJar(); - break; - } catch (IOException ioException) { - logger.warn("An I/O error occurred while attempting to delete an unnecessary Geyser jar! Trying again " + (2 - i) + " more times."); - ioException.printStackTrace(); - try { - Thread.sleep(50); - } catch (InterruptedException interruptException) { - logger.error("Failed to delay an additional attempt!"); - interruptException.printStackTrace(); - } - } - } - } catch (IOException e) { - logger.error("An I/O error occurred while attempting to replace the current Geyser jar with the new one!"); - e.printStackTrace(); - } - } - - /** - * Load GeyserUpdater's config, create it if it doesn't exist - */ - public void loadConfig() { - try { - configuration = ConfigurationProvider.getProvider(YamlConfiguration.class).load(Config.startConfig(this, "config.yml")); - } catch (IOException exception) { - exception.printStackTrace(); - } - } - - /** - * Check the config version of GeyserUpdater - */ - public void checkConfigVersion(){ - //Change version number only when editing config.yml! - if (configuration.getInt("Config-Version", 0) != 2){ - logger.error("Your copy of config.yml is outdated. Please delete it and let a fresh copy of config.yml be regenerated!"); - } - } - - /** - * Check the version of GeyserUpdater against the spigot resource page - */ - public void checkUpdaterVersion() { - getProxy().getScheduler().runAsync(this, () -> { - String pluginVersion = getDescription().getVersion(); - String latestVersion = SpigotResourceUpdateChecker.getVersion(); - if (latestVersion == null || latestVersion.length() == 0) { - logger.error("Failed to determine the latest GeyserUpdater version!"); - } else { - if (latestVersion.equals(pluginVersion)) { - logger.info("You are using the latest version of GeyserUpdater!"); - } else { - logger.info("Your version: " + pluginVersion + ". Latest version: " + latestVersion + ". Download the newer version at https://www.spigotmc.org/resources/geyserupdater.88555/."); - } - } - - }); - } - - /** - * Check for a newer version of Geyser every 24hrs - */ - public void scheduleAutoUpdate() { - UpdaterLogger.getLogger().debug("Scheduling auto updater"); - // todo: build this in different way so that we don't repeat it if the Auto-Update-Interval is zero or -1 or something - getProxy().getScheduler().schedule(this, () -> { - logger.debug("Checking if a new build of Geyser exists."); - try { - // Checking for the build numbers of current build. - boolean isLatest = GeyserProperties.isLatestBuild(); - if (!isLatest) { - logger.info("A newer build of Geyser is available! Attempting to download the latest build now..."); - GeyserBungeeDownloader.updateGeyser(); - } - } catch (IOException e) { - logger.error("Failed to check for updates to Geyser! We were unable to reach the Geyser build server, or your local branch does not exist on it."); - e.printStackTrace(); - } - }, 1, getConfig().getLong("Auto-Update-Interval", 24L) * 60, TimeUnit.MINUTES); - } - - /** - * Replace the Geyser jar in the plugin folder with the one in GeyserUpdater/BuildUpdate - * Should only be called once Geyser has been disabled - * - * @throws IOException if there was an IO failure - */ - public void moveGeyserJar() throws IOException { - // Moving Geyser Jar to Plugins folder "Overwriting". - File fileToCopy = new File("plugins/GeyserUpdater/BuildUpdate/Geyser-BungeeCord.jar"); - if (fileToCopy.exists()) { - logger.debug("Moving the new Geyser jar to the plugins folder."); - FileInputStream input = new FileInputStream(fileToCopy); - File newFile = new File("plugins/Geyser-BungeeCord.jar"); - FileOutputStream output = new FileOutputStream(newFile); - byte[] buf = new byte[1024]; - int bytesRead; - while ((bytesRead = input.read(buf)) > 0) { - output.write(buf, 0, bytesRead); - } - input.close(); - output.close(); - } else { - logger.debug("Found no new Geyser jar to copy to the plugins folder."); - } - } - - /** - * Delete the Geyser jar in GeyserUpdater/BuildUpdate - * - * @throws IOException If it failed to delete - */ - private void deleteGeyserJar() throws IOException { - UpdaterLogger.getLogger().debug("Deleting the Geyser jar in the BuildUpdate folder if it exists"); - Path file = Paths.get("plugins/GeyserUpdater/BuildUpdate/Geyser-BungeeCord.jar"); - Files.deleteIfExists(file); - } - public static BungeeUpdater getPlugin() { - return plugin; - } - public Configuration getConfig() { - return configuration; - } -} diff --git a/src/main/java/com/projectg/geyserupdater/bungee/Config.java b/src/main/java/com/projectg/geyserupdater/bungee/Config.java deleted file mode 100644 index 8993d637..00000000 --- a/src/main/java/com/projectg/geyserupdater/bungee/Config.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.projectg.geyserupdater.bungee; - -import net.md_5.bungee.api.plugin.Plugin; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; - -public class Config { - public static File startConfig(Plugin plugin, String file) { - File folder = plugin.getDataFolder(); - if (!folder.exists()) { - folder.mkdir(); - } - File resourceFile = new File(folder, file); - try { - if (!resourceFile.exists()) { - try (InputStream in = plugin.getResourceAsStream(file); - OutputStream out = new FileOutputStream(resourceFile)) { - byte[] buffer = new byte[in.available()]; - in.read(buffer); - out.write(buffer); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - return resourceFile; - } -} diff --git a/src/main/java/com/projectg/geyserupdater/bungee/listeners/BungeeJoinListener.java b/src/main/java/com/projectg/geyserupdater/bungee/listeners/BungeeJoinListener.java deleted file mode 100644 index 8e8eb787..00000000 --- a/src/main/java/com/projectg/geyserupdater/bungee/listeners/BungeeJoinListener.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.projectg.geyserupdater.bungee.listeners; - -import com.projectg.geyserupdater.common.util.FileUtils; - -import net.md_5.bungee.api.chat.TextComponent; -import net.md_5.bungee.api.event.PostLoginEvent; -import net.md_5.bungee.api.plugin.Listener; -import net.md_5.bungee.event.EventHandler; - -public class BungeeJoinListener implements Listener { - - @EventHandler - public void onPostLogin(PostLoginEvent event) { - // We allow a cached result of maximum age 30 minutes to be used - if (FileUtils.checkFile("plugins/GeyserUpdater/BuildUpdate/Geyser-BungeeCord.jar", true)) { - if (event.getPlayer().hasPermission("gupdater.geyserupdate")) { - event.getPlayer().sendMessage(new TextComponent("[GeyserUpdater] A new Geyser build has been downloaded! Please restart BungeeCord in order to use the updated build!")); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/projectg/geyserupdater/bungee/util/GeyserBungeeDownloader.java b/src/main/java/com/projectg/geyserupdater/bungee/util/GeyserBungeeDownloader.java deleted file mode 100644 index 3c8cfe76..00000000 --- a/src/main/java/com/projectg/geyserupdater/bungee/util/GeyserBungeeDownloader.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.projectg.geyserupdater.bungee.util; - -import com.projectg.geyserupdater.bungee.BungeeUpdater; -import com.projectg.geyserupdater.common.logger.UpdaterLogger; -import com.projectg.geyserupdater.common.util.FileUtils; -import com.projectg.geyserupdater.common.util.GeyserProperties; - -import net.md_5.bungee.api.ChatColor; -import net.md_5.bungee.api.chat.TextComponent; -import net.md_5.bungee.api.connection.ProxiedPlayer; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -public class GeyserBungeeDownloader { - private static BungeeUpdater plugin; - private static UpdaterLogger logger; - - /** - * Download the latest build of Geyser from Jenkins CI for the currently used branch. - * If enabled in the config, the server will also attempt to restart. - */ - public static void updateGeyser() { - plugin = BungeeUpdater.getPlugin(); - logger = UpdaterLogger.getLogger(); - - UpdaterLogger.getLogger().debug("Attempting to download a new build of Geyser."); - - // New task so that we don't block the main thread. All new tasks on bungeecord are async. - plugin.getProxy().getScheduler().runAsync(plugin, () -> { - // Download the newest geyser build - if (downloadGeyser()) { - String successMsg = "The latest build of Geyser has been downloaded! A restart must occur in order for changes to take effect."; - logger.info(successMsg); - for (ProxiedPlayer player : plugin.getProxy().getPlayers()) { - if (player.hasPermission("gupdater.geyserupdate")) { - player.sendMessage(new TextComponent(ChatColor.GREEN + successMsg)); - } - } - if (plugin.getConfig().getBoolean("Auto-Restart-Server")) { - restartServer(); - } - } else { - // fail messages are already sent to the logger in downloadGeyser() - String failMsg = "A severe error occurred when download a new build of Geyser. Please check the server console for further information!"; - for (ProxiedPlayer player : plugin.getProxy().getPlayers()) { - if (player.hasPermission("gupdater.geyserupdate")) { - player.sendMessage(new TextComponent(ChatColor.RED + failMsg)); - } - } - } - }); - } - - /** - * Internal code for downloading the latest build of Geyser from Jenkins CI for the currently used branch. - * - * @return true if the download was successful, false if not. - */ - private static boolean downloadGeyser() { - String fileUrl; - try { - fileUrl = "https://ci.opencollab.dev/job/GeyserMC/job/Geyser/job/" + GeyserProperties.getGeyserGitPropertiesValue("git.branch") + "/lastSuccessfulBuild/artifact/bootstrap/bungeecord/target/Geyser-BungeeCord.jar"; - } catch (IOException e) { - logger.error("Failed to get the current Geyser branch when attempting to download a new build of Geyser!"); - e.printStackTrace(); - return false; - } - String outputPath = "plugins/GeyserUpdater/BuildUpdate/Geyser-BungeeCord.jar"; - try { - FileUtils.downloadFile(fileUrl, outputPath); - } catch (IOException e) { - logger.error("Failed to download the newest build of Geyser"); - e.printStackTrace(); - return false; - } - - if (!FileUtils.checkFile(outputPath, false)) { - logger.error("Failed to find the downloaded Geyser build!"); - return false; - } else { - return true; - } - } - - /** - * Attempt to restart the server - */ - private static void restartServer() { - logger.warn("The server will be restarting in 10 seconds!"); - for (ProxiedPlayer player : plugin.getProxy().getPlayers()) { - player.sendMessage(new TextComponent(ChatColor.translateAlternateColorCodes('&', plugin.getConfig().getString("Restart-Message-Players")))); - } - plugin.getProxy().getScheduler().schedule(plugin, () -> plugin.getProxy().stop(), 10L, TimeUnit.SECONDS); - } -} \ No newline at end of file diff --git a/src/main/java/com/projectg/geyserupdater/common/util/FileUtils.java b/src/main/java/com/projectg/geyserupdater/common/util/FileUtils.java deleted file mode 100644 index 0f9506df..00000000 --- a/src/main/java/com/projectg/geyserupdater/common/util/FileUtils.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.projectg.geyserupdater.common.util; - -import com.projectg.geyserupdater.common.logger.UpdaterLogger; - -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; -import java.net.URLConnection; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -public class FileUtils { - // TODO: this whole cached thing only works if you're using checkFile for one file... - - /** - * Epoch time of that last occurrence that {@link #checkFile(String, boolean)} directly checked a file. Returns a value of 0 if the check file method has never been called. - */ - private static long callTime = 0; - - /** - * Returns a cached result of {@link #checkFile(String, boolean)}. Returns null if the method has never been called. - */ - private static boolean cachedResult; - - /** - * Check if a file exists. - * - * @param path the path of the file to test - * @param allowCached allow a cached result of maximum 30 minutes to be returned - * @return true if the file exists, false if not - */ - public static boolean checkFile(String path, boolean allowCached) { - UpdaterLogger logger = UpdaterLogger.getLogger(); - if (allowCached) { - long elapsedTime = System.currentTimeMillis() - callTime; - if (elapsedTime < 30 * 60 * 1000) { - logger.debug("Returning a cached result of the last time we checked if a file exists. The cached result is: " + cachedResult); - return cachedResult; - } else { - logger.debug("Not returning a cached result of the last time we checked if a file exists because it has been too long."); - } - } - Path p = Paths.get(path); - boolean exists = Files.exists(p); - - logger.debug("Checked if a file exists. The result: " + exists); - callTime = System.currentTimeMillis(); - cachedResult = exists; - return exists; - } - - /** - * Download a file - * - * @param fileURL the url of the file - * @param outputPath the path of the output file to write to - */ - public static void downloadFile(String fileURL, String outputPath) throws IOException { - // TODO: better download code? - - UpdaterLogger.getLogger().debug("Attempting to download a file with URL and output path: " + fileURL + " , " + outputPath); - - Path outputDirectory = Paths.get(outputPath).getParent(); - Files.createDirectories(outputDirectory); - - OutputStream os; - InputStream is; - // create a url object - URL url = new URL(fileURL); - // connection to the file - URLConnection connection = url.openConnection(); - // get input stream to the file - is = connection.getInputStream(); - // get output stream to download file - os = new FileOutputStream(outputPath); - final byte[] b = new byte[2048]; - int length; - // read from input stream and write to output stream - while ((length = is.read(b)) != -1) { - os.write(b, 0, length); - } - // close streams - is.close(); - os.close(); - } -} - diff --git a/src/main/java/com/projectg/geyserupdater/common/util/GeyserProperties.java b/src/main/java/com/projectg/geyserupdater/common/util/GeyserProperties.java deleted file mode 100644 index 48b63603..00000000 --- a/src/main/java/com/projectg/geyserupdater/common/util/GeyserProperties.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.projectg.geyserupdater.common.util; - -import com.projectg.geyserupdater.common.logger.UpdaterLogger; -import org.geysermc.connector.utils.FileUtils; -import org.geysermc.connector.utils.WebUtils; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.Properties; - -public class GeyserProperties { - - // todo: a check to see if the local git branch is available on the CI (that knows if it failed because of a bad connection or not) - // todo: proper error handling - - /** - * Compare the local build number to the latest build number on Geyser CI - * - * @return true if local build number equals latest build number on Geyser CI - * @throws IOException if it fails to fetch either build number - */ - public static boolean isLatestBuild() throws IOException { - UpdaterLogger.getLogger().debug("Running isLatestBuild()"); - int jenkinsBuildNumber = getLatestGeyserBuildNumberFromJenkins(getGeyserGitPropertiesValue("git.branch")); - int localBuildNumber = Integer.parseInt(getGeyserGitPropertiesValue("git.build.number")); - // Compare build numbers. - // We treat higher build numbers as "out of date" here because Geyser's build numbers have been (accidentally) reset in the past. - // Self-compiled builds of Geyser simply do not have a `git.build.number` value, so it is /very/ unlikely that a user will ever have a Git build number higher than upstream anyway. - return jenkinsBuildNumber == localBuildNumber; - } - - /** Query the git properties of Geyser - * - * @param propertyKey the key of property to query - * @return the value of the property - * @throws IOException if failed to load the Geyser git properties - */ - public static String getGeyserGitPropertiesValue(String propertyKey) throws IOException { - UpdaterLogger.getLogger().debug("Running getGeyserGitPropertiesValue()"); - Properties gitProperties = new Properties(); - gitProperties.load(FileUtils.getResource("git.properties")); - return gitProperties.getProperty(propertyKey); - } - - /** Get the latest build number of a given branch of Geyser from jenkins CI - * - * @param gitBranch the branch to query - * @return the latest build number - * @throws UnsupportedEncodingException if failed to encode the given gitBranch - */ - public static int getLatestGeyserBuildNumberFromJenkins(String gitBranch) throws UnsupportedEncodingException { - UpdaterLogger.getLogger().debug("Running getLatestGeyserBuildNumberFromJenkins()"); - String buildXMLContents = WebUtils.getBody("https://ci.opencollab.dev/job/GeyserMC/job/Geyser/job/" + URLEncoder.encode(gitBranch, StandardCharsets.UTF_8.toString()) + "/lastSuccessfulBuild/api/xml?xpath=//buildNumber"); - return Integer.parseInt(buildXMLContents.replaceAll("<(\\\\)?(/)?buildNumber>", "").trim()); - } -} \ No newline at end of file diff --git a/src/main/java/com/projectg/geyserupdater/common/util/SpigotResourceUpdateChecker.java b/src/main/java/com/projectg/geyserupdater/common/util/SpigotResourceUpdateChecker.java deleted file mode 100644 index c81fc9d5..00000000 --- a/src/main/java/com/projectg/geyserupdater/common/util/SpigotResourceUpdateChecker.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.projectg.geyserupdater.common.util; - -import com.projectg.geyserupdater.common.logger.UpdaterLogger; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.Scanner; - -public class SpigotResourceUpdateChecker { - - private static final String VERSION_REGEX = "(\\d+.){1,2}\\d+"; - - /** - * Get the latest version of GeyserUpdater from the spigot resource page - * @return the latest version, null if there was an error. - */ - public static String getVersion() { - - try (InputStream inputStream = new URL("https://api.spigotmc.org/legacy/update.php?resource=88555").openStream(); Scanner scanner = new Scanner(inputStream)) { - StringBuilder builder = new StringBuilder(); - while (scanner.hasNext()) { - builder.append(scanner.next()); - } - String version = builder.toString(); - if (version.matches(VERSION_REGEX)) { - return version; - } else { - UpdaterLogger.getLogger().warn("Got unexpected string when checking Spigot resource page version: " + version); - return null; - } - } catch (IOException exception) { - UpdaterLogger.getLogger().error("Failed to check for updates: " + exception.getMessage()); - return null; - } - } -} diff --git a/src/main/java/com/projectg/geyserupdater/spigot/SpigotUpdater.java b/src/main/java/com/projectg/geyserupdater/spigot/SpigotUpdater.java deleted file mode 100644 index 913a0cdb..00000000 --- a/src/main/java/com/projectg/geyserupdater/spigot/SpigotUpdater.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.projectg.geyserupdater.spigot; - -import com.projectg.geyserupdater.common.logger.JavaUtilUpdaterLogger; -import com.projectg.geyserupdater.common.logger.UpdaterLogger; -import com.projectg.geyserupdater.common.util.FileUtils; -import com.projectg.geyserupdater.common.util.GeyserProperties; -import com.projectg.geyserupdater.spigot.command.GeyserUpdateCommand; -import com.projectg.geyserupdater.spigot.listeners.SpigotJoinListener; -import com.projectg.geyserupdater.spigot.util.CheckSpigotRestart; -import com.projectg.geyserupdater.spigot.util.GeyserSpigotDownloader; -import com.projectg.geyserupdater.common.util.SpigotResourceUpdateChecker; -import com.projectg.geyserupdater.spigot.util.bstats.Metrics; - -import org.bukkit.Bukkit; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.scheduler.BukkitRunnable; - -import java.io.File; -import java.io.IOException; -import java.util.Objects; - -public class SpigotUpdater extends JavaPlugin { - private static SpigotUpdater plugin; - - @Override - public void onEnable() { - plugin = this; - new JavaUtilUpdaterLogger(getLogger()); - new Metrics(this, 10202); - - loadConfig(); - if (getConfig().getBoolean("Enable-Debug", false)) { - UpdaterLogger.getLogger().info("Trying to enable debug logging."); - UpdaterLogger.getLogger().enableDebug(); - } - - checkConfigVersion(); - // Check our version - checkUpdaterVersion(); - - Objects.requireNonNull(getCommand("geyserupdate")).setExecutor(new GeyserUpdateCommand()); - getCommand("geyserupdate").setPermission("gupdater.geyserupdate"); - // Player alert if a restart is required when they join - Bukkit.getServer().getPluginManager().registerEvents(new SpigotJoinListener(), this); - - // Check if a restart script already exists - // We create one if it doesn't - if (getConfig().getBoolean("Auto-Script-Generating")) { - try { - CheckSpigotRestart.checkYml(); - } catch (Exception e) { - e.printStackTrace(); - } - } - // If true, start auto updating now and every 24 hours - if (getConfig().getBoolean("Auto-Update-Geyser")) { - scheduleAutoUpdate(); - } - // Enable File Checking here. delay of 30 minutes and period of 12 hours (given in ticks) - new BukkitRunnable() { - - @Override - public void run() { - if (FileUtils.checkFile("plugins/update/Geyser-Spigot.jar", false)) { - UpdaterLogger.getLogger().info("A new Geyser build has been downloaded! Please restart the server in order to use the updated build!"); - } - } - }.runTaskTimerAsynchronously(this, 30 * 60 * 20, 12 * 60 * 60 * 20); - } - - /** - * Load GeyserUpdater's config, create it if it doesn't exist - */ - private void loadConfig() { - File configFile = new File(getDataFolder(), "config.yml"); - if (!configFile.exists()) { - configFile.getParentFile().mkdirs(); - saveResource("config.yml", false); - } - FileConfiguration config = new YamlConfiguration(); - try { - config.load(configFile); - } catch (IOException | InvalidConfigurationException e) { - e.printStackTrace(); - } - } - - /** - * Check the config version of GeyserUpdater - */ - public void checkConfigVersion() { - //Change version number only when editing config.yml! - if (getConfig().getInt("Config-Version", 0) != 2) { - UpdaterLogger.getLogger().warn("Your copy of config.yml is outdated. Please delete it and let a fresh copy of config.yml be regenerated!"); - } - } - - /** - * Check the version of GeyserUpdater against the spigot resource page - */ - public void checkUpdaterVersion() { - UpdaterLogger logger = UpdaterLogger.getLogger(); - String pluginVersion = plugin.getDescription().getVersion(); - new BukkitRunnable() { - @Override - public void run() { - String latestVersion = SpigotResourceUpdateChecker.getVersion(); - if (latestVersion == null || latestVersion.length() == 0) { - logger.error("Failed to determine the latest GeyserUpdater version!"); - } else { - if (latestVersion.equals(pluginVersion)) { - logger.info("You are using the latest version of GeyserUpdater!"); - } else { - logger.info("Your version: " + pluginVersion + ". Latest version: " + latestVersion + ". Download the newer version at https://www.spigotmc.org/resources/geyserupdater.88555/."); - } - } - } - }.runTaskAsynchronously(this); - } - - /** - * Check for a newer version of Geyser every 24hrs - */ - public void scheduleAutoUpdate() { - UpdaterLogger.getLogger().debug("Scheduling auto updater"); - // todo: build this in different way so that we don't repeat it if the Auto-Update-Interval is zero or -1 or something - new BukkitRunnable() { - - @Override - public void run() { - UpdaterLogger.getLogger().debug("Checking if a new build of Geyser exists."); - try { - boolean isLatest = GeyserProperties.isLatestBuild(); - if (!isLatest) { - UpdaterLogger.getLogger().info("A newer build of Geyser is available! Attempting to download the latest build now..."); - GeyserSpigotDownloader.updateGeyser(); - } - } catch (IOException e) { - UpdaterLogger.getLogger().error("Failed to check for updates to Geyser! We were unable to reach the Geyser build server, or your local branch does not exist on it."); - e.printStackTrace(); - } - // Auto-Update-Interval is in hours. We convert it into ticks - } - }.runTaskTimer(this, 60 * 20, getConfig().getLong("Auto-Update-Interval", 24L) * 60 * 60 * 20); - } - - public static SpigotUpdater getPlugin() { - return plugin; - } -} diff --git a/src/main/java/com/projectg/geyserupdater/spigot/listeners/SpigotJoinListener.java b/src/main/java/com/projectg/geyserupdater/spigot/listeners/SpigotJoinListener.java deleted file mode 100644 index 6e21a573..00000000 --- a/src/main/java/com/projectg/geyserupdater/spigot/listeners/SpigotJoinListener.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.projectg.geyserupdater.spigot.listeners; - -import com.projectg.geyserupdater.common.util.FileUtils; - -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; - -public class SpigotJoinListener implements Listener { - - @EventHandler - public void onPlayerJoin(PlayerJoinEvent event) { - // We allow a cached result of maximum age 30 minutes to be used - if (FileUtils.checkFile("plugins/update/Geyser-Spigot.jar", true)) { - if (event.getPlayer().hasPermission("gupdater.geyserupdate")) { - event.getPlayer().sendMessage("[GeyserUpdater] A new Geyser build has been downloaded! Please restart the server in order to use the updated build!"); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/projectg/geyserupdater/spigot/util/GeyserSpigotDownloader.java b/src/main/java/com/projectg/geyserupdater/spigot/util/GeyserSpigotDownloader.java deleted file mode 100644 index 0cdbd4a1..00000000 --- a/src/main/java/com/projectg/geyserupdater/spigot/util/GeyserSpigotDownloader.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.projectg.geyserupdater.spigot.util; - -import com.projectg.geyserupdater.common.logger.UpdaterLogger; -import com.projectg.geyserupdater.common.util.FileUtils; -import com.projectg.geyserupdater.common.util.GeyserProperties; -import com.projectg.geyserupdater.spigot.SpigotUpdater; - -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; - -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -public class GeyserSpigotDownloader { - private static SpigotUpdater plugin; - private static UpdaterLogger logger; - - /** - * Download the latest build of Geyser from Jenkins CI for the currently used branch. - * If enabled in the config, the server will also attempt to restart. - */ - public static void updateGeyser() { - plugin = SpigotUpdater.getPlugin(); - logger = UpdaterLogger.getLogger(); - - UpdaterLogger.getLogger().debug("Attempting to download a new build of Geyser."); - - boolean doRestart = plugin.getConfig().getBoolean("Auto-Restart-Server"); - - // Start the process async - new BukkitRunnable() { - @Override - public void run() { - // Download the newest build and store the success state - boolean downloadSuccess = downloadGeyser(); - // No additional code should be run after the following BukkitRunnable - // Run it synchronously because it isn't thread-safe - new BukkitRunnable() { - @Override - public void run() { - if (downloadSuccess) { - String successMsg = "The latest build of Geyser has been downloaded! A restart must occur in order for changes to take effect."; - logger.info(successMsg); - for (Player player : Bukkit.getOnlinePlayers()) { - if (player.hasPermission("gupdater.geyserupdate")) { - player.sendMessage(ChatColor.GREEN + successMsg); - } - } - if (doRestart) { - restartServer(); - } - } else { - // fail messages are already sent to the logger in downloadGeyser() - String failMsg = "A error(); error occurred when download a new build of Geyser. Please check the server console for further information!"; - for (Player player : Bukkit.getOnlinePlayers()) { - if (player.hasPermission("gupdater.geyserupdate")) { - player.sendMessage(ChatColor.RED + failMsg); - } - } - } - } - }.runTask(plugin); - } - }.runTaskAsynchronously(plugin); - } - - /** - * Internal code for downloading the latest build of Geyser from Jenkins CI for the currently used branch. - * - * @return true if the download was successful, false if not. - */ - private static boolean downloadGeyser() { - String fileUrl; - try { - fileUrl = "https://ci.opencollab.dev/job/GeyserMC/job/Geyser/job/" + GeyserProperties.getGeyserGitPropertiesValue("git.branch") + "/lastSuccessfulBuild/artifact/bootstrap/spigot/target/Geyser-Spigot.jar"; - } catch (IOException e) { - logger.error("Failed to get the current Geyser branch when attempting to download a new build of Geyser!"); - e.printStackTrace(); - return false; - } - // todo: make sure we use the update folder defined in bukkit.yml (it can be changed) - String outputPath = "plugins/update/Geyser-Spigot.jar"; - try { - FileUtils.downloadFile(fileUrl, outputPath); - } catch (IOException e) { - logger.error("Failed to download the newest build of Geyser"); - e.printStackTrace(); - return false; - } - - if (!FileUtils.checkFile(outputPath, false)) { - logger.error("Failed to find the downloaded Geyser build!"); - return false; - } else { - return true; - } - } - - /** - * Attempt to restart the server - */ - private static void restartServer() { - logger.warn("The server will be restarting in 10 seconds!"); - for (Player player : Bukkit.getOnlinePlayers()) { - player.sendMessage(ChatColor.translateAlternateColorCodes('&', plugin.getConfig().getString("Restart-Message-Players"))); - } - // Attempt to restart the server 10 seconds after the message - new BukkitRunnable() { - @Override - public void run() { - try { - Object spigotServer; - try { - spigotServer = SpigotUpdater.getPlugin().getServer().getClass().getMethod("spigot").invoke(SpigotUpdater.getPlugin().getServer()); - } catch (NoSuchMethodException e) { - logger.error("You are not running Spigot (or a fork of it, such as Paper)! GeyserUpdater cannot automatically restart your server!"); - e.printStackTrace(); - return; - } - Method restartMethod = spigotServer.getClass().getMethod("restart"); - restartMethod.setAccessible(true); - restartMethod.invoke(spigotServer); - } catch (NoSuchMethodException e) { - logger.error("Your server version is too old to be able to be automatically restarted!"); - e.printStackTrace(); - } catch (InvocationTargetException | IllegalAccessException e) { - logger.error("Failed to restart the server!"); - e.printStackTrace(); - } - } - }.runTaskLater(plugin, 200); // 200 ticks is around 10 seconds (at 20 TPS) - } -} \ No newline at end of file diff --git a/src/main/java/com/projectg/geyserupdater/velocity/VelocityUpdater.java b/src/main/java/com/projectg/geyserupdater/velocity/VelocityUpdater.java deleted file mode 100644 index 00a61959..00000000 --- a/src/main/java/com/projectg/geyserupdater/velocity/VelocityUpdater.java +++ /dev/null @@ -1,249 +0,0 @@ -package com.projectg.geyserupdater.velocity; - -import com.projectg.geyserupdater.common.logger.UpdaterLogger; -import com.projectg.geyserupdater.common.util.FileUtils; -import com.projectg.geyserupdater.common.util.GeyserProperties; -import com.projectg.geyserupdater.common.util.ScriptCreator; -import com.projectg.geyserupdater.velocity.command.GeyserUpdateCommand; -import com.projectg.geyserupdater.velocity.listeners.VelocityJoinListener; -import com.projectg.geyserupdater.velocity.logger.Slf4jUpdaterLogger; -import com.projectg.geyserupdater.velocity.util.GeyserVelocityDownloader; -import com.projectg.geyserupdater.velocity.util.bstats.Metrics; - -import com.google.inject.Inject; - -import com.moandjiezana.toml.Toml; - -import org.geysermc.connector.GeyserConnector; -import org.slf4j.Logger; - -import com.velocitypowered.api.event.PostOrder; -import com.velocitypowered.api.event.Subscribe; -import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; -import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; -import com.velocitypowered.api.plugin.Dependency; -import com.velocitypowered.api.plugin.Plugin; -import com.velocitypowered.api.plugin.annotation.DataDirectory; -import com.velocitypowered.api.proxy.ProxyServer; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.concurrent.TimeUnit; - -@Plugin(id = "geyserupdater", name = "GeyserUpdater", version = "1.5.0", description = "Automatically or manually downloads new builds of Geyser and applies them on server restart.", authors = {"Jens"}, - dependencies = {@Dependency(id = "geyser")}) -public class VelocityUpdater { - - private static VelocityUpdater plugin; - private final ProxyServer server; - private final Logger baseLogger; - private final Path dataDirectory; - private final Toml config; - private final Metrics.Factory metricsFactory; - - @Inject - public VelocityUpdater(ProxyServer server, Logger baseLogger, @DataDirectory final Path folder, Metrics.Factory metricsFactory) { - VelocityUpdater.plugin = this; - this.server = server; - this.baseLogger = baseLogger; - this.dataDirectory = folder; - this.config = loadConfig(dataDirectory); - this.metricsFactory = metricsFactory; - } - - @Subscribe - public void onProxyInitialization(ProxyInitializeEvent event) { - metricsFactory.make(this, 10673); - new Slf4jUpdaterLogger(baseLogger); - - if (getConfig().getBoolean("Enable-Debug", false)) { - UpdaterLogger.getLogger().info("Trying to enable debug logging."); - UpdaterLogger.getLogger().enableDebug(); - } - - checkConfigVersion(); - // todo: meta version checking - - // Register our only command - server.getCommandManager().register("geyserupdate", new GeyserUpdateCommand()); - // Player alert if a restart is required when they join - server.getEventManager().register(this, new VelocityJoinListener()); - - // Make startup script if enabled - if (config.getBoolean("Auto-Script-Generating")) { - try { - ScriptCreator.createRestartScript(true); - } catch (IOException e) { - e.printStackTrace(); - } - } - // Auto update Geyser if enabled in the config - if (config.getBoolean("Auto-Update-Geyser")) { - scheduleAutoUpdate(); - } - // Check if downloaded Geyser file exists periodically - server.getScheduler() - .buildTask(this, () -> { - FileUtils.checkFile("plugins/GeyserUpdater/BuildUpdate/Geyser-Velocity.jar", true); - UpdaterLogger.getLogger().info("A new Geyser build has been downloaded! Please restart Velocity in order to use the updated build!"); - }) - .delay(30L, TimeUnit.MINUTES) - .repeat(12L, TimeUnit.HOURS) - .schedule(); - } - - @Subscribe(order = PostOrder.LAST) - public void onShutdown(ProxyShutdownEvent event) { - // This test isn't ideal but it'll work for now - if (!GeyserConnector.getInstance().getBedrockServer().isClosed()) { - throw new UnsupportedOperationException("Cannot shutdown GeyserUpdater before Geyser has shutdown! No updates will be applied."); - } - try { - moveGeyserJar(); - for (int i = 0; i <= 2; i++) { - try { - deleteGeyserJar(); - break; - } catch (IOException ioException) { - UpdaterLogger.getLogger().warn("An I/O error occurred while attempting to delete an unnecessary Geyser jar! Trying again " + (2 - i) + " more times."); - ioException.printStackTrace(); - try { - Thread.sleep(50); - } catch (InterruptedException interruptException) { - UpdaterLogger.getLogger().error("Failed to delay an additional attempt!"); - interruptException.printStackTrace(); - } - } - } - } catch (IOException e) { - UpdaterLogger.getLogger().error("An I/O error occurred while attempting to replace the current Geyser jar with the new one!"); - e.printStackTrace(); - } - } - - /** - * Load GeyserUpdater's config - * - * @param path The config's directory - * @return The configuration - */ - private Toml loadConfig(Path path) { - File folder = path.toFile(); - File file = new File(folder, "config.toml"); - - if (!file.exists()) { - if (!file.getParentFile().exists()) { - file.getParentFile().mkdirs(); - } - try (InputStream input = getClass().getResourceAsStream("/" + file.getName())) { - if (input != null) { - Files.copy(input, file.toPath()); - } else { - file.createNewFile(); - } - } catch (IOException exception) { - exception.printStackTrace(); - return null; - } - } - return new Toml().read(file); - } - - /** - * Check the config version of GeyserUpdater - */ - public void checkConfigVersion() { - //Change version number only when editing config.yml! - if (getConfig().getLong("Config-Version", 0L).compareTo(2L) != 0) { - UpdaterLogger.getLogger().warn("Your copy of config.yml is outdated. Please delete it and let a fresh copy of config.yml be regenerated!"); - } - } - - /** - * Check for a newer version of Geyser every 24hrs - */ - public void scheduleAutoUpdate() { - UpdaterLogger.getLogger().debug("Scheduling auto updater"); - // Checking for the build numbers of current build. - // todo: build this in different way so that we don't repeat it if the Auto-Update-Interval is zero or -1 or something - server.getScheduler() - .buildTask(this, () -> { - UpdaterLogger.getLogger().debug("Checking if a new build of Geyser exists."); - try { - boolean isLatest = GeyserProperties.isLatestBuild(); - if (!isLatest) { - UpdaterLogger.getLogger().info("A newer build of Geyser is available! Attempting to download the latest build now..."); - GeyserVelocityDownloader.updateGeyser(); - } - } catch (IOException e) { - UpdaterLogger.getLogger().error("Failed to check for updates to Geyser! We were unable to reach the Geyser build server, or your local branch does not exist on it."); - e.printStackTrace(); - } - }) - .delay(1L, TimeUnit.MINUTES) - .repeat(getConfig().getLong("Auto-Update-Interval", 24L), TimeUnit.HOURS) - .schedule(); - } - - /** - * Replace the Geyser jar in the plugin folder with the one in GeyserUpdater/BuildUpdate - * Should only be called once Geyser has been disabled - * - * @throws IOException if there was an IO failure - */ - public void moveGeyserJar() throws IOException { - // Moving Geyser Jar to Plugins folder "Overwriting". - File fileToCopy = new File("plugins/GeyserUpdater/BuildUpdate/Geyser-Velocity.jar"); - if (fileToCopy.exists()) { - UpdaterLogger.getLogger().debug("Moving the new Geyser jar to the plugins folder."); - FileInputStream input = new FileInputStream(fileToCopy); - File newFile = new File("plugins/Geyser-Velocity.jar"); - FileOutputStream output = new FileOutputStream(newFile); - byte[] buf = new byte[1024]; - int bytesRead; - while ((bytesRead = input.read(buf)) > 0) { - output.write(buf, 0, bytesRead); - } - input.close(); - output.close(); - } else { - UpdaterLogger.getLogger().debug("Found no new Geyser jar to copy to the plugins folder."); - } - } - - /** - * Delete the Geyser jar in GeyserUpdater/BuildUpdate - * - * @throws IOException if it failed to delete - */ - private void deleteGeyserJar() throws IOException { - UpdaterLogger.getLogger().debug("Deleting the Geyser jar in the BuildUpdate folder if it exists"); - Path file = Paths.get("plugins/GeyserUpdater/BuildUpdate/Geyser-Velocity.jar"); - Files.deleteIfExists(file); - } - - public static VelocityUpdater getPlugin() { - return plugin; - } - public ProxyServer getProxyServer() { - return server; - } - public Path getDataDirectory() { - return dataDirectory; - } - public Toml getConfig() { - return config; - } -} - - - - - - diff --git a/src/main/java/com/projectg/geyserupdater/velocity/listeners/VelocityJoinListener.java b/src/main/java/com/projectg/geyserupdater/velocity/listeners/VelocityJoinListener.java deleted file mode 100644 index 70b404fa..00000000 --- a/src/main/java/com/projectg/geyserupdater/velocity/listeners/VelocityJoinListener.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.projectg.geyserupdater.velocity.listeners; - -import com.projectg.geyserupdater.common.util.FileUtils; - -import com.velocitypowered.api.event.Subscribe; -import com.velocitypowered.api.event.connection.PostLoginEvent; - -import net.kyori.adventure.text.Component; - -public class VelocityJoinListener { - - @Subscribe - public void onPostLogin(PostLoginEvent event) { - // We allow a cached result of maximum age 30 minutes to be used - if (FileUtils.checkFile("plugins/GeyserUpdater/BuildUpdate/Geyser-Velocity.jar",true)) { - if (event.getPlayer().hasPermission("gupdater.geyserupdate")) { - event.getPlayer().sendMessage(Component.text("[GeyserUpdater] A new Geyser build has been downloaded! Please restart Velocity in order to use the updated build!")); - } - } - } -} diff --git a/src/main/java/com/projectg/geyserupdater/velocity/util/GeyserVelocityDownloader.java b/src/main/java/com/projectg/geyserupdater/velocity/util/GeyserVelocityDownloader.java deleted file mode 100644 index a6087c9e..00000000 --- a/src/main/java/com/projectg/geyserupdater/velocity/util/GeyserVelocityDownloader.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.projectg.geyserupdater.velocity.util; - -import com.projectg.geyserupdater.common.logger.UpdaterLogger; -import com.projectg.geyserupdater.common.util.FileUtils; -import com.projectg.geyserupdater.common.util.GeyserProperties; -import com.projectg.geyserupdater.velocity.VelocityUpdater; - -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.api.proxy.ProxyServer; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.TextColor; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -public class GeyserVelocityDownloader { - private static VelocityUpdater plugin; - private static ProxyServer server; - private static UpdaterLogger logger; - - /** - * Download the latest build of Geyser from Jenkins CI for the currently used branch. - * If enabled in the config, the server will also attempt to restart. - */ - public static void updateGeyser() { - plugin = VelocityUpdater.getPlugin(); - server = plugin.getProxyServer(); - logger = UpdaterLogger.getLogger(); - - UpdaterLogger.getLogger().debug("Attempting to download a new build of Geyser."); - - // New task so that we don't block the main thread. All new tasks on velocity are async. - plugin.getProxyServer().getScheduler().buildTask(plugin, () -> { - // Download the newest geyser build - // todo: do the colour codes for the Adventure text formatting work? - if (downloadGeyser()) { - String successMsg = "The latest build of Geyser has been downloaded! A restart must occur in order for changes to take effect."; - logger.info(successMsg); - for (Player player : server.getAllPlayers()) { - if (player.hasPermission("gupdater.geyserupdate")) { - player.sendMessage(Component.text(successMsg).color(TextColor.fromHexString("55FF55"))); - } - } - if (plugin.getConfig().getBoolean("Auto-Restart-Server")) { - restartServer(); - } - } else { - // fail messages are already sent to the logger in downloadGeyser() - String failMsg = "A severe error occurred when download a new build of Geyser. Please check the server console for further information!"; - for (Player player : server.getAllPlayers()) { - if (player.hasPermission("gupdater.geyserupdate")) { - player.sendMessage(Component.text(failMsg).color(TextColor.fromHexString("AA0000"))); - } - } - } - }).schedule(); - } - - /** - * Internal code for downloading the latest build of Geyser from Jenkins CI for the currently used branch. - * - * @return true if the download was successful, false if not. - */ - private static boolean downloadGeyser() { - String fileUrl; - try { - fileUrl = "https://ci.opencollab.dev/job/GeyserMC/job/Geyser/job/" + GeyserProperties.getGeyserGitPropertiesValue("git.branch") + "/lastSuccessfulBuild/artifact/bootstrap/velocity/target/Geyser-Velocity.jar"; - } catch (IOException e) { - logger.error("Failed to get the current Geyser branch when attempting to download a new build of Geyser!"); - e.printStackTrace(); - return false; - } - String outputPath = "plugins/GeyserUpdater/BuildUpdate/Geyser-Velocity.jar"; - try { - FileUtils.downloadFile(fileUrl, outputPath); - } catch (IOException e) { - logger.error("Failed to download the newest build of Geyser"); - e.printStackTrace(); - return false; - } - - if (!FileUtils.checkFile(outputPath, false)) { - logger.error("Failed to find the downloaded Geyser build!"); - return false; - } else { - return true; - } - } - /** - * Attempt to restart the server - */ - private static void restartServer() { - logger.warn("The server will be restarting in 10 seconds!"); - for (Player player : server.getAllPlayers()) { - player.sendMessage(Component.text(plugin.getConfig().getString("Restart-Message-Players"))); - } - server.getScheduler() - .buildTask(plugin, server::shutdown) - .delay(10L, TimeUnit.SECONDS) - .schedule(); - } -} - diff --git a/src/main/java/dev/projectg/geyserupdater/bungee/BungeePlayerHandler.java b/src/main/java/dev/projectg/geyserupdater/bungee/BungeePlayerHandler.java new file mode 100644 index 00000000..3f426d26 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/bungee/BungeePlayerHandler.java @@ -0,0 +1,28 @@ +package dev.projectg.geyserupdater.bungee; + +import dev.projectg.geyserupdater.common.PlayerManager; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class BungeePlayerHandler implements PlayerManager { + + @Override + public @NotNull List getOnlinePlayers() { + List uuidList = new ArrayList<>(); + for (ProxiedPlayer player : ProxyServer.getInstance().getPlayers()) { + uuidList.add(player.getUniqueId()); + } + return uuidList; + } + + @Override + public void sendMessage(@NotNull UUID uuid, @NotNull String message) { + ProxyServer.getInstance().getPlayer(uuid).sendMessage(new TextComponent(message)); + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/bungee/BungeeScheduler.java b/src/main/java/dev/projectg/geyserupdater/bungee/BungeeScheduler.java new file mode 100644 index 00000000..94387678 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/bungee/BungeeScheduler.java @@ -0,0 +1,43 @@ +package dev.projectg.geyserupdater.bungee; + +import dev.projectg.geyserupdater.common.scheduler.Task; +import dev.projectg.geyserupdater.common.scheduler.UpdaterScheduler; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.api.scheduler.ScheduledTask; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nonnull; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +public class BungeeScheduler implements UpdaterScheduler { + + private final Plugin plugin; + + public BungeeScheduler(@Nonnull Plugin plugin) { + Objects.requireNonNull(plugin); + this.plugin = plugin; + } + + @Override + public Task schedule(@NotNull Runnable runnable, boolean async, long delay, long repeat, TimeUnit unit) { + // https://github.com/SpigotMC/BungeeCord/blob/master/proxy/src/main/java/net/md_5/bungee/scheduler/BungeeTask.java + + Objects.requireNonNull(runnable); + + return new BungeeTask(plugin.getProxy().getScheduler().schedule(plugin, runnable, delay, repeat, unit)); + } + + private static class BungeeTask implements Task { + private final ScheduledTask task; + + public BungeeTask(ScheduledTask task) { + this.task = task; + } + + @Override + public void cancel() { + task.cancel(); + } + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/bungee/BungeeUpdater.java b/src/main/java/dev/projectg/geyserupdater/bungee/BungeeUpdater.java new file mode 100644 index 00000000..2d29e49c --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/bungee/BungeeUpdater.java @@ -0,0 +1,66 @@ +package dev.projectg.geyserupdater.bungee; + +import dev.projectg.geyserupdater.bungee.command.GeyserUpdateCommand; +import dev.projectg.geyserupdater.bungee.bstats.Metrics; +import dev.projectg.geyserupdater.common.GeyserUpdater; +import dev.projectg.geyserupdater.common.UpdaterBootstrap; +import dev.projectg.geyserupdater.common.logger.JavaUtilUpdaterLogger; +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; + +import dev.projectg.geyserupdater.common.util.ScriptCreator; +import net.md_5.bungee.api.plugin.Plugin; +import space.arim.dazzleconf.error.InvalidConfigException; + +import java.io.IOException; +import java.nio.file.Path; + +public class BungeeUpdater extends Plugin implements UpdaterBootstrap { + + private GeyserUpdater updater; + + @Override + public void onEnable() { + Path dataFolder = this.getDataFolder().toPath(); + try { + updater = new GeyserUpdater( + dataFolder, + dataFolder.resolve("BuildUpdate"), + this.getProxy().getPluginsFolder().toPath(), + this, + new JavaUtilUpdaterLogger(getLogger()), + new BungeeScheduler(this), + new BungeePlayerHandler(), + this.getDescription().getVersion(), + "bootstrap/bungeecord/target/Geyser-BungeeCord.jar", + "bootstrap/bungee/target/floodgate-bungee.jar" + ); + } catch (IOException | InvalidConfigException e) { + getLogger().severe("Failed to start GeyserUpdater! Disabling..."); + e.printStackTrace(); + return; + } + + // Ensure we get disabled last + new PluginMapModifier().run(updater.getLogger(), this); + + this.getProxy().getPluginManager().registerCommand(this, new GeyserUpdateCommand()); + new Metrics(this, 10203); + } + + @Override + public void onDisable() { + if (updater != null) { + try { + updater.shutdown(); + } catch (IOException e) { + UpdaterLogger.getLogger().error("Failed to install ALL updates:"); + e.printStackTrace(); + } + } + } + + @Override + public void createRestartScript() throws IOException { + ScriptCreator.createRestartScript(true); + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/bungee/PluginMapModifier.java b/src/main/java/dev/projectg/geyserupdater/bungee/PluginMapModifier.java new file mode 100644 index 00000000..e6f0eabb --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/bungee/PluginMapModifier.java @@ -0,0 +1,83 @@ +package dev.projectg.geyserupdater.bungee; + +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; +import dev.projectg.geyserupdater.common.util.ReflectionUtils; +import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.api.plugin.PluginManager; +import net.md_5.bungee.api.scheduler.ScheduledTask; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * A wrapper class for {@link this#run(UpdaterLogger, Plugin)}. + * It is wrapped to hide a {@link ScheduledTask} field, which is required for a Task which modifies the Plugin LinkedHashMap at the right moment to cancel itself. + */ +public class PluginMapModifier { + + private ScheduledTask task = null; + + private final String resultMessage = "This may result in errors and inability to apply updates during the shutdown process."; + + /** + * Place a {@link Plugin} at the top of BungeeCord's LinkedHashMap of plugins. This will result in the plugin starting in the normal order, but being disabled last. + * This method is meant to be called within {@link Plugin#onEnable()} + * @param logger The Logger to use for messages + * @param plugin The Plugin to place at the top + */ + protected void run(UpdaterLogger logger, Plugin plugin) { + // https://github.com/SpigotMC/BungeeCord/blob/master/proxy/src/main/java/net/md_5/bungee/BungeeCord.java + BungeeCord bungeeCord = BungeeCord.getInstance(); + PluginManager pluginManager = bungeeCord.getPluginManager(); + + Collection listeners; + LinkedHashMap plugins; + try { + listeners = ReflectionUtils.getFieldValue(bungeeCord, Collection.class, "listeners"); + plugins = ReflectionUtils.getFieldValue(pluginManager, LinkedHashMap.class, "plugins"); + } catch (IllegalAccessException | NoSuchFieldException | ClassCastException e) { + logger.error("Failed to get necessary private fields to modify BungeeCord's plugin list order"); + logger.error(resultMessage); + e.printStackTrace(); + return; + } + + // modify the plugin map once it is no longer being iterated over by BungeeCord to avoid ConcurrentModificationException. + // onEnable() is called by BungeeCord when iterating over the plugin list, so we must modify it after bungeecord is done enabling ALL plugins + // the listeners List is populated after all plugin enabling is finished. + task = bungeeCord.getScheduler().schedule(plugin, () -> { + if (bungeeCord.isRunning) { + if (!listeners.isEmpty()) { + LinkedHashMap sortedPlugins = new LinkedHashMap<>(); + String updaterName = plugin.getDescription().getName(); + sortedPlugins.put(updaterName, plugins.get(updaterName)); // put ourselves at the very start, which means we disable last + sortedPlugins.putAll(plugins); // put the rest of the plugins in the default order + + Set originalOrder = null; + if (logger.isDebug()) { + originalOrder = plugins.keySet(); + } + + synchronized (plugins) { + plugins.clear(); + plugins.putAll(sortedPlugins); + } + + logger.info("Successfully modified the order of BungeeCord's plugin Map."); + if (logger.isDebug()) { + logger.debug("Original order: " + originalOrder); + logger.debug("New order: " + plugins.keySet()); + } + task.cancel(); + } + } else { + logger.error("BungeeCord began shutdown before we were able to modify the plugin list order!"); + logger.error(resultMessage); + task.cancel(); + } + }, 2, 5, TimeUnit.SECONDS); + } +} diff --git a/src/main/java/com/projectg/geyserupdater/bungee/util/bstats/Metrics.java b/src/main/java/dev/projectg/geyserupdater/bungee/bstats/Metrics.java similarity index 99% rename from src/main/java/com/projectg/geyserupdater/bungee/util/bstats/Metrics.java rename to src/main/java/dev/projectg/geyserupdater/bungee/bstats/Metrics.java index f9579f84..e1ac07c6 100644 --- a/src/main/java/com/projectg/geyserupdater/bungee/util/bstats/Metrics.java +++ b/src/main/java/dev/projectg/geyserupdater/bungee/bstats/Metrics.java @@ -1,4 +1,4 @@ -package com.projectg.geyserupdater.bungee.util.bstats; +package dev.projectg.geyserupdater.bungee.bstats; import com.google.gson.JsonArray; import com.google.gson.JsonObject; diff --git a/src/main/java/com/projectg/geyserupdater/bungee/command/GeyserUpdateCommand.java b/src/main/java/dev/projectg/geyserupdater/bungee/command/GeyserUpdateCommand.java similarity index 78% rename from src/main/java/com/projectg/geyserupdater/bungee/command/GeyserUpdateCommand.java rename to src/main/java/dev/projectg/geyserupdater/bungee/command/GeyserUpdateCommand.java index 9988beed..f2ea76b6 100644 --- a/src/main/java/com/projectg/geyserupdater/bungee/command/GeyserUpdateCommand.java +++ b/src/main/java/dev/projectg/geyserupdater/bungee/command/GeyserUpdateCommand.java @@ -1,18 +1,10 @@ -package com.projectg.geyserupdater.bungee.command; +package dev.projectg.geyserupdater.bungee.command; -import com.projectg.geyserupdater.bungee.util.GeyserBungeeDownloader; -import com.projectg.geyserupdater.common.Messages; -import com.projectg.geyserupdater.common.logger.UpdaterLogger; -import com.projectg.geyserupdater.common.util.GeyserProperties; +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; -import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.CommandSender; -import net.md_5.bungee.api.chat.TextComponent; -import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.plugin.Command; -import java.io.IOException; - public class GeyserUpdateCommand extends Command { @@ -23,6 +15,10 @@ public GeyserUpdateCommand() { public void execute(CommandSender commandSender, String[] args) { UpdaterLogger logger = UpdaterLogger.getLogger(); + //todo: bungee command + + /* + if (commandSender instanceof ProxiedPlayer) { ProxiedPlayer player = (ProxiedPlayer) commandSender; try { @@ -40,7 +36,6 @@ public void execute(CommandSender commandSender, String[] args) { e.printStackTrace(); } } else { - // TODO: filter this against command blocks try { logger.info(Messages.Command.CHECK_START); boolean isLatest = GeyserProperties.isLatestBuild(); @@ -55,5 +50,7 @@ public void execute(CommandSender commandSender, String[] args) { e.printStackTrace(); } } + + */ } } \ No newline at end of file diff --git a/src/main/java/dev/projectg/geyserupdater/common/GeyserUpdater.java b/src/main/java/dev/projectg/geyserupdater/common/GeyserUpdater.java new file mode 100644 index 00000000..177c1740 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/GeyserUpdater.java @@ -0,0 +1,166 @@ +package dev.projectg.geyserupdater.common; + +import dev.projectg.geyserupdater.common.config.Configurator; +import dev.projectg.geyserupdater.common.config.UpdaterConfiguration; +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; +import dev.projectg.geyserupdater.common.scheduler.UpdaterScheduler; +import dev.projectg.geyserupdater.common.update.PluginId; +import dev.projectg.geyserupdater.common.update.Updatable; +import dev.projectg.geyserupdater.common.update.UpdateManager; +import dev.projectg.geyserupdater.common.util.SpigotUtils; +import space.arim.dazzleconf.error.InvalidConfigException; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + + +public class GeyserUpdater { + + private static GeyserUpdater INSTANCE = null; + + public final String version; + + private final Path downloadFolder; + private final Path installFolder; + private final UpdaterLogger logger; + private final UpdaterScheduler scheduler; + private final PlayerManager playerManager; + private final UpdateManager updateManager; + private final UpdaterConfiguration config; + + /** + * @param dataFolder The data folder for GeyserUpdater + * @param downloadFolder The default directory to download updates to. Updatables may override it. + * @param installFolder The default directory to move downloads to whenever GeyserUpdater is shown down. Updatables may override it. + * @param bootstrap The {@link UpdaterBootstrap} platform implemenetation + * @param logger The {@link UpdaterLogger} platform implemenetation + * @param scheduler The {@link UpdaterScheduler} platform implemenetation + * @param playerManager The {@link PlayerManager} platform implemenetation + * @param version The version of GeyserUpdater + * @param geyserArtifact The artifact link for Geyser. For example: "bootstrap/velocity/target/Geyser-Velocity.jar" + * @param floodgateArtifact The artifact link for Floodgate. For example: "bootstrap/velocity/target/floodgate-velocity.jar" + * @throws IOException If there was an exception loading the config. {@link this#shutdown()} Should not be called if this exception is thrown. + */ + public GeyserUpdater(Path dataFolder, + Path downloadFolder, + Path installFolder, + UpdaterBootstrap bootstrap, + UpdaterLogger logger, + UpdaterScheduler scheduler, + PlayerManager playerManager, + String version, + String geyserArtifact, + String floodgateArtifact) throws IOException, InvalidConfigException { + this.downloadFolder = downloadFolder; + this.installFolder = installFolder; + this.logger = logger; + this.scheduler = scheduler; + this.playerManager = playerManager; + this.version = version; + + INSTANCE = this; + + // Meta version checking + scheduler.run(() -> { + String latestVersion = SpigotUtils.getVersion(88555); + if (latestVersion == null || latestVersion.isEmpty()) { + logger.error("Failed to determine the latest GeyserUpdater version!"); + } else { + if (latestVersion.equals(version)) { + logger.info("You are using the latest version of GeyserUpdater!"); + } else { + logger.warn("Your version: " + version + ". Latest version: " + latestVersion + ". Download the newer version at https://www.spigotmc.org/resources/geyserupdater.88555/"); + } + } + }, true); + + // Load the config + config = Configurator.loadConfig(dataFolder.resolve("config.yml")); + if (UpdaterConfiguration.DEFAULT_VERSION != config.version()) { + throw new IllegalStateException("Your copy of config.yml is outdated (your version: " + config.version() + ", latest version: " + UpdaterConfiguration.DEFAULT_VERSION + "). Please delete it and let a fresh copy of config.yml be regenerated!"); + } + if (config.enableDebug()) { + logger.enableDebug(); + } + + // Make startup script if enabled + if (config.generateRestartScript()) { + try { + logger.debug("Attempting to create restart script"); + bootstrap.createRestartScript(); + } catch (IOException e) { + logger.error("Error while creating restart script:"); + e.printStackTrace(); + } + } + + // Set the enable/autoCheck/autoUpdate values + PluginId.loadSettings(config); + + // Set the correct download links for geyser and floodgate + PluginId.GEYSER.setArtifact(geyserArtifact); + PluginId.FLOODGATE.setArtifact(floodgateArtifact); + + // Manager for updating plugins + this.updateManager = new UpdateManager(downloadFolder, scheduler, config); + + // todo: restart after updates + // todo: better message sending, to players too + } + + /** + * Installs all updates to the correct folder, if necessary. Will do nothing if the downloadFolder is the same file as the installFolder. + * @throws IOException If there was a failure moving ALL updates. + */ + public void shutdown() throws IOException { + updateManager.shutdown(); //fixme: wait for the last download to finish, or cancel it and delete the unfinished file before copying files + + UpdaterLogger.getLogger().debug("Installing plugins from the cache."); + Files.createDirectories(installFolder); + + // Only move files that we have tracked + for (Updatable updatable : updateManager.getTrackedUpdatables()) { + Path update = updatable.outputFile; + if (Files.exists(update)) { + try { + if (Files.isSameFile(update.getParent(), installFolder)) { + // it is already where it should be, don't move it + continue; + } + } catch (IOException e) { + logger.warn("Failed to check if the install folder is the same as the download folder for " + updatable + ". Attempting to move files from the downloadFolder to the installFolder anyway..."); + if (logger.isDebug()) { + e.printStackTrace(); + } + } + + try { + Files.move(update, installFolder.resolve(update.getFileName()), StandardCopyOption.REPLACE_EXISTING); + logger.debug("Moved " + updatable.outputFile + " to " + installFolder); + } catch (IOException e) { + UpdaterLogger.getLogger().error("Failed to copy update file " + updatable + " to directory " + installFolder); + e.printStackTrace(); + } + } + } + } + + public static GeyserUpdater getInstance() { + return INSTANCE; + } + public UpdaterConfiguration getConfig() { + return config; + } + public UpdaterLogger getLogger() { + return logger; + } + public UpdaterScheduler getScheduler() { + return scheduler; + } + + public PlayerManager getPlayerHandler() { + return playerManager; + } +} diff --git a/src/main/java/com/projectg/geyserupdater/common/Messages.java b/src/main/java/dev/projectg/geyserupdater/common/Messages.java similarity index 92% rename from src/main/java/com/projectg/geyserupdater/common/Messages.java rename to src/main/java/dev/projectg/geyserupdater/common/Messages.java index 72d0faae..4aa965a9 100644 --- a/src/main/java/com/projectg/geyserupdater/common/Messages.java +++ b/src/main/java/dev/projectg/geyserupdater/common/Messages.java @@ -1,4 +1,4 @@ -package com.projectg.geyserupdater.common; +package dev.projectg.geyserupdater.common; public class Messages { diff --git a/src/main/java/dev/projectg/geyserupdater/common/PlayerManager.java b/src/main/java/dev/projectg/geyserupdater/common/PlayerManager.java new file mode 100644 index 00000000..44d3d79d --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/PlayerManager.java @@ -0,0 +1,16 @@ +package dev.projectg.geyserupdater.common; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.UUID; + +public interface PlayerManager { + + @Nonnull + List getOnlinePlayers(); + + void sendMessage(@Nonnull UUID uuid, @NotNull String message); + +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/UpdaterBootstrap.java b/src/main/java/dev/projectg/geyserupdater/common/UpdaterBootstrap.java new file mode 100644 index 00000000..fd14d194 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/UpdaterBootstrap.java @@ -0,0 +1,11 @@ +package dev.projectg.geyserupdater.common; + +import java.io.IOException; + +public interface UpdaterBootstrap { + + void onDisable(); + + void createRestartScript() throws IOException; + +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/command/CommandManager.java b/src/main/java/dev/projectg/geyserupdater/common/command/CommandManager.java new file mode 100644 index 00000000..ab688e9b --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/command/CommandManager.java @@ -0,0 +1,14 @@ +package dev.projectg.geyserupdater.common.command; + +public class CommandManager { + + + + public CommandManager() { + + } + + public boolean process(CommandSender sender, String cmd, String[] args) { + return false; + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/command/CommandSender.java b/src/main/java/dev/projectg/geyserupdater/common/command/CommandSender.java new file mode 100644 index 00000000..8612838c --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/command/CommandSender.java @@ -0,0 +1,12 @@ +package dev.projectg.geyserupdater.common.command; + +import dev.projectg.geyserupdater.common.GeyserUpdater; +import dev.projectg.geyserupdater.common.PlayerManager; + +public class CommandSender { + + public void sendMessage() { + PlayerManager handler = GeyserUpdater.getInstance().getPlayerHandler(); + + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/command/UpdaterCommand.java b/src/main/java/dev/projectg/geyserupdater/common/command/UpdaterCommand.java new file mode 100644 index 00000000..7c2bc181 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/command/UpdaterCommand.java @@ -0,0 +1,6 @@ +package dev.projectg.geyserupdater.common.command; + +public interface UpdaterCommand { + + boolean process(CommandSender sender, String cmd, String[] args); +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/config/Configurator.java b/src/main/java/dev/projectg/geyserupdater/common/config/Configurator.java new file mode 100644 index 00000000..8dac2cf4 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/config/Configurator.java @@ -0,0 +1,19 @@ +package dev.projectg.geyserupdater.common.config; + +import space.arim.dazzleconf.ConfigurationOptions; +import space.arim.dazzleconf.ext.snakeyaml.CommentMode; +import space.arim.dazzleconf.ext.snakeyaml.SnakeYamlConfigurationFactory; +import space.arim.dazzleconf.ext.snakeyaml.SnakeYamlOptions; + +import java.util.Map; + +public class Configurator { + + Map + + + static { + + } + +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/config/UpdaterConfiguration.java b/src/main/java/dev/projectg/geyserupdater/common/config/UpdaterConfiguration.java new file mode 100644 index 00000000..30c0cc3a --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/config/UpdaterConfiguration.java @@ -0,0 +1,40 @@ +package dev.projectg.geyserupdater.common.config; + +import lombok.Getter; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Required; +import org.spongepowered.configurate.objectmapping.meta.Setting; + +@Getter +@ConfigSerializable +@SuppressWarnings("FieldMayBeFinal") +public class UpdaterConfiguration { + + public static int DEFAULT_VERSION = 3; + + @Setting("auto-check-interval") + private int autoUpdateInterval = 24; + + @Setting("delete-on-fail") + private boolean deleteOnFail = true; + + @Setting("restart-server") + private boolean restartServer = false; + + @Setting("restart-script") + private boolean generateRestartScript = false; + + @Setting("restart-message") + private String restartMessage = "§2This server will be restarting in 10 seconds!"; + + @Setting("download-time-limit") + private int downloadTimeLimit = 180; + + @Setting("debug") + private boolean enableDebug = false; + + @Required + @Setting("config-version") + private int version = 3; + +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/config/lombok.config b/src/main/java/dev/projectg/geyserupdater/common/config/lombok.config new file mode 100644 index 00000000..18f74be1 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/config/lombok.config @@ -0,0 +1 @@ +lombok.accessors.fluent = true \ No newline at end of file diff --git a/src/main/java/dev/projectg/geyserupdater/common/config/module/DefinedModule.java b/src/main/java/dev/projectg/geyserupdater/common/config/module/DefinedModule.java new file mode 100644 index 00000000..fb390801 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/config/module/DefinedModule.java @@ -0,0 +1,23 @@ +package dev.projectg.geyserupdater.common.config.module; + +import dev.projectg.geyserupdater.common.config.module.project.Project; +import lombok.Getter; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Setting; + +@ConfigSerializable +@SuppressWarnings("FieldMayBeFinal") +@Getter +public class DefinedModule extends PresetModule implements IModule { + + @Setting("enable") + private boolean enable = true; + + @Setting("auto-check") + private boolean autoCheck = true; + + @Setting("auto-update") + private boolean autoUpdate = false; + + private Project project = null; +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/config/module/IModule.java b/src/main/java/dev/projectg/geyserupdater/common/config/module/IModule.java new file mode 100644 index 00000000..ec305f49 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/config/module/IModule.java @@ -0,0 +1,14 @@ +package dev.projectg.geyserupdater.common.config.module; + +import dev.projectg.geyserupdater.common.config.module.project.Project; + +public interface IModule { + + boolean enable(); + + boolean autoCheck(); + + boolean autoUpdate(); + + Project project(); +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/config/module/PresetModule.java b/src/main/java/dev/projectg/geyserupdater/common/config/module/PresetModule.java new file mode 100644 index 00000000..5413bbff --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/config/module/PresetModule.java @@ -0,0 +1,30 @@ +package dev.projectg.geyserupdater.common.config.module; + +import dev.projectg.geyserupdater.common.config.module.project.Project; +import lombok.Getter; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.NodeKey; +import org.spongepowered.configurate.objectmapping.meta.Setting; + +@ConfigSerializable +@SuppressWarnings("FieldMayBeFinal") +@Getter +public class PresetModule implements IModule { + + @NodeKey + private String preset; + + @Setting("enable") + private boolean enable = true; + + @Setting("auto-check") + private boolean autoCheck = true; + + @Setting("auto-update") + private boolean autoUpdate = false; + + @Override + public Project project() { + + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/config/module/project/JenkinsProject.java b/src/main/java/dev/projectg/geyserupdater/common/config/module/project/JenkinsProject.java new file mode 100644 index 00000000..3b4daec0 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/config/module/project/JenkinsProject.java @@ -0,0 +1,16 @@ +package dev.projectg.geyserupdater.common.config.module.project; + +import space.arim.dazzleconf.annote.ConfKey; +import space.arim.dazzleconf.annote.ConfSerialisers; +import space.arim.dazzleconf.serialiser.URLValueSerialiser; + +import java.net.URL; + +@ConfSerialisers(URLValueSerialiser.class) +public interface JenkinsProject { + @ConfKey("project") + String projectLink(); + + @ConfKey("download") + URL downloadLink(); +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/config/module/project/Project.java b/src/main/java/dev/projectg/geyserupdater/common/config/module/project/Project.java new file mode 100644 index 00000000..aa9a4608 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/config/module/project/Project.java @@ -0,0 +1,4 @@ +package dev.projectg.geyserupdater.common.config.module.project; + +public interface Project { +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/config/module/project/SpigotProject.java b/src/main/java/dev/projectg/geyserupdater/common/config/module/project/SpigotProject.java new file mode 100644 index 00000000..daa01cd5 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/config/module/project/SpigotProject.java @@ -0,0 +1,8 @@ +package dev.projectg.geyserupdater.common.config.module.project; + +import space.arim.dazzleconf.annote.ConfKey; + +public interface SpigotProject { + @ConfKey("resource") + int resourceId(); +} diff --git a/src/main/java/com/projectg/geyserupdater/common/logger/JavaUtilUpdaterLogger.java b/src/main/java/dev/projectg/geyserupdater/common/logger/JavaUtilUpdaterLogger.java similarity index 80% rename from src/main/java/com/projectg/geyserupdater/common/logger/JavaUtilUpdaterLogger.java rename to src/main/java/dev/projectg/geyserupdater/common/logger/JavaUtilUpdaterLogger.java index 2710bdd1..00d1c2c8 100644 --- a/src/main/java/com/projectg/geyserupdater/common/logger/JavaUtilUpdaterLogger.java +++ b/src/main/java/dev/projectg/geyserupdater/common/logger/JavaUtilUpdaterLogger.java @@ -1,9 +1,10 @@ -package com.projectg.geyserupdater.common.logger; +package dev.projectg.geyserupdater.common.logger; import java.util.logging.Level; import java.util.logging.Logger; -public final class JavaUtilUpdaterLogger implements UpdaterLogger { +public class JavaUtilUpdaterLogger implements UpdaterLogger { + private final Logger logger; private Level originLevel; @@ -29,12 +30,14 @@ public void info(String message) { @Override public void debug(String message) { - logger.fine(message); + logger.info(message); } + // todo: try to making debug/trace logging labbeled correctly. check how floodgate does it + @Override public void trace(String message) { - logger.finer(message); + logger.info(message); } @Override diff --git a/src/main/java/com/projectg/geyserupdater/common/logger/UpdaterLogger.java b/src/main/java/dev/projectg/geyserupdater/common/logger/UpdaterLogger.java similarity index 96% rename from src/main/java/com/projectg/geyserupdater/common/logger/UpdaterLogger.java rename to src/main/java/dev/projectg/geyserupdater/common/logger/UpdaterLogger.java index db610f46..0fcd25e8 100644 --- a/src/main/java/com/projectg/geyserupdater/common/logger/UpdaterLogger.java +++ b/src/main/java/dev/projectg/geyserupdater/common/logger/UpdaterLogger.java @@ -1,4 +1,4 @@ -package com.projectg.geyserupdater.common.logger; +package dev.projectg.geyserupdater.common.logger; public interface UpdaterLogger { diff --git a/src/main/java/com/projectg/geyserupdater/common/logger/UpdaterLoggerHolder.java b/src/main/java/dev/projectg/geyserupdater/common/logger/UpdaterLoggerHolder.java similarity index 59% rename from src/main/java/com/projectg/geyserupdater/common/logger/UpdaterLoggerHolder.java rename to src/main/java/dev/projectg/geyserupdater/common/logger/UpdaterLoggerHolder.java index 046a8f95..ba26b18e 100644 --- a/src/main/java/com/projectg/geyserupdater/common/logger/UpdaterLoggerHolder.java +++ b/src/main/java/dev/projectg/geyserupdater/common/logger/UpdaterLoggerHolder.java @@ -1,4 +1,4 @@ -package com.projectg.geyserupdater.common.logger; +package dev.projectg.geyserupdater.common.logger; class UpdaterLoggerHolder { diff --git a/src/main/java/dev/projectg/geyserupdater/common/scheduler/Task.java b/src/main/java/dev/projectg/geyserupdater/common/scheduler/Task.java new file mode 100644 index 00000000..09cf0ac2 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/scheduler/Task.java @@ -0,0 +1,6 @@ +package dev.projectg.geyserupdater.common.scheduler; + +public interface Task { + + void cancel(); +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/scheduler/UpdaterScheduler.java b/src/main/java/dev/projectg/geyserupdater/common/scheduler/UpdaterScheduler.java new file mode 100644 index 00000000..61a97d75 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/scheduler/UpdaterScheduler.java @@ -0,0 +1,51 @@ +package dev.projectg.geyserupdater.common.scheduler; + +import javax.annotation.Nonnull; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +public interface UpdaterScheduler { + + /** + * Schedule a Runnable to be run on a new thread. + * @param runnable The Runnable + * @param async true to run the Runnable off the main server thread, false to run it on the main server thread. Disregarded for BungeeCord and Velocity. + */ + default Task run(@Nonnull Runnable runnable, boolean async) { + Objects.requireNonNull(runnable); + return schedule(runnable, async, 0, 0, TimeUnit.MILLISECONDS); + } + + /** + * Schedule a Runnable to be run on a new thread. + * @param runnable The Runnable + * @param async true to run the Runnable off the main server thread, false to run it on the main server thread. Disregarded for BungeeCord and Velocity. + * @param delay The delay in milliseconds. A value less than zero should be considered unsafe. + */ + default Task runDelayed(@Nonnull Runnable runnable, boolean async, long delay, TimeUnit unit) { + Objects.requireNonNull(runnable); + return schedule(runnable, async, delay, 0, unit); + } + + /** + * Schedule a Runnable to be run. + * @param runnable The Runnable + * @param async true to run the Runnable off the main server thread, false to run it on the main server thread. Disregarded for BungeeCord and Velocity. + * @param repeat The repeat period, in milliseconds. A value of 0 or less will only run the Runnable once. A value less than zero should be considered unsafe. + */ + default Task runTimer(@Nonnull Runnable runnable, boolean async, long repeat, TimeUnit unit) { + Objects.requireNonNull(runnable); + return schedule(runnable, async, 0, repeat, unit); + } + + /** + * Schedule a Runnable to be run on a new thread. + * @param runnable The Runnable + * @param async true to run the Runnable off the main server thread, false to run it on the main server thread. Disregarded for BungeeCord and Velocity. + * @param delay The delay in milliseconds. A value less than zero should be considered unsafe. + * @param repeat The repeat period, in milliseconds. A value of 0 or less will only run the Runnable once. A value less than zero should be considered unsafe. + */ + Task schedule(@Nonnull Runnable runnable, boolean async, long delay, long repeat, TimeUnit unit); +} + + diff --git a/src/main/java/dev/projectg/geyserupdater/common/update/DownloadManager.java b/src/main/java/dev/projectg/geyserupdater/common/update/DownloadManager.java new file mode 100644 index 00000000..074869ad --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/update/DownloadManager.java @@ -0,0 +1,119 @@ +package dev.projectg.geyserupdater.common.update; + +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; +import dev.projectg.geyserupdater.common.scheduler.Task; +import dev.projectg.geyserupdater.common.scheduler.UpdaterScheduler; +import dev.projectg.geyserupdater.common.util.WebUtils; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +public class DownloadManager { + + private final UpdateManager updateManager; + private final UpdaterScheduler scheduler; + private final int downloadTimeLimit; + + private final List queue = new LinkedList<>(); + + // Used by the hang checker to check if the current download is the same as when it was scheduled + @Nullable private Updatable currentUpdate = null; + + // Used by the hang checker to cancel the download if necessary + @Nullable private Task downloader = null; + + public DownloadManager(UpdateManager updateManager, UpdaterScheduler scheduler, int downloadTimeLimit) { + this.updateManager = updateManager; + this.scheduler = scheduler; + this.downloadTimeLimit = downloadTimeLimit; + } + + public void queue(Updatable updatable) { + queue.add(updatable); + if (downloader == null) { + downloadAll(); + } + } + + private void downloadAll() { + // Run the download on a new thread + this.downloader = scheduler.run(() -> { + + while (!queue.isEmpty()) { + currentUpdate = Objects.requireNonNull(queue.get(0)); + + // Create a timer to stop this download from running too long. Either the hang checker is cancelled or the hang checker cancels this. + Task hangChecker = scheduleHangChecker(currentUpdate); + + try { + Files.createDirectories(currentUpdate.outputFile.getParent()); + WebUtils.downloadFile(currentUpdate.downloadUrl, currentUpdate.outputFile); + } catch (IOException e) { + UpdaterLogger.getLogger().error("Caught exception while downloading file " + currentUpdate.outputFile + " with URL: " + currentUpdate.downloadUrl); + e.printStackTrace(); + updateManager.finish(currentUpdate, DownloadResult.UNKNOWN_FAIL); + continue; + } + + hangChecker.cancel(); + queue.remove(0); + updateManager.finish(currentUpdate, DownloadResult.SUCCESS); + } + + // Revert everything while having it locked so that the state is always correctly read by a different thread + synchronized (this) { + currentUpdate = null; + downloader = null; + } + + // Everything should be downloaded now unless the queue was added to after the above for loop was finished + // But the synchronized block was not entered + }, true); + } + + private Task scheduleHangChecker(Updatable updatable) { + // The time to allow the download to take, in seconds + + return scheduler.runDelayed(() -> { + if (downloader == null) { + throw new AssertionError("HangChecker should not execute while nothing is downloading."); + } + + if (updatable == currentUpdate) { + // Revert everything while having it locked so that the state is always correctly read by a different thread + synchronized (this) { + queue.clear(); + currentUpdate = null; + downloader.cancel(); + downloader = null; + } + + UpdaterLogger.getLogger().error("The download queue has been stopped and cleared because the download for " + updatable + " took longer than " + downloadTimeLimit + + " seconds. Increase the download-time-limit in the config if you have a slow internet connection."); + + updateManager.finish(updatable, DownloadResult.TIMEOUT); + } + }, true, downloadTimeLimit, TimeUnit.SECONDS); + } + + /** + * Shuts down any running download queues. The queue will be cleared, however the current download will be allowed to finish. + * @return A list of {@link Updatable} that had their download cancelled. + */ + public List shutdown() { + List cancelled = new ArrayList<>(); + + // Allow the current download to finish and cancel everything else + while (queue.size() > 1) { + cancelled.add(queue.remove(1)); + } + + return cancelled; + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/update/DownloadResult.java b/src/main/java/dev/projectg/geyserupdater/common/update/DownloadResult.java new file mode 100644 index 00000000..8aedb020 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/update/DownloadResult.java @@ -0,0 +1,7 @@ +package dev.projectg.geyserupdater.common.update; + +public enum DownloadResult { + SUCCESS, + TIMEOUT, + UNKNOWN_FAIL +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/update/PluginId.java b/src/main/java/dev/projectg/geyserupdater/common/update/PluginId.java new file mode 100644 index 00000000..3d5dc48f --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/update/PluginId.java @@ -0,0 +1,80 @@ +package dev.projectg.geyserupdater.common.update; + +import dev.projectg.geyserupdater.common.config.UpdaterConfiguration; + +public enum PluginId { + GEYSER("https://ci.opencollab.dev/job/GeyserMC/job/Geyser/job/", "org.geysermc.connector.GeyserConnector"), + FLOODGATE("https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/", "org.geysermc.floodgate.FloodgatePlatform"); + + /** + * https://ci.opencollab.dev/job/GeyserMC/job/{PLUGIN_PAGE}/job/ + */ + private final String projectLink; + + /** + * A class from the given plugin + */ + private final String pluginClassName; + private String branch; + private String artifactLink; + + private boolean enable = false; + private boolean autoCheck = false; + private boolean autoUpdate = false; + + PluginId(String link, String pluginClassName) { + this.projectLink = link; + this.pluginClassName = pluginClassName; + } + + public boolean isEnable() { + return enable; + } + public boolean isAutoCheck() { + return autoCheck; + } + public boolean isAutoUpdate() { + return autoUpdate; + } + + /** + * @return The download link. Not usable if {@link PluginId#setArtifact(String)} has not been called. + */ + public String getLatestFileLink() { + return projectLink + branch + "/lastSuccessfulBuild/" + artifactLink; + } + + public String getLatestBuildNumber() { + return projectLink + branch + "/lastSuccessfulBuild/buildNumber"; + } + + /** + * @param branch The branch to be used for {@link PluginId#getLatestFileLink()} + */ + public void setBranch(String branch) { + this.branch = branch; + } + + /** + * @param artifactLink The artifact link. `bootstrap/spigot/target/Geyser-Spigot.jar` for example. + */ + public void setArtifact(String artifactLink) { + this.artifactLink = "artifact/" + artifactLink; + } + + /** + * @return A class from the plugin. Will throw an {@link ClassNotFoundException} if the class is not available, i.e. the plugin is not loaded. + */ + public Class getPluginClass() throws ClassNotFoundException { + return Class.forName(pluginClassName); + } + + /** + * Load the enable, autoCheck, and autoUpdate configuration settings into the enum values. + * {@link JacksonConfiguration#getUpdateEntries()} should contain entries whose keys are equal an enum value's name in lowercase + * @param config The config to load from + */ + public static void loadSettings(UpdaterConfiguration config) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/update/RecursiveDownloadManager.java b/src/main/java/dev/projectg/geyserupdater/common/update/RecursiveDownloadManager.java new file mode 100644 index 00000000..62562ae4 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/update/RecursiveDownloadManager.java @@ -0,0 +1,116 @@ +package dev.projectg.geyserupdater.common.update; + +import dev.projectg.geyserupdater.common.GeyserUpdater; +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; +import dev.projectg.geyserupdater.common.scheduler.Task; +import dev.projectg.geyserupdater.common.scheduler.UpdaterScheduler; +import dev.projectg.geyserupdater.common.util.WebUtils; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.Files; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@SuppressWarnings("unused") // Keeping this to compare to DownloadManager +public class RecursiveDownloadManager { + + private final GeyserUpdater updater; + + private final List queue = new LinkedList<>(); + + // Used for making sure one download is ever running + private boolean isDownloading = false; + + // Used by the hang checker to check if the current download is the same as when it was scheduled + @Nullable private Updatable currentUpdate = null; + + // Used by the hang checker to cancel the download if necessary + @Nullable private Task downloader = null; + + // maybe refactor this to use a for loop instead of being recursive? i dunno + + public RecursiveDownloadManager(GeyserUpdater updater) { + this.updater = updater; + } + + public void queue(Updatable updatable) { + queue.add(updatable); + downloadIfPossible(); + } + + private void download(Updatable updatable) { + isDownloading = true; + currentUpdate = updatable; + + UpdaterScheduler scheduler = updater.getScheduler(); + + // Run the download on a new thread + this.downloader = scheduler.run(() -> { + + // Create a timer to stop this download from running too long. Either the hang checker is cancelled or the hang checker cancels this. + Task hangChecker = scheduleHangChecker(updater, updatable); + + try { + WebUtils.downloadFile(updatable.downloadUrl, updatable.outputFile); + } catch (IOException ioException) { + UpdaterLogger.getLogger().error("Failed to download file: " + updatable.downloadUrl + " for " + updatable); + ioException.printStackTrace(); + } + hangChecker.cancel(); + + // Revert everything while having it locked so that the state is always correctly read by original thread + synchronized (this) { + this.queue.remove(updatable); + this.isDownloading = false; + this.currentUpdate = null; + this.downloader = null; + } + + // Initiate another download if necessary + downloadIfPossible(); + }, true); + } + + private void downloadIfPossible() { + if (!queue.isEmpty() && !isDownloading) { + download(queue.get(0)); + } + } + + private Task scheduleHangChecker(GeyserUpdater updater, Updatable updatable) { + // The time to allow the download to take, in seconds + int downloadTimeLimit = updater.getConfig().getDownloadTimeLimit(); + + return updater.getScheduler().runDelayed(() -> { + if (!isDownloading || downloader == null) { + throw new AssertionError("HangChecker should not execute while nothing is downloading."); + } + + if (updatable == currentUpdate) { + // Revert everything while having it locked so that the state is always correctly read by a different thread + synchronized (this) { + queue.clear(); + isDownloading = false; + currentUpdate = null; + downloader.cancel(); + downloader = null; + } + + UpdaterLogger logger = UpdaterLogger.getLogger(); + + logger.error("The download queue has been stopped and cleared because the download for " + updatable + " took longer than " + downloadTimeLimit + + " seconds. Increase the download-time-limit in the config if you have a slow internet connection."); + + try { + boolean deletedFailedFile = Files.deleteIfExists(updatable.outputFile); + logger.debug("Failed download for " + updatable + " had a file?: " + deletedFailedFile); + } catch (IOException e) { + logger.error("Failed to delete failed download file of " + updatable); + e.printStackTrace(); + } + } + }, true, downloadTimeLimit, TimeUnit.SECONDS); + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/update/Updatable.java b/src/main/java/dev/projectg/geyserupdater/common/update/Updatable.java new file mode 100644 index 00000000..0a51b9a1 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/update/Updatable.java @@ -0,0 +1,66 @@ +package dev.projectg.geyserupdater.common.update; + +import dev.projectg.geyserupdater.common.update.identity.IdentityComparer; +import dev.projectg.geyserupdater.common.util.WebUtils; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; + +public class Updatable { + + @Nonnull public final String pluginIdentity; + @Nonnull public final IdentityComparer identityComparer; + @Nullable public final IdentityComparer hashComparer; + @Nonnull public final String downloadUrl; + @Nonnull public final Path outputFile; + public final boolean autoCheck; + public final boolean autoUpdate; + + /** + * Immutable container for information regarding how to update something + * @param name The name + * @param identityComparer The {@link IdentityComparer} to check if the plugin is outdated + * @param hashComparer The {@link IdentityComparer} to check if the hash of the downloaded file is acceptable. + * @param downloadUrl The complete download link of the plugin + * @param file If the Path is a file, the download will be written to that file. If the Path is a directory, the file will be written to that directory, and the filename will deduced from the link provided. + */ + public Updatable(@Nonnull String name, + @Nonnull IdentityComparer identityComparer, + @Nullable IdentityComparer hashComparer, + @Nonnull String downloadUrl, + @Nonnull Path file, + boolean autoCheck, + boolean autoUpdate) { + + this.pluginIdentity = Objects.requireNonNull(name); + this.identityComparer = Objects.requireNonNull(identityComparer); + this.hashComparer = hashComparer; + Objects.requireNonNull(downloadUrl); + Objects.requireNonNull(file); + this.autoCheck = autoCheck; + this.autoUpdate = autoUpdate; + + // Remove / from the end of the link if necessary + if (downloadUrl.endsWith("/")) { + this.downloadUrl = downloadUrl.substring(0, downloadUrl.length() - 1); + } else { + this.downloadUrl = downloadUrl; + } + + // Figure out the output file name if necessary + if (Files.isDirectory(file)) { + this.outputFile = Paths.get(WebUtils.getFileName(downloadUrl)); + } else { + this.outputFile = file; + } + } + + @Override + public String toString() { + return pluginIdentity; + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/update/UpdateManager.java b/src/main/java/dev/projectg/geyserupdater/common/update/UpdateManager.java new file mode 100644 index 00000000..34d556ee --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/update/UpdateManager.java @@ -0,0 +1,255 @@ +package dev.projectg.geyserupdater.common.update; + +import com.google.common.collect.ImmutableMap; +import dev.projectg.geyserupdater.common.config.UpdaterConfiguration; +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; +import dev.projectg.geyserupdater.common.scheduler.UpdaterScheduler; +import dev.projectg.geyserupdater.common.update.identity.IdentityComparer; +import dev.projectg.geyserupdater.common.update.identity.provider.FileHashProvider; +import dev.projectg.geyserupdater.common.update.identity.provider.JenkinsBuildProvider; +import dev.projectg.geyserupdater.common.update.identity.provider.JenkinsHashProvider; +import dev.projectg.geyserupdater.common.update.identity.type.BuildNumber; +import dev.projectg.geyserupdater.common.util.WebUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class UpdateManager { + + /** + * The {@link DownloadManager} to use for downloading new versions of plugins. + */ + private final DownloadManager downloadManager; + private final UpdaterScheduler scheduler; + private final UpdaterConfiguration config; + + private final Map registry = new HashMap<>(); + + private boolean isAutoChecking = false; + + public UpdateManager(Path defaultDownloadLocation, UpdaterScheduler scheduler, UpdaterConfiguration config) { + this.scheduler = scheduler; + this.config = config; + this.downloadManager = new DownloadManager(this, scheduler, config.getDownloadTimeLimit()); + UpdaterLogger logger = UpdaterLogger.getLogger(); + + for (PluginId pluginId : PluginId.values()) { + if (pluginId.isEnable()) { + String name = pluginId.name(); + + // todo: move getting the local build number and branch into PluginId + + // Get the inputstream of the git.properties + InputStream is; + try { + Class clazz = pluginId.getPluginClass(); + + is = clazz.getClassLoader().getResourceAsStream("git.properties"); + if (is == null) { + logger.error("Unable to find resource 'git.properties' for plugin: " + name); + continue; + } + } catch (ClassNotFoundException e) { + logger.error("Unable to find class for " + name + ", is it loaded? Unable to update."); + continue; + } + + // load the inputstream into a Properties + Properties gitProperties = new Properties(); + try { + gitProperties.load(is); + is.close(); + } catch (IOException e) { + logger.error("Failed to get git.properties for plugin: " + name + ". Unable to update."); + e.printStackTrace(); + continue; + } + + // Get the build number and branch + String buildNumberString = gitProperties.getProperty("git.build.number"); + String branch = gitProperties.getProperty("git.branch"); + if (buildNumberString == null || branch == null) { + UpdaterLogger.getLogger().error("Failed to find build number or branch in git Properties '" + gitProperties + "' of plugin '" + name + "'. Not updating."); + continue; + } + + pluginId.setBranch(branch); + + // The download location, used for both downloading and hash checking + Path downloadLocation = defaultDownloadLocation.resolve(Paths.get(WebUtils.getFileName(pluginId.getLatestFileLink()))); + + // For age comparer + BuildNumber buildNumber = new BuildNumber(Integer.parseInt(buildNumberString)); + JenkinsBuildProvider buildProvider; + try { + buildProvider = new JenkinsBuildProvider(pluginId.getLatestBuildNumber()); + } catch (MalformedURLException e) { + UpdaterLogger.getLogger().error("Failed to create build number checker for " + name + ". Not updating."); + e.printStackTrace(); + continue; + } + + // File hash comparer + IdentityComparer hashComparer = null; + try { + FileHashProvider localHashProvider = new FileHashProvider(downloadLocation); + JenkinsHashProvider jenkinsHashProvider = new JenkinsHashProvider(pluginId.getLatestFileLink() + "/*fingerprint*/"); + hashComparer = new IdentityComparer<>(localHashProvider, jenkinsHashProvider); + } catch (MalformedURLException e) { + UpdaterLogger.getLogger().error("Failure while getting location of file for " + name + ". It will be possible to update it, but not to compare file hashes."); + e.printStackTrace(); + } + + register(new Updatable( + name, + new IdentityComparer<>(buildNumber, buildProvider), + hashComparer, + pluginId.getLatestFileLink(), + downloadLocation, + pluginId.isAutoCheck(), + pluginId.isAutoUpdate())); + } + } + + // Load extra stuff from the config if we wanted, I guess + } + + /** + * Register an {@link Updatable} to be updated + * @param updatable The {@link Updatable} to be updated + */ + public void register(Updatable updatable) { + registry.put(updatable, UpdateStatus.UNKNOWN); + if (updatable.autoCheck && !isAutoChecking) { + scheduleUpdateChecker(scheduler, config.getAutoUpdateInterval()); + } + } + + public boolean isTracked(Updatable updatable) { + return registry.containsKey(updatable); + } + + /** + * Potentially blocking + */ + public boolean isOutdated(Updatable updatable) { + if (!isTracked(updatable)) { + throw new IllegalArgumentException("Updatable must be tracked by the UpdateManager in order to check if it is outdated!"); + } + UpdateStatus status = Objects.requireNonNull(registry.get(updatable)); + switch (status) { + case UNKNOWN: + // It was latest last we checked, or we haven't checker before + return !updatable.identityComparer.checkIfEquals(); + case OUTDATED: + return true; + default: + // A new version is either being downloaded or has been already + return false; + } + } + + /** + * Blocking + */ + protected void finish(Updatable updatable, DownloadResult result) { + if (registry.get(updatable) != UpdateStatus.DOWNLOADING) { + throw new IllegalStateException("Cannot finish an Updatable if its current status is not DOWNLOADING"); + } + UpdaterLogger logger = UpdaterLogger.getLogger(); + + if (updatable.hashComparer == null) { + if (result == DownloadResult.SUCCESS) { + // cant check hash, but the download result is success + logger.info("Successfully downloaded update for: " + updatable); + registry.put(updatable, UpdateStatus.DOWNLOADED); + return; + } + } else { + Object downloadHash = updatable.hashComparer.callLocalValue(); + Object anticipatedHash = updatable.hashComparer.callExternalValue(); + if (downloadHash == null) { + logger.error("Failed to find hash for downloaded update of: " + updatable); + } else if (anticipatedHash == null) { + logger.error("Failed to find anticipated hash for downloaded update of: " + updatable); + } else if (downloadHash.equals(anticipatedHash)) { + // hash is "correct" + logger.info("Successfully downloaded update for: " + updatable); + logger.debug("Downloaded file hash: <" + downloadHash + ">. Anticipated hash: <" + anticipatedHash + ">"); + registry.put(updatable, UpdateStatus.DOWNLOADED); + return; + } else { + logger.warn("The file hash of the downloaded file did not match the hash provided online for " + updatable); + logger.warn("Downloaded file hash: <" + downloadHash + ">. Anticipated hash: <" + anticipatedHash + ">"); + } + } + + // Hash is not correct, or cannot check hash and there was a fail + if (config.isDeleteOnFail()) { + logger.warn("Deleting failed download."); + try { + Files.deleteIfExists(updatable.outputFile); + } catch (IOException e) { + logger.error("Failed to delete failed download file of " + updatable); + e.printStackTrace(); + } + } + } + + private void scheduleUpdateChecker(UpdaterScheduler scheduler, long interval) { + scheduler.schedule(() -> { + List outdatedOnes = new ArrayList<>(); + List autoDownloads = new ArrayList<>(); + + for (Updatable updatable : registry.keySet()) { + // Only check if it is not known to be outdated, and if it should be checked automatically + if (registry.get(updatable) == UpdateStatus.UNKNOWN && updatable.autoCheck) { + // We should check if it needs an update + if (!updatable.identityComparer.checkIfEquals()) { + // It is outdated + registry.put(updatable, UpdateStatus.OUTDATED); + outdatedOnes.add(updatable.toString()); + if (updatable.autoUpdate) { + registry.put(updatable, UpdateStatus.DOWNLOADING); + downloadManager.queue(updatable); + autoDownloads.add(updatable.toString()); + } + } + } + } + + // todo: also send messages to players with the permission + if (!outdatedOnes.isEmpty()) { + UpdaterLogger logger = UpdaterLogger.getLogger(); + logger.info("The following updatables are outdated: " + outdatedOnes); + if (!autoDownloads.isEmpty()) { + logger.info("The following updatables are set to download automatically and have been queued for download: " + autoDownloads); + } + } + + }, true, 0L, interval, TimeUnit.HOURS); + + isAutoChecking = true; + } + + /** + * @return a {@link Map#keySet()} of the tracked Updatable registry + */ + public Set getTrackedUpdatables() { + return registry.keySet(); + } + + public void shutdown() { + List cancelled = downloadManager.shutdown(); + if (!cancelled.isEmpty()) { + UpdaterLogger.getLogger().info("Cancelled the following downloads because of a shutdown: " + cancelled.stream().map(Updatable::toString).collect(Collectors.joining(", "))); + } + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/update/UpdateStatus.java b/src/main/java/dev/projectg/geyserupdater/common/update/UpdateStatus.java new file mode 100644 index 00000000..60390bef --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/update/UpdateStatus.java @@ -0,0 +1,20 @@ +package dev.projectg.geyserupdater.common.update; + +public enum UpdateStatus { + /** + * The Updatable has not been checked, or the last time it was checked it was not outdated. Another check may find that it is outdated. + */ + UNKNOWN, + /** + * The Updatable is outdated but is not being downloaded. + */ + OUTDATED, + /** + * The Updatable is outdated and is being downloaded. + */ + DOWNLOADING, + /** + * The Updatable is outdated but it has been downloaded. + */ + DOWNLOADED +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/update/identity/IdentityComparer.java b/src/main/java/dev/projectg/geyserupdater/common/update/identity/IdentityComparer.java new file mode 100644 index 00000000..c922ad70 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/update/identity/IdentityComparer.java @@ -0,0 +1,86 @@ +package dev.projectg.geyserupdater.common.update.identity; + +import dev.projectg.geyserupdater.common.update.identity.provider.IdentityProvider; +import dev.projectg.geyserupdater.common.update.identity.type.Identity; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * @param The external {@link IdentityProvider} implementation + * @param The {@link Identity} implementation + */ +public class IdentityComparer, T extends IdentityProvider, S extends Identity> { + + private final @Nullable S localIdentity; + private final @Nullable U localIdentityProvider; + private final @Nonnull T externalIdentityProvider; + + /** + * Create an identity comparer, which stores a way to check a local identity and way to check an external identity. Both params should provide the same {@link Identity} implementation. + * @param localIdentityProvider The local identity provider. {@link IdentityProvider#getValue()} + * @param externalIdentityProvider The external identity provider. {@link IdentityProvider#getValue()} + */ + public IdentityComparer(@Nonnull U localIdentityProvider, @Nonnull T externalIdentityProvider) { + Objects.requireNonNull(localIdentityProvider); + Objects.requireNonNull(externalIdentityProvider); + + this.localIdentity = null; + this.localIdentityProvider = localIdentityProvider; + this.externalIdentityProvider = externalIdentityProvider; + } + + /** + * Create an identity comparer, which stores a local identity and way to check an external identity. + * @param localIdentity The local identity. + * @param externalIdentityProvider The external identity provider. {@link IdentityProvider#getValue()} will be called every time {@link IdentityComparer#checkIfEquals()} is called. + */ + public IdentityComparer(@Nonnull S localIdentity, @Nonnull T externalIdentityProvider) { + Objects.requireNonNull(localIdentity); + Objects.requireNonNull(externalIdentityProvider); + + this.localIdentity = localIdentity; + this.localIdentityProvider = null; + this.externalIdentityProvider = externalIdentityProvider; + } + + /** + * Request the {@link Identity} from the external {@link IdentityProvider} and compare it to the stored local {@link Identity}. Should be regarded as a blocking operation. + * @return True if the local identity {@link Object#equals(Object)} the external identity. Also returns true if either are null. + */ + public boolean checkIfEquals() { + // todo: return an enum or wrapper, to better deal with null values + Object localValue = callLocalValue(); + Object externalValue = callExternalValue(); + if (localValue == null || externalValue == null) { + return true; + } + + return localValue.equals(externalValue); + } + + @Nullable + public Object callLocalValue() { + if (localIdentity == null) { + S localId = Objects.requireNonNull(localIdentityProvider).getValue(); + if (localId == null) { + return null; + } else { + return localId.value(); + } + } else { + return localIdentity.value(); + } + } + + @Nullable + public Object callExternalValue() { + S externalId = externalIdentityProvider.getValue(); + if (externalId == null) { + return null; + } else { + return externalId.value(); + } + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/update/identity/provider/FileHashProvider.java b/src/main/java/dev/projectg/geyserupdater/common/update/identity/provider/FileHashProvider.java new file mode 100644 index 00000000..b68eac9f --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/update/identity/provider/FileHashProvider.java @@ -0,0 +1,35 @@ +package dev.projectg.geyserupdater.common.update.identity.provider; + +import com.google.common.hash.Hashing; +import com.google.common.io.ByteSource; +import com.google.common.io.Files; +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; +import dev.projectg.geyserupdater.common.update.identity.type.Md5FileHash; + +import java.io.IOException; +import java.nio.file.Path; + +public class FileHashProvider implements IdentityProvider { + + Path file; + + public FileHashProvider(Path file) { + this.file = file; + } + + @Override + public Md5FileHash getValue() { + // https://stackoverflow.com/questions/304268/getting-a-files-md5-checksum-in-java + ByteSource byteSource = Files.asByteSource(file.toFile()); + + // todo: non beta usage? I dont know. + String md5Hash = null; + try { + md5Hash = byteSource.hash(Hashing.md5()).toString(); + } catch (IOException e) { + UpdaterLogger.getLogger().error("Exception while getting md5 hash of file: " + file.getFileName()); + e.printStackTrace(); + } + return new Md5FileHash(md5Hash); + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/update/identity/provider/IdentityProvider.java b/src/main/java/dev/projectg/geyserupdater/common/update/identity/provider/IdentityProvider.java new file mode 100644 index 00000000..4046996b --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/update/identity/provider/IdentityProvider.java @@ -0,0 +1,11 @@ +package dev.projectg.geyserupdater.common.update.identity.provider; + +import dev.projectg.geyserupdater.common.update.identity.type.Identity; + +import javax.annotation.Nullable; + +public interface IdentityProvider> { + + @Nullable + S getValue(); +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/update/identity/provider/JenkinsBuildProvider.java b/src/main/java/dev/projectg/geyserupdater/common/update/identity/provider/JenkinsBuildProvider.java new file mode 100644 index 00000000..219fa482 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/update/identity/provider/JenkinsBuildProvider.java @@ -0,0 +1,46 @@ +package dev.projectg.geyserupdater.common.update.identity.provider; + +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; +import dev.projectg.geyserupdater.common.update.identity.type.BuildNumber; +import dev.projectg.geyserupdater.common.util.WebUtils; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Provides a build number from a given Jenkins link. + */ +public class JenkinsBuildProvider implements IdentityProvider { + + private final URL url; + + /** + * Creates a build number provider for a given link. + * @param link A link which onto which "/buildNumber" will be added, which should link to a page that contains only the build number as a plain integer. For example: https://ci.opencollab.dev/job/GeyserMC/job/Geyser/job/master/lastSuccessfulBuild/buildNumber + * @throws MalformedURLException If the link provided cannot be converted to a {@link URL} + */ + public JenkinsBuildProvider(String link) throws MalformedURLException { + url = new URL(link + "/buildNumber"); + } + + @Override + public BuildNumber getValue() { + BuildNumber buildNumber = null; + try { + String body = WebUtils.getBody(url); + try { + buildNumber = new BuildNumber(Integer.parseInt(body)); + } catch (NumberFormatException e) { + UpdaterLogger.getLogger().error("Failed to get a build number from a Jenkins server because an integer was not returned."); + UpdaterLogger.getLogger().error("Body returned: <" + body + "> (excluding the angle brackets)"); + e.printStackTrace(); + } + } catch (IOException e) { + UpdaterLogger.getLogger().error("Failed to get a build number from a Jenkins server:"); + e.printStackTrace(); + } + + return buildNumber; + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/update/identity/provider/JenkinsHashProvider.java b/src/main/java/dev/projectg/geyserupdater/common/update/identity/provider/JenkinsHashProvider.java new file mode 100644 index 00000000..5f737cbe --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/update/identity/provider/JenkinsHashProvider.java @@ -0,0 +1,56 @@ +package dev.projectg.geyserupdater.common.update.identity.provider; + +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; +import dev.projectg.geyserupdater.common.update.identity.type.Md5FileHash; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +public class JenkinsHashProvider implements IdentityProvider { + + private final URL url; + + public JenkinsHashProvider(String url) throws MalformedURLException { + this.url = new URL(url); + } + + @Override + public Md5FileHash getValue() { + // Don't use WebUtils so that we don't have to iterate over the whole page contents, and have better error messages. + + try { + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("GET"); + con.setRequestProperty("User-Agent", "GeyserUpdater"); + + if (con.getResponseCode() != 200) { + UpdaterLogger.getLogger().error("Unable to find md5 from jenkins fingerprint at: " + url + " because the Http response code was not 200 (OK)."); + return null; + } + + try (BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()))) { + + String inputLine; + while ((inputLine = in.readLine()) != null) { + if (inputLine.contains("MD5")) { + con.disconnect(); + return new Md5FileHash(inputLine.substring(inputLine.indexOf("MD5: ") + 5, inputLine.indexOf(""))); + } + } + con.disconnect(); + + UpdaterLogger.getLogger().error("Unable to find md5 from jenkins fingerprint at: " + url + " because the page scan failed to find the hash."); + return null; + } + } catch (IOException e) { + e.printStackTrace(); + } + + UpdaterLogger.getLogger().error("Unable to find md5 from jenkins fingerprint at: " + url + " because of an exception."); + return null; + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/update/identity/type/BuildNumber.java b/src/main/java/dev/projectg/geyserupdater/common/update/identity/type/BuildNumber.java new file mode 100644 index 00000000..0f1cd600 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/update/identity/type/BuildNumber.java @@ -0,0 +1,17 @@ +package dev.projectg.geyserupdater.common.update.identity.type; + +import org.jetbrains.annotations.NotNull; + +public class BuildNumber implements Identity { + + private final int buildNumber; + + public BuildNumber(int buildNumber) { + this.buildNumber = buildNumber; + } + + @Override + public @NotNull Integer value() { + return buildNumber; + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/update/identity/type/Identity.java b/src/main/java/dev/projectg/geyserupdater/common/update/identity/type/Identity.java new file mode 100644 index 00000000..5e8e269b --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/update/identity/type/Identity.java @@ -0,0 +1,13 @@ +package dev.projectg.geyserupdater.common.update.identity.type; + +import javax.annotation.Nonnull; + +/** + * Provides implementation for something that can be quantified as an age of something. + * @param The Type of the age. + */ +public interface Identity { + + @Nonnull + T value(); +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/update/identity/type/Md5FileHash.java b/src/main/java/dev/projectg/geyserupdater/common/update/identity/type/Md5FileHash.java new file mode 100644 index 00000000..ba45e2e5 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/update/identity/type/Md5FileHash.java @@ -0,0 +1,20 @@ +package dev.projectg.geyserupdater.common.update.identity.type; + +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nonnull; +import java.util.Objects; + +public class Md5FileHash implements Identity { + + private final String hash; + + public Md5FileHash(@Nonnull String md5Hash) { + hash = Objects.requireNonNull(md5Hash); + } + + @Override + public @NotNull String value() { + return hash; + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/util/FileUtils.java b/src/main/java/dev/projectg/geyserupdater/common/util/FileUtils.java new file mode 100644 index 00000000..06573bc4 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/util/FileUtils.java @@ -0,0 +1,19 @@ +package dev.projectg.geyserupdater.common.util; + +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class FileUtils { + + /** + * Get the file that a class resides in. + * @param clazz The class + * @return The file as a {@link Path} + * @throws URISyntaxException if there was a failure getting the {@link java.net.URL} of the {@link java.security.CodeSource} + */ + public static Path getCodeSourceLocation(Class clazz) throws URISyntaxException { + return Paths.get(clazz.getProtectionDomain().getCodeSource().getLocation().toURI()); + } +} + diff --git a/src/main/java/com/projectg/geyserupdater/common/util/OsUtils.java b/src/main/java/dev/projectg/geyserupdater/common/util/OsUtils.java similarity index 95% rename from src/main/java/com/projectg/geyserupdater/common/util/OsUtils.java rename to src/main/java/dev/projectg/geyserupdater/common/util/OsUtils.java index 473d7739..c5275e37 100644 --- a/src/main/java/com/projectg/geyserupdater/common/util/OsUtils.java +++ b/src/main/java/dev/projectg/geyserupdater/common/util/OsUtils.java @@ -1,4 +1,4 @@ -package com.projectg.geyserupdater.common.util; +package dev.projectg.geyserupdater.common.util; public class OsUtils { diff --git a/src/main/java/dev/projectg/geyserupdater/common/util/ReflectionUtils.java b/src/main/java/dev/projectg/geyserupdater/common/util/ReflectionUtils.java new file mode 100644 index 00000000..9002dfd3 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/util/ReflectionUtils.java @@ -0,0 +1,25 @@ +package dev.projectg.geyserupdater.common.util; + +import javax.annotation.Nonnull; +import java.lang.reflect.Field; + +public class ReflectionUtils { + + /** + * Get the value of a {@link Field} in a {@link Object} + * @param obj The Object which contains the Field + * @param fieldClass The expected Class of the Field + * @param fieldName The name of the Field + * @param The expected Type of the Field's value + * @return The value of the field as Type T. + * @throws IllegalAccessException If the value was inaccessible, regardless of Field#setAccessible(true) + * @throws NoSuchFieldException If the Object does not contain the Field specified + * @throws ClassCastException If the Type of the Field found does not match the fieldClass given + */ + @Nonnull + public static T getFieldValue(Object obj, Class fieldClass, String fieldName) throws IllegalAccessException, NoSuchFieldException, ClassCastException { + Field field = obj.getClass().getDeclaredField(fieldName); // get the field from the object + field.setAccessible(true); // allow getting the field's value regardless of accessibility + return fieldClass.cast(field.get(obj)); // get the value and cast it to the desired type + } +} diff --git a/src/main/java/com/projectg/geyserupdater/common/util/ScriptCreator.java b/src/main/java/dev/projectg/geyserupdater/common/util/ScriptCreator.java similarity index 96% rename from src/main/java/com/projectg/geyserupdater/common/util/ScriptCreator.java rename to src/main/java/dev/projectg/geyserupdater/common/util/ScriptCreator.java index c5883dcc..f18be179 100644 --- a/src/main/java/com/projectg/geyserupdater/common/util/ScriptCreator.java +++ b/src/main/java/dev/projectg/geyserupdater/common/util/ScriptCreator.java @@ -1,6 +1,6 @@ -package com.projectg.geyserupdater.common.util; +package dev.projectg.geyserupdater.common.util; -import com.projectg.geyserupdater.common.logger.UpdaterLogger; +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; import java.io.DataOutputStream; import java.io.File; diff --git a/src/main/java/dev/projectg/geyserupdater/common/util/SpigotUtils.java b/src/main/java/dev/projectg/geyserupdater/common/util/SpigotUtils.java new file mode 100644 index 00000000..2b20d16c --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/util/SpigotUtils.java @@ -0,0 +1,45 @@ +package dev.projectg.geyserupdater.common.util; + +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Scanner; + +public class SpigotUtils { + + private static final String VERSION_REGEX = "(\\d+.){1,2}\\d+(-SNAPSHOT|-RC\\d{1,2}){0,1}"; + + /** + * Get the latest version of GeyserUpdater from the spigot resource page + * @return the latest version, null if there was an error. + */ + public static String getVersion(int resourceId) { + + try (InputStream inputStream = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + resourceId).openStream(); Scanner scanner = new Scanner(inputStream)) { + StringBuilder builder = new StringBuilder(); + while (scanner.hasNext()) { + builder.append(scanner.next()); + } + String version = builder.toString(); + if (!version.matches(VERSION_REGEX)) { + UpdaterLogger.getLogger().warn("Got unexpected string when checking version of Spigot resource " + resourceId + ": " + version); + } + + return version; + } catch (IOException exception) { + UpdaterLogger.getLogger().error("Failed to check for updates: " + exception.getMessage()); + return null; + } + } + + public static URL getDownloadUrl(int resourceId) { + try { + return new URL("https://api.spiget.org/v2/resources/" + resourceId + "/download"); + } catch (MalformedURLException e) { + throw new AssertionError("Unexpected MalformedURLException when getting download link for spigot resource"); + } + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/common/util/WebUtils.java b/src/main/java/dev/projectg/geyserupdater/common/util/WebUtils.java new file mode 100644 index 00000000..7d7de5ed --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/common/util/WebUtils.java @@ -0,0 +1,97 @@ +package dev.projectg.geyserupdater.common.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.projectg.geyserupdater.common.GeyserUpdater; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.stream.Collectors; + +// Full credit to GeyserMC +// https://github.com/GeyserMC/Geyser/blob/master/connector/src/main/java/org/geysermc/connector/utils/WebUtils.java + +public class WebUtils { + + /** + * Makes a web request to the given URL and returns the body as a string + * + * @param reqURL URL to fetch + * @return Body contents + */ + public static String getBody(URL reqURL) throws IOException { + HttpURLConnection con = (HttpURLConnection) reqURL.openConnection(); + con.setRequestMethod("GET"); + con.setRequestProperty("User-Agent", "GeyserUpdater-" + GeyserUpdater.getInstance().version); // Otherwise Java 8 fails on checking updates + + return connectionToString(con); + } + + public static String getBody(String reqURL) throws IOException { + return getBody(new URL(reqURL)); + } + + /** + * Makes a web request to the given URL and returns the body as a {@link JsonNode}. + * + * @param reqURL URL to fetch + * @return the response as JSON + */ + public static JsonNode getJson(String reqURL) throws IOException { + HttpURLConnection con = (HttpURLConnection) new URL(reqURL).openConnection(); + con.setRequestProperty("User-Agent", "GeyserUpdater-" + GeyserUpdater.getInstance().version); + return new ObjectMapper().readTree(con.getInputStream()); + } + + /** + * Downloads a file from the given URL and saves it to disk + * + * @param reqURL File to fetch + * @param fileLocation Location to save on disk + */ + public static void downloadFile(String reqURL, Path fileLocation) throws IOException { + HttpURLConnection con = (HttpURLConnection) new URL(reqURL).openConnection(); + con.setRequestProperty("User-Agent", "GeyserUpdater-" + GeyserUpdater.getInstance().version); + InputStream in = con.getInputStream(); + Files.copy(in, fileLocation, StandardCopyOption.REPLACE_EXISTING); + // todo: need to close the inputstream or not? + in.close(); + con.disconnect(); + } + + + /** + * Get the string output from the passed {@link HttpURLConnection} + * + * @param con The connection to get the string from + * @return The body of the returned page + */ + private static String connectionToString(HttpURLConnection con) throws IOException { + // Send the request (we don't use this but its required for getErrorStream() to work) + con.getResponseCode(); + + // Read the error message if there is one if not just read normally + InputStream inputStream = con.getErrorStream(); + if (inputStream == null) { + inputStream = con.getInputStream(); + } + + try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream))) { + String content = in.lines().collect(Collectors.joining("\n")); + con.disconnect(); + return content; + } + } + + /** + * Get the filename at the end of a url + * @param url The url to get the filename at the end from. Should not end with a / + */ + public static String getFileName(String url) { + return url.substring(url.lastIndexOf("/") + 1); + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/spigot/SpigotPlayerHandler.java b/src/main/java/dev/projectg/geyserupdater/spigot/SpigotPlayerHandler.java new file mode 100644 index 00000000..41f3a6f4 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/spigot/SpigotPlayerHandler.java @@ -0,0 +1,33 @@ +package dev.projectg.geyserupdater.spigot; + +import dev.projectg.geyserupdater.common.PlayerManager; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class SpigotPlayerHandler implements PlayerManager { + + private final Server server; + + public SpigotPlayerHandler(Server server) { + this.server = server; + } + + @Override + public @NotNull List getOnlinePlayers() { + List uuidList = new ArrayList<>(); + for (Player player : server.getOnlinePlayers()) { + uuidList.add(player.getUniqueId()); + } + return uuidList; + } + + @Override + public void sendMessage(@NotNull UUID uuid, @NotNull String message) { + server.getPlayer(uuid).sendMessage(message); + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/spigot/SpigotScheduler.java b/src/main/java/dev/projectg/geyserupdater/spigot/SpigotScheduler.java new file mode 100644 index 00000000..2cf37149 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/spigot/SpigotScheduler.java @@ -0,0 +1,61 @@ +package dev.projectg.geyserupdater.spigot; + +import dev.projectg.geyserupdater.common.scheduler.Task; +import dev.projectg.geyserupdater.common.scheduler.UpdaterScheduler; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.scheduler.BukkitTask; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nonnull; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +public class SpigotScheduler implements UpdaterScheduler { + + private final Plugin plugin; + + public SpigotScheduler(@Nonnull Plugin plugin) { + Objects.requireNonNull(plugin); + this.plugin = plugin; + } + + @Override + public Task schedule(@NotNull Runnable runnable, boolean async, long delay, long repeat, TimeUnit unit) { + // https://hub.spigotmc.org/stash/projects/SPIGOT/repos/craftbukkit/browse/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java + // https://hub.spigotmc.org/stash/projects/SPIGOT/repos/craftbukkit/browse/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java + + Objects.requireNonNull(runnable); + + BukkitScheduler scheduler = plugin.getServer().getScheduler(); + + BukkitTask bukkitTask; + if (repeat <= 0) { + if (async) { + bukkitTask = scheduler.runTaskLaterAsynchronously(plugin, runnable, unit.toSeconds(delay) * 20); // 20 ticks in a second + } else { + bukkitTask = scheduler.runTaskLater(plugin, runnable, unit.toSeconds(delay) * 20); + } + } else { + if (async) { + bukkitTask = scheduler.runTaskTimerAsynchronously(plugin, runnable, unit.toSeconds(delay) * 20, unit.toSeconds(repeat) * 20); + } else { + bukkitTask = scheduler.runTaskTimer(plugin, runnable, unit.toSeconds(delay) * 20, unit.toSeconds(repeat) * 20); + } + } + return new SpigotTask(bukkitTask); + } + + private static class SpigotTask implements Task { + private final BukkitTask task; + + private SpigotTask(BukkitTask task) { + this.task = task; + } + + @Override + public void cancel() { + task.cancel(); + } + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/spigot/SpigotUpdater.java b/src/main/java/dev/projectg/geyserupdater/spigot/SpigotUpdater.java new file mode 100644 index 00000000..8cfedd6e --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/spigot/SpigotUpdater.java @@ -0,0 +1,69 @@ +package dev.projectg.geyserupdater.spigot; + +import dev.projectg.geyserupdater.common.GeyserUpdater; +import dev.projectg.geyserupdater.common.UpdaterBootstrap; +import dev.projectg.geyserupdater.common.logger.JavaUtilUpdaterLogger; +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; +import dev.projectg.geyserupdater.spigot.command.GeyserUpdateCommand; +import dev.projectg.geyserupdater.spigot.util.CheckSpigotRestart; +import dev.projectg.geyserupdater.spigot.util.bstats.Metrics; + +import org.bukkit.Server; +import org.bukkit.plugin.java.JavaPlugin; +import space.arim.dazzleconf.error.InvalidConfigException; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Objects; + +public class SpigotUpdater extends JavaPlugin implements UpdaterBootstrap { + + private GeyserUpdater updater; + + + @Override + public void onEnable() { + Server server = this.getServer(); + Path updateFolder = server.getUpdateFolderFile().toPath(); + try { + updater = new GeyserUpdater( + this.getDataFolder().toPath(), + updateFolder, + updateFolder, + this, + new JavaUtilUpdaterLogger(getLogger()), + new SpigotScheduler(this), + new SpigotPlayerHandler(server), + this.getDescription().getVersion(), + "bootstrap/spigot/target/Geyser-Spigot.jar", + "bootstrap/spigot/target/floodgate-spigot.jar" + ); + } catch (IOException | InvalidConfigException e) { + getLogger().severe("Failed to start GeyserUpdater! Disabling..."); + e.printStackTrace(); + return; + } + + Objects.requireNonNull(getCommand("geyserupdate")).setExecutor(new GeyserUpdateCommand()); + getCommand("geyserupdate").setPermission("gupdater.geyserupdate"); + new Metrics(this, 10202); + } + + @Override + public void onDisable() { + // bukkit has the native update folder to update jars on startup, which means we don't need to worry about modifying jars in use and when we shutdown + if (updater != null) { + try { + updater.shutdown(); + } catch (IOException e) { + UpdaterLogger.getLogger().error("Failed to install ALL updates:"); + e.printStackTrace(); + } + } + } + + @Override + public void createRestartScript() throws IOException { + CheckSpigotRestart.checkYml(); + } +} diff --git a/src/main/java/com/projectg/geyserupdater/spigot/command/GeyserUpdateCommand.java b/src/main/java/dev/projectg/geyserupdater/spigot/command/GeyserUpdateCommand.java similarity index 83% rename from src/main/java/com/projectg/geyserupdater/spigot/command/GeyserUpdateCommand.java rename to src/main/java/dev/projectg/geyserupdater/spigot/command/GeyserUpdateCommand.java index 859e4ee0..a01e0d4a 100644 --- a/src/main/java/com/projectg/geyserupdater/spigot/command/GeyserUpdateCommand.java +++ b/src/main/java/dev/projectg/geyserupdater/spigot/command/GeyserUpdateCommand.java @@ -1,27 +1,23 @@ -package com.projectg.geyserupdater.spigot.command; +package dev.projectg.geyserupdater.spigot.command; -import com.projectg.geyserupdater.common.Messages; -import com.projectg.geyserupdater.common.logger.UpdaterLogger; -import com.projectg.geyserupdater.common.util.GeyserProperties; -import com.projectg.geyserupdater.spigot.util.GeyserSpigotDownloader; +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; -import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; -import org.bukkit.command.ConsoleCommandSender; -import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -import java.io.IOException; - public class GeyserUpdateCommand implements CommandExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { UpdaterLogger logger = UpdaterLogger.getLogger(); + //todo: spigot command + + /* + if (sender instanceof Player) { Player player = (Player) sender; if (command.getName().equalsIgnoreCase("geyserupdate") && player.hasPermission("gupdater.geyserupdate")) { @@ -57,6 +53,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command } else { return false; } + */ return true; } } \ No newline at end of file diff --git a/src/main/java/com/projectg/geyserupdater/spigot/util/CheckSpigotRestart.java b/src/main/java/dev/projectg/geyserupdater/spigot/util/CheckSpigotRestart.java similarity index 90% rename from src/main/java/com/projectg/geyserupdater/spigot/util/CheckSpigotRestart.java rename to src/main/java/dev/projectg/geyserupdater/spigot/util/CheckSpigotRestart.java index 3c85fdcd..66f829c4 100644 --- a/src/main/java/com/projectg/geyserupdater/spigot/util/CheckSpigotRestart.java +++ b/src/main/java/dev/projectg/geyserupdater/spigot/util/CheckSpigotRestart.java @@ -1,8 +1,8 @@ -package com.projectg.geyserupdater.spigot.util; +package dev.projectg.geyserupdater.spigot.util; -import com.projectg.geyserupdater.common.logger.UpdaterLogger; -import com.projectg.geyserupdater.common.util.OsUtils; -import com.projectg.geyserupdater.common.util.ScriptCreator; +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; +import dev.projectg.geyserupdater.common.util.OsUtils; +import dev.projectg.geyserupdater.common.util.ScriptCreator; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; diff --git a/src/main/java/com/projectg/geyserupdater/spigot/util/bstats/Metrics.java b/src/main/java/dev/projectg/geyserupdater/spigot/util/bstats/Metrics.java similarity index 97% rename from src/main/java/com/projectg/geyserupdater/spigot/util/bstats/Metrics.java rename to src/main/java/dev/projectg/geyserupdater/spigot/util/bstats/Metrics.java index 54c664b9..c58c626c 100644 --- a/src/main/java/com/projectg/geyserupdater/spigot/util/bstats/Metrics.java +++ b/src/main/java/dev/projectg/geyserupdater/spigot/util/bstats/Metrics.java @@ -1,10 +1,9 @@ -package com.projectg.geyserupdater.spigot.util.bstats; +package dev.projectg.geyserupdater.spigot.util.bstats; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; - import org.bukkit.Bukkit; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; @@ -13,26 +12,13 @@ import org.bukkit.plugin.ServicePriority; import javax.net.ssl.HttpsURLConnection; -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; +import java.io.*; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.Callable; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; +import java.util.*; +import java.util.concurrent.*; import java.util.logging.Level; import java.util.zip.GZIPOutputStream; @@ -72,7 +58,7 @@ public class Metrics { private static final String URL = "https://bStats.org/submitData/bukkit"; // Is bStats enabled on this server? - private boolean enabled; + private final boolean enabled; // Should failed requests be logged? private static boolean logFailedRequests; diff --git a/src/main/java/dev/projectg/geyserupdater/velocity/VelocityPlayerHandler.java b/src/main/java/dev/projectg/geyserupdater/velocity/VelocityPlayerHandler.java new file mode 100644 index 00000000..acb04caf --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/velocity/VelocityPlayerHandler.java @@ -0,0 +1,34 @@ +package dev.projectg.geyserupdater.velocity; + +import dev.projectg.geyserupdater.common.PlayerManager; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class VelocityPlayerHandler implements PlayerManager { + + private final ProxyServer proxyServer; + + public VelocityPlayerHandler(@Nonnull ProxyServer proxyServer) { + this.proxyServer = proxyServer; + } + + @Override + public @NotNull List getOnlinePlayers() { + List uuidList = new ArrayList<>(); + for (Player player : proxyServer.getAllPlayers()) { + uuidList.add(player.getUniqueId()); + } + return uuidList; + } + + @Override + public void sendMessage(@NotNull UUID uuid, @NotNull String message) { + + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/velocity/VelocityScheduler.java b/src/main/java/dev/projectg/geyserupdater/velocity/VelocityScheduler.java new file mode 100644 index 00000000..2e0999ac --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/velocity/VelocityScheduler.java @@ -0,0 +1,45 @@ +package dev.projectg.geyserupdater.velocity; + +import dev.projectg.geyserupdater.common.scheduler.Task; +import dev.projectg.geyserupdater.common.scheduler.UpdaterScheduler; +import com.velocitypowered.api.scheduler.ScheduledTask; + +import javax.annotation.Nonnull; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +public class VelocityScheduler implements UpdaterScheduler { + + private final VelocityUpdater plugin; + + public VelocityScheduler(@Nonnull VelocityUpdater plugin) { + Objects.requireNonNull(plugin); + this.plugin = plugin; + } + + @Override + public Task schedule(@Nonnull Runnable runnable, boolean async, long delay, long repeat, @Nonnull TimeUnit unit) { + // https://github.com/VelocityPowered/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java + + Objects.requireNonNull(runnable); + Objects.requireNonNull(unit); + + return new VelocityTask(plugin.getProxyServer().getScheduler().buildTask(plugin, runnable) + .delay(delay, unit) + .repeat(repeat, unit) + .schedule()); + } + + private static class VelocityTask implements Task { + private final ScheduledTask task; + + private VelocityTask(ScheduledTask task) { + this.task = task; + } + + @Override + public void cancel() { + task.cancel(); + } + } +} diff --git a/src/main/java/dev/projectg/geyserupdater/velocity/VelocityUpdater.java b/src/main/java/dev/projectg/geyserupdater/velocity/VelocityUpdater.java new file mode 100644 index 00000000..0775f770 --- /dev/null +++ b/src/main/java/dev/projectg/geyserupdater/velocity/VelocityUpdater.java @@ -0,0 +1,100 @@ +package dev.projectg.geyserupdater.velocity; + +import com.google.inject.Inject; +import dev.projectg.geyserupdater.common.GeyserUpdater; +import dev.projectg.geyserupdater.common.UpdaterBootstrap; +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; +import dev.projectg.geyserupdater.common.util.ScriptCreator; +import dev.projectg.geyserupdater.velocity.command.GeyserUpdateCommand; +import dev.projectg.geyserupdater.velocity.logger.Slf4jUpdaterLogger; +import dev.projectg.geyserupdater.velocity.bstats.Metrics; +import com.velocitypowered.api.event.PostOrder; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; +import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; +import com.velocitypowered.api.plugin.Plugin; +import com.velocitypowered.api.plugin.annotation.DataDirectory; +import com.velocitypowered.api.proxy.ProxyServer; +import org.slf4j.Logger; +import space.arim.dazzleconf.error.InvalidConfigException; + +import java.io.IOException; +import java.nio.file.Path; + +@Plugin(id = "geyserupdater", name = "GeyserUpdater", version = VelocityUpdater.VERSION, + description = "Automatically or manually downloads new builds of Geyser and applies them on server restart.", + authors = {"Jens", "Konicai"}) +public class VelocityUpdater implements UpdaterBootstrap { + + public static final String VERSION = "1.6.0"; + + private GeyserUpdater updater; + private final ProxyServer server; + private final Path dataDirectory; + private final Logger logger; + private final Metrics.Factory metricsFactory; + + @Inject + public VelocityUpdater(ProxyServer server, Logger baseLogger, @DataDirectory final Path dataDirectory, Metrics.Factory metricsFactory) { + this.server = server; + this.dataDirectory = dataDirectory; + this.logger = baseLogger; + this.metricsFactory = metricsFactory; + } + + @Subscribe + public void onProxyInitialization(ProxyInitializeEvent event) throws IOException, InvalidConfigException { + + updater = new GeyserUpdater( + dataDirectory, + dataDirectory.resolve("BuildUpdate"), + dataDirectory.getParent(), + this, + new Slf4jUpdaterLogger(logger), + new VelocityScheduler(this), + new VelocityPlayerHandler(server), + VERSION, + "bootstrap/velocity/target/Geyser-Velocity.jar", + "bootstrap/velocity/target/floodgate-velocity.jar" + ); + + // Register our only command + server.getCommandManager().register("geyserupdate", new GeyserUpdateCommand()); + metricsFactory.make(this, 10673); + } + + @Subscribe(order = PostOrder.LAST) + public void onShutdown(ProxyShutdownEvent event) { + // PostOrder.LAST ensures that we don't modify plugin jars while they are being used. + // Hopefully plugins that are being updated don't also use PostOrder.LAST + onDisable(); + } + + @Override + public void onDisable() { + + if (updater != null) { + try { + updater.shutdown(); + } catch (IOException e) { + UpdaterLogger.getLogger().error("Failed to install ALL updates:"); + e.printStackTrace(); + } + } + } + + @Override + public void createRestartScript() throws IOException { + ScriptCreator.createRestartScript(true); + } + + public ProxyServer getProxyServer() { + return server; + } +} + + + + + + diff --git a/src/main/java/com/projectg/geyserupdater/velocity/util/bstats/Metrics.java b/src/main/java/dev/projectg/geyserupdater/velocity/bstats/Metrics.java similarity index 98% rename from src/main/java/com/projectg/geyserupdater/velocity/util/bstats/Metrics.java rename to src/main/java/dev/projectg/geyserupdater/velocity/bstats/Metrics.java index c1956535..4884ae47 100644 --- a/src/main/java/com/projectg/geyserupdater/velocity/util/bstats/Metrics.java +++ b/src/main/java/dev/projectg/geyserupdater/velocity/bstats/Metrics.java @@ -1,36 +1,18 @@ -package com.projectg.geyserupdater.velocity.util.bstats; +package dev.projectg.geyserupdater.velocity.bstats; +import com.google.inject.Inject; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; - -import com.google.inject.Inject; - import org.slf4j.Logger; import javax.net.ssl.HttpsURLConnection; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStreamReader; +import java.io.*; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -955,7 +937,7 @@ private void writeConfig() throws IOException { "# There is no performance penalty associated with having metrics enabled, and data sent to"); configContent.add("# bStats is fully anonymous."); configContent.add("enabled=" + defaultEnabled); - configContent.add("server-uuid=" + UUID.randomUUID().toString()); + configContent.add("server-uuid=" + UUID.randomUUID()); configContent.add("log-errors=false"); configContent.add("log-sent-data=false"); configContent.add("log-response-status-text=false"); diff --git a/src/main/java/com/projectg/geyserupdater/velocity/command/GeyserUpdateCommand.java b/src/main/java/dev/projectg/geyserupdater/velocity/command/GeyserUpdateCommand.java similarity index 72% rename from src/main/java/com/projectg/geyserupdater/velocity/command/GeyserUpdateCommand.java rename to src/main/java/dev/projectg/geyserupdater/velocity/command/GeyserUpdateCommand.java index c5ddae66..51b3196b 100644 --- a/src/main/java/com/projectg/geyserupdater/velocity/command/GeyserUpdateCommand.java +++ b/src/main/java/dev/projectg/geyserupdater/velocity/command/GeyserUpdateCommand.java @@ -1,20 +1,15 @@ -package com.projectg.geyserupdater.velocity.command; +package dev.projectg.geyserupdater.velocity.command; -import com.projectg.geyserupdater.common.Messages; -import com.projectg.geyserupdater.common.util.GeyserProperties; -import com.projectg.geyserupdater.velocity.util.GeyserVelocityDownloader; - -import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.command.RawCommand; -import net.kyori.adventure.text.Component; - -import java.io.IOException; - public class GeyserUpdateCommand implements RawCommand { @Override public void execute(final Invocation invocation) { + + //todo: velocity command + + /* CommandSource source = invocation.source(); try { @@ -30,6 +25,7 @@ public void execute(final Invocation invocation) { source.sendMessage(Component.text(Messages.Command.FAIL_CHECK)); e.printStackTrace(); } + */ } @Override public boolean hasPermission(final Invocation invocation) { diff --git a/src/main/java/com/projectg/geyserupdater/velocity/logger/Slf4jUpdaterLogger.java b/src/main/java/dev/projectg/geyserupdater/velocity/logger/Slf4jUpdaterLogger.java similarity index 91% rename from src/main/java/com/projectg/geyserupdater/velocity/logger/Slf4jUpdaterLogger.java rename to src/main/java/dev/projectg/geyserupdater/velocity/logger/Slf4jUpdaterLogger.java index d8fbb5f5..9bfc2fde 100644 --- a/src/main/java/com/projectg/geyserupdater/velocity/logger/Slf4jUpdaterLogger.java +++ b/src/main/java/dev/projectg/geyserupdater/velocity/logger/Slf4jUpdaterLogger.java @@ -1,6 +1,6 @@ -package com.projectg.geyserupdater.velocity.logger; +package dev.projectg.geyserupdater.velocity.logger; -import com.projectg.geyserupdater.common.logger.UpdaterLogger; +import dev.projectg.geyserupdater.common.logger.UpdaterLogger; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.config.Configurator; import org.slf4j.Logger; diff --git a/src/main/resources/bungee.yml b/src/main/resources/bungee.yml index b2cdfbdb..a55d0c0e 100644 --- a/src/main/resources/bungee.yml +++ b/src/main/resources/bungee.yml @@ -1,6 +1,5 @@ name: ${project.name} -main: com.projectg.geyserupdater.bungee.BungeeUpdater +main: dev.projectg.geyserupdater.bungee.BungeeUpdater version: ${project.version} description: Automatically or manually downloads new builds of Geyser and applies them on server restart. -author: Jens -depends: ["Geyser-BungeeCord"] +author: Jens, Konicai \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 78e0f5e8..41436ade 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -4,23 +4,60 @@ # NOTICE: Please read the README on our github page for full information regarding these options! # https://github.com/ProjectG-Plugins/GeyserUpdater -# If enabled, GeyserUpdater will check for new Geyser builds on server start, and on the interval specified by Auto-Update-Interval. If a new build exists, it will be downloaded. -Auto-Update-Geyser: false # The interval in hours between each auto update check. -Auto-Update-Interval: 24 +auto-check-interval: 24 +# The maximum amount of seconds that a download is allowed to run for. Increase if you are running into issues on a slow internet connection. +download-time-limit: 180 +# Delete the downloaded file if the file hash of the downloaded file did not match what the download server provided. +# Or if the file hash was not checked and the download time limit was reached, or an exception occurred. +# If the file hash is not correct the downloaded file is likely corrupt or unfinished. +delete-on-fail: true # If enabled, GeyserUpdater will attempt to restart the server 10 seconds after a new version of Geyser has been successfully downloaded. # If you aren't using a hosting provider or a server wrapper, you will need a restart script. -Auto-Restart-Server: false +restart-server: false # When enabled, GeyserUpdater will automatically generate a restart script for you. If you are using CraftBukkit or a proxy # you will need to use the generated script to start your server! If you are using a hosting provider or a server wrapper you probably don't need this. -Auto-Script-Generating: false - +auto-script-generating: false # Configure the message that is sent to all online players warning them that the server will be restarting in 10 seconds. -Restart-Message-Players: '&2This server will be restarting in 10 seconds!' +restart-message-players: "§2This server will be restarting in 10 seconds!" # Enable debug logging -Enable-Debug: false - +enable-debug: false # Please do not change this version value! -Config-Version: 2 \ No newline at end of file +config-version: 3 + + +modules: + GeyserHub: + enable: true + auto-check: true + auto-update: false + version: + jenkins: + # Requires plugin to have a git.properties containing a git.build.number value + project: "https://ci.opencollab.dev/job/GeyserMC/job/Geyser/job/master" + spigot: + # Compares version of plugin that the platform provides vs the latest version on SpigotMC + resource: 88555 + github: + id: + + download: + literal: + # Allows specifying a static download location and file + file: "https://ci.opencollab.dev/job/GeyserMC/job/Geyser/job/master/lastSuccessfulBuild/artifact/bootstrap/spigot/target/Geyser-Spigot.jar" + spigot: + # Downloads file from a SpigotMC listing. The download link provided on SpigotMC must lead to an immediate file download. + resource: 88555 + +presets: + geyser: + enable: true + auto-check: true + auto-update: true + custom: + # Certain presets may require or allow additional data + branch: master + compare-hash: true + diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 6e6c49fe..441c8e9f 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,9 +1,11 @@ name: ${project.name} -main: com.projectg.geyserupdater.spigot.SpigotUpdater +main: dev.projectg.geyserupdater.spigot.SpigotUpdater version: ${project.version} -depend: [Geyser-Spigot] -description: Automatically or manually downloads new builds of Geyser and applies them on server restart. api-version: 1.13 +description: Automatically or manually downloads new builds of Geyser and applies them on server restart. +authors: + - Jens + - Konicai commands: geyserupdate: