mirror of
https://github.com/LeavesMC/Leaves.git
synced 2025-12-19 14:59:32 +00:00
304 lines
14 KiB
Diff
304 lines
14 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: violetc <58360096+s-yh-china@users.noreply.github.com>
|
|
Date: Mon, 31 Jul 2023 17:42:25 +0800
|
|
Subject: [PATCH] Leaves Updater
|
|
|
|
---------
|
|
|
|
Co-authored-by: MC_XiaoHei <xiaohei.xor7studio@foxmail.com>
|
|
|
|
diff --git a/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java
|
|
index ebd62b1d3a60d3e22e3849047293e1f62533eae7..e964f05c5c0f2d1d9b4c0a3459076e4b95f0f4e8 100644
|
|
--- a/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java
|
|
+++ b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java
|
|
@@ -33,6 +33,7 @@ public final class LeavesCommand extends Command {
|
|
private static final Map<String, LeavesSubcommand> SUBCOMMANDS = Util.make(() -> {
|
|
final Map<Set<String>, LeavesSubcommand> commands = new HashMap<>();
|
|
commands.put(Set.of("config"), new ConfigCommand());
|
|
+ commands.put(Set.of("update"), new UpdateCommand());
|
|
|
|
return commands.entrySet().stream()
|
|
.flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue())))
|
|
diff --git a/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java b/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..7f94df607e8ffd48ab2cb7c90d520c2b4a906cdc
|
|
--- /dev/null
|
|
+++ b/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java
|
|
@@ -0,0 +1,21 @@
|
|
+package org.leavesmc.leaves.command.subcommands;
|
|
+
|
|
+import org.bukkit.ChatColor;
|
|
+import org.bukkit.command.CommandSender;
|
|
+import org.leavesmc.leaves.command.LeavesSubcommand;
|
|
+import org.leavesmc.leaves.util.LeavesUpdateHelper;
|
|
+
|
|
+public class UpdateCommand implements LeavesSubcommand {
|
|
+
|
|
+ @Override
|
|
+ public boolean execute(CommandSender sender, String subCommand, String[] args) {
|
|
+ sender.sendMessage(ChatColor.GRAY + "Trying to update Leaves, see the console for more info.");
|
|
+ LeavesUpdateHelper.tryUpdateLeaves();
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean tabCompletes() {
|
|
+ return false;
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/org/leavesmc/leaves/util/LeavesUpdateHelper.java b/src/main/java/org/leavesmc/leaves/util/LeavesUpdateHelper.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..f72e8bca8c2978abaf1acab30ca0853d484c1e6f
|
|
--- /dev/null
|
|
+++ b/src/main/java/org/leavesmc/leaves/util/LeavesUpdateHelper.java
|
|
@@ -0,0 +1,249 @@
|
|
+package org.leavesmc.leaves.util;
|
|
+
|
|
+import com.google.common.base.Charsets;
|
|
+import com.google.gson.Gson;
|
|
+import com.google.gson.JsonArray;
|
|
+import com.google.gson.JsonElement;
|
|
+import com.google.gson.JsonObject;
|
|
+import com.google.gson.JsonSyntaxException;
|
|
+import io.papermc.paper.ServerBuildInfo;
|
|
+import net.minecraft.Util;
|
|
+import org.jetbrains.annotations.NotNull;
|
|
+import org.leavesmc.leaves.LeavesConfig;
|
|
+import org.leavesmc.leaves.LeavesLogger;
|
|
+
|
|
+import java.io.BufferedReader;
|
|
+import java.io.BufferedWriter;
|
|
+import java.io.File;
|
|
+import java.io.FileInputStream;
|
|
+import java.io.FileWriter;
|
|
+import java.io.IOException;
|
|
+import java.io.InputStreamReader;
|
|
+import java.net.HttpURLConnection;
|
|
+import java.net.URI;
|
|
+import java.net.URISyntaxException;
|
|
+import java.net.URL;
|
|
+import java.nio.channels.Channels;
|
|
+import java.nio.channels.FileChannel;
|
|
+import java.nio.channels.ReadableByteChannel;
|
|
+import java.nio.file.Files;
|
|
+import java.nio.file.Path;
|
|
+import java.security.MessageDigest;
|
|
+import java.time.Duration;
|
|
+import java.time.LocalTime;
|
|
+import java.util.Map;
|
|
+import java.util.concurrent.Executors;
|
|
+import java.util.concurrent.ScheduledExecutorService;
|
|
+import java.util.concurrent.TimeUnit;
|
|
+import java.util.concurrent.locks.ReentrantLock;
|
|
+
|
|
+import static java.nio.file.StandardOpenOption.CREATE;
|
|
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
|
|
+import static java.nio.file.StandardOpenOption.WRITE;
|
|
+
|
|
+public class LeavesUpdateHelper {
|
|
+
|
|
+ private final static String autoUpdateDir = "auto_update";
|
|
+ private final static String corePathFileName = autoUpdateDir + File.separator + "core.path";
|
|
+
|
|
+ private final static ReentrantLock updateLock = new ReentrantLock();
|
|
+ private static boolean updateTaskStarted = false;
|
|
+
|
|
+ private static final ScheduledExecutorService autoUpdateExecutor = Executors.newScheduledThreadPool(1);
|
|
+
|
|
+ public static void init() {
|
|
+ File workingDirFile = new File(autoUpdateDir);
|
|
+ if (!workingDirFile.exists()) {
|
|
+ if (!workingDirFile.mkdir()) {
|
|
+ LeavesLogger.LOGGER.warning("Failed to create working directory: " + autoUpdateDir);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ File corePathFile = new File(corePathFileName);
|
|
+ if (!corePathFile.exists()) {
|
|
+ try {
|
|
+ if (!corePathFile.createNewFile()) {
|
|
+ throw new IOException();
|
|
+ }
|
|
+ } catch (IOException e) {
|
|
+ LeavesLogger.LOGGER.severe("Failed to create core path file: " + corePathFileName, e);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ File leavesUpdateDir = new File(autoUpdateDir + File.separator + "leaves");
|
|
+ if (!leavesUpdateDir.exists()) {
|
|
+ if (!leavesUpdateDir.mkdir()) {
|
|
+ LeavesLogger.LOGGER.warning("Failed to create leaves update directory: " + leavesUpdateDir);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (LeavesConfig.autoUpdate) {
|
|
+ LocalTime currentTime = LocalTime.now();
|
|
+ long dailyTaskPeriod = 24 * 60 * 60 * 1000;
|
|
+
|
|
+ for (String time : LeavesConfig.autoUpdateTime) {
|
|
+ try {
|
|
+ LocalTime taskTime = LocalTime.of(Integer.parseInt(time.split(":")[0]), Integer.parseInt(time.split(":")[1]));
|
|
+ Duration task = Duration.between(currentTime, taskTime);
|
|
+ if (task.isNegative()) {
|
|
+ task = task.plusDays(1);
|
|
+ }
|
|
+ autoUpdateExecutor.scheduleAtFixedRate(LeavesUpdateHelper::tryUpdateLeaves, task.toMillis(), dailyTaskPeriod, TimeUnit.MILLISECONDS);
|
|
+ } catch (Exception ignored) {
|
|
+ LeavesLogger.LOGGER.warning("Illegal auto-update time ignored: " + time);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static void tryUpdateLeaves() {
|
|
+ updateLock.lock();
|
|
+ try {
|
|
+ if (!updateTaskStarted) {
|
|
+ updateTaskStarted = true;
|
|
+ new Thread(LeavesUpdateHelper::downloadLeaves).start();
|
|
+ }
|
|
+ } finally {
|
|
+ updateLock.unlock();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private static void downloadLeaves() {
|
|
+ ServerBuildInfo version = ServerBuildInfo.buildInfo();
|
|
+ if (version.gitCommit().isEmpty() || version.buildNumber().isEmpty()) {
|
|
+ LeavesLogger.LOGGER.info("IDE, custom build? Can not update!");
|
|
+ updateTaskStarted = false;
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ LeavesLogger.LOGGER.info("Now gitHash: " + version.gitCommit().get());
|
|
+ LeavesLogger.LOGGER.info("Trying to get latest build info.");
|
|
+ LeavesBuildInfo buildInfo = getLatestBuildInfo(version.minecraftVersionId(), version.gitCommit().get());
|
|
+
|
|
+ if (buildInfo != LeavesBuildInfo.ERROR) {
|
|
+ if (!buildInfo.needUpdate) {
|
|
+ LeavesLogger.LOGGER.warning("You are running the latest version, stopping update.");
|
|
+ updateTaskStarted = false;
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ LeavesLogger.LOGGER.info("Got build info, trying to download " + buildInfo.fileName);
|
|
+ try {
|
|
+ Path outFile = Path.of(autoUpdateDir, "leaves", buildInfo.fileName + ".cache");
|
|
+ Files.deleteIfExists(outFile);
|
|
+
|
|
+ try (
|
|
+ final ReadableByteChannel source = Channels.newChannel(new URI(
|
|
+ buildInfo.url + LeavesConfig.autoUpdateSource).toURL().openStream()
|
|
+ );
|
|
+ final FileChannel fileChannel = FileChannel.open(outFile, CREATE, WRITE, TRUNCATE_EXISTING)
|
|
+ ) {
|
|
+ fileChannel.transferFrom(source, 0, Long.MAX_VALUE);
|
|
+ LeavesLogger.LOGGER.info("Download " + buildInfo.fileName + " completed.");
|
|
+ } catch (final IOException e) {
|
|
+ LeavesLogger.LOGGER.warning("Download " + buildInfo.fileName + " failed.", e);
|
|
+ Files.deleteIfExists(outFile);
|
|
+ updateTaskStarted = false;
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if (!isFileValid(outFile, buildInfo.sha256)) {
|
|
+ LeavesLogger.LOGGER.warning("Hash check failed for downloaded file " + buildInfo.fileName);
|
|
+ Files.deleteIfExists(outFile);
|
|
+ updateTaskStarted = false;
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ File nowServerCore = new File(autoUpdateDir + File.separator + "leaves" + File.separator + buildInfo.fileName);
|
|
+ File backupServerCore = new File(autoUpdateDir + File.separator + "leaves" + File.separator + buildInfo.fileName + ".old");
|
|
+ Util.safeReplaceFile(nowServerCore.toPath(), outFile, backupServerCore.toPath());
|
|
+
|
|
+ try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(corePathFileName))) {
|
|
+ bufferedWriter.write(autoUpdateDir + File.separator + "leaves" + File.separator + buildInfo.fileName);
|
|
+ } catch (IOException e) {
|
|
+ LeavesLogger.LOGGER.warning("Fail to download leaves core", e);
|
|
+ updateTaskStarted = false;
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ LeavesLogger.LOGGER.info("Leaves update completed, please restart your server.");
|
|
+ } catch (Exception e) {
|
|
+ LeavesLogger.LOGGER.severe("Leaves update failed", e);
|
|
+ }
|
|
+ } else {
|
|
+ LeavesLogger.LOGGER.warning("Stopping update.");
|
|
+ }
|
|
+ updateTaskStarted = false;
|
|
+ }
|
|
+
|
|
+ private static boolean isFileValid(Path file, String hash) {
|
|
+ try (FileInputStream inputStream = new FileInputStream(file.toFile())) {
|
|
+ byte[] buffer = new byte[1024];
|
|
+ MessageDigest md5 = MessageDigest.getInstance("SHA-256");
|
|
+
|
|
+ for (int numRead; (numRead = inputStream.read(buffer)) > 0; ) {
|
|
+ md5.update(buffer, 0, numRead);
|
|
+ }
|
|
+
|
|
+ return toHexString(md5.digest()).equals(hash);
|
|
+ } catch (Exception e) {
|
|
+ LeavesLogger.LOGGER.warning("Fail to validate file " + file, e);
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ @NotNull
|
|
+ private static String toHexString(byte @NotNull [] bytes) {
|
|
+ StringBuilder builder = new StringBuilder();
|
|
+ for (byte b : bytes) {
|
|
+ builder.append(String.format("%02x", b));
|
|
+ }
|
|
+ return builder.toString();
|
|
+ }
|
|
+
|
|
+ private static LeavesBuildInfo getLatestBuildInfo(String mcVersion, String gitHash) {
|
|
+ try {
|
|
+ HttpURLConnection connection = (HttpURLConnection) new URI(
|
|
+ "https://api.leavesmc.org/v2/projects/leaves/versions/" + mcVersion + "/builds/latest"
|
|
+ ).toURL().openConnection();
|
|
+ connection.connect();
|
|
+ if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
|
|
+ return LeavesBuildInfo.ERROR;
|
|
+ }
|
|
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8))) {
|
|
+ JsonObject obj = new Gson().fromJson(reader, JsonObject.class);
|
|
+ String channel = obj.get("channel").getAsString();
|
|
+ if ("experimental".equals(channel) && !LeavesConfig.autoUpdateAllowExperimental) {
|
|
+ LeavesLogger.LOGGER.warning("Experimental version is not allowed to update for default, if you really want to update, please set misc.auto-update.allow-experimental to true in leaves.yml");
|
|
+ return LeavesBuildInfo.ERROR;
|
|
+ }
|
|
+ int build = obj.get("build").getAsInt();
|
|
+
|
|
+ JsonArray changes = obj.get("changes").getAsJsonArray();
|
|
+ boolean needUpdate = true;
|
|
+ for (JsonElement change : changes) {
|
|
+ if (change.getAsJsonObject().get("commit").getAsString().startsWith(gitHash)) {
|
|
+ needUpdate = false;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ JsonObject downloadInfo = obj.get("downloads").getAsJsonObject().get("application").getAsJsonObject();
|
|
+ String fileName = downloadInfo.get("name").getAsString();
|
|
+ String sha256 = downloadInfo.get("sha256").getAsString();
|
|
+ String url = "https://api.leavesmc.org/v2/projects/leaves/versions/" + mcVersion + "/builds/" + build + "/downloads/";
|
|
+ return new LeavesBuildInfo(build, fileName, sha256, needUpdate, url);
|
|
+ } catch (JsonSyntaxException | NumberFormatException e) {
|
|
+ LeavesLogger.LOGGER.warning("Fail to get latest build info", e);
|
|
+ return LeavesBuildInfo.ERROR;
|
|
+ }
|
|
+ } catch (IOException | URISyntaxException e) {
|
|
+ LeavesLogger.LOGGER.warning("Fail to get latest build info", e);
|
|
+ return LeavesBuildInfo.ERROR;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private record LeavesBuildInfo(int build, String fileName, String sha256, boolean needUpdate, String url) {
|
|
+ public static LeavesBuildInfo ERROR = null;
|
|
+ }
|
|
+}
|