diff --git a/.gitignore b/.gitignore index c6d98d1..5ff6309 100644 --- a/.gitignore +++ b/.gitignore @@ -1,98 +1,38 @@ -# ---> JetBrains -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -# ---> Maven target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -# https://github.com/takari/maven-wrapper#usage-without-binary-jar -.mvn/wrapper/maven-wrapper.jar +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ -# Eclipse m2e generated files -# Eclipse Core -.project -# JDT-specific (Eclipse Java Development Tools) +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated .classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..03351b2 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 9954939..b41edd8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ -# FMCServer +# FMCServer - ForeverMC 服务器插件 -ForeverMC 服务器状态报告插件 \ No newline at end of file +适用版本:1.9.4 - 最新 + +详细说明请看主分支 README.md diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..dc123f3 --- /dev/null +++ b/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + + net.imyeyu.fmcserver + FMCServer + 0.0.2 + jar + + + 1.9.4+ + 8 + 8 + 8 + UTF-8 + + + + + spigot-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + ${project.artifactId}-${project.version}+${mc.version} + E:\SpigotMC\test194\plugins + + jar-with-dependencies + + + + + package + + single + + + + + + + + + + org.apache.commons + commons-lang3 + 3.17.0 + + + org.apache.commons + commons-io + 1.3.2 + + + commons-codec + commons-codec + 1.17.1 + + + org.apache.httpcomponents.client5 + httpclient5-fluent + 5.4.1 + + + org.spigotmc + spigot-api + 1.9.4-R0.1-SNAPSHOT + provided + + + org.projectlombok + lombok + 1.18.34 + provided + + + diff --git a/src/main/java/cn/forevermc/server/spigot/FMCServer.java b/src/main/java/cn/forevermc/server/spigot/FMCServer.java new file mode 100644 index 0000000..7d5f719 --- /dev/null +++ b/src/main/java/cn/forevermc/server/spigot/FMCServer.java @@ -0,0 +1,53 @@ +package cn.forevermc.server.spigot; + +import cn.forevermc.server.spigot.command.DebugCommand; +import cn.forevermc.server.spigot.event.DebugEvent; +import cn.forevermc.server.spigot.service.ReportService; +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.UUID; + +/** + * ForeverMC 服务器状态报告插件 + * + * @author 夜雨 + * @since 2022-11-14 11:05 + */ +public class FMCServer extends JavaPlugin { + + @Getter + private static FMCServer instance; + + private ReportService reportService; + + @Override + public void onLoad() { + instance = this; + } + + @Override + public void onEnable() { + saveDefaultConfig(); + + String reportId = getConfig().getString("report.id"); + if (StringUtils.isBlank(reportId)) { + getConfig().set("report.id", UUID.randomUUID().toString()); + saveConfig(); + } + if (getConfig().getBoolean("report.enable")) { + reportService = new ReportService(); + } + getCommand("debug").setExecutor(new DebugCommand()); + getServer().getPluginManager().registerEvents(new DebugEvent(), this); + } + + @Override + public void onDisable() { + saveConfig(); + if (reportService != null && reportService.isWorking()) { + reportService.cancel(); + } + } +} diff --git a/src/main/java/cn/forevermc/server/spigot/bean/ReportRequest.java b/src/main/java/cn/forevermc/server/spigot/bean/ReportRequest.java new file mode 100644 index 0000000..e365367 --- /dev/null +++ b/src/main/java/cn/forevermc/server/spigot/bean/ReportRequest.java @@ -0,0 +1,171 @@ +package cn.forevermc.server.spigot.bean; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 服务器状态 + * + * @author 夜雨 + * @since 2024-08-06 20:38 + */ +@Data +public class ReportRequest { + + private String id; + + /** true 为调试中 */ + private boolean debugging; + + /** TPS */ + private double tps; + + /** 报告时间 */ + private long reportAt; + + /** 静态基本信息 */ + private BaseInfo baseInfo; + + /** 主世界状态 */ + private List worldStatusList = new ArrayList<>(); + + /** JVM 状态 */ + private JVM jvm = new JVM(); + + /** 在线列表 */ + private List onlineList = new ArrayList<>(); + + /** + * 设置 TPS 取平均 + * + * @param tpsList TPS 列表 + */ + public void setTps(double[] tpsList) { + double sum = 0; + for (int i = 0; i < tpsList.length; i++) { + sum += tpsList[i]; + } + tps = sum / tpsList.length; + } + + /** + * 静态基本信息 + * + * @author 夜雨 + * @since 2025-01-19 10:25 + */ + @Data + public static class BaseInfo { + + /** 核心 */ + private String core; + + /** 启动时间 */ + private Long bootAt; + + /** 最大在线数量 */ + private Integer maxOnline; + + /** 图标 Base64 */ + private String icon; + + /** 游戏版本 */ + private String version; + } + + /** + * JVM 状态 + * + * @author 夜雨 + * @since 2022-11-11 14:52 + */ + @Data + public static class JVM { + + /** JVM 名称 */ + private String name; + + /** CPU 已使用 */ + private double cpuUsed; + + /** 内存状态 */ + private Memory memory = new Memory(); + + /** + * 内存状态 + * + * @author 夜雨 + * @since 2022-11-15 09:54 + */ + @Data + public static class Memory { + + /** 已使用 */ + private long used; + + /** 已申请 */ + private long committed; + + /** 最大内存 */ + private long max; + } + } + + /** + * 世界时间 + * + * @author 夜雨 + * @since 2024-10-29 23:59 + */ + @Data + public static class WorldStatus { + + /** 世界名称 */ + private String name; + + /** 总时刻 */ + private long ticks; + + /** 总天数 */ + private int day; + + /** 时 */ + private int hour; + + /** 分 */ + private int minute; + + /** true 为正在下雨 */ + private boolean isRaining; + + /** true 为正在雷雨 */ + private boolean isThundering; + + /** + * 设置自世界诞生后经历的总时刻,会同时计算详细时间 + * + * @param ticks 总时刻 + */ + public void setTicks(long ticks) { + this.ticks = ticks; + + // +6000 是因为 tick 为 0 时太阳从地平线开始升起,此时应该为早晨 6 点 + this.ticks += 6000; + + // mcTime | mcTick | realTime + // 1s | 0.27 | 0.0138s + // 1m | 16.6 | 0.83s + // 1h | 1000 | 50s + // 1d | 24000 | 20m + + day = (int) (this.ticks / 24000); + + int remainingTick = (int) (this.ticks % 24000); + + hour = remainingTick / 1000; + minute = (int) ((remainingTick - hour * 1000) / 16.6); + } + } +} diff --git a/src/main/java/cn/forevermc/server/spigot/bean/ServerStatusRequest.java b/src/main/java/cn/forevermc/server/spigot/bean/ServerStatusRequest.java new file mode 100644 index 0000000..3ceacc0 --- /dev/null +++ b/src/main/java/cn/forevermc/server/spigot/bean/ServerStatusRequest.java @@ -0,0 +1,94 @@ +package cn.forevermc.server.spigot.bean; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 服务器状态 + * + * @author 夜雨 + * @since 2024-08-06 20:38 + */ +@Data +public class ServerStatusRequest { + + private String id; + + private ServerStatus status; + + /** + * @author 夜雨 + * @since 2021-12-02 19:56 + */ + @Data + public static class ServerStatus { + + /** true 为维护中 */ + private boolean isDebugging; + + /** TPS */ + private double tps; + + /** 报告时间 */ + private long reportAt; + + /** JVM 状态 */ + private JVM jvm = new JVM(); + + /** 在线列表 */ + private List onlineList = new ArrayList<>(); + + /** + * 设置 TPS 取平均 + * + * @param tpsList TPS 列表 + */ + public void setTps(double[] tpsList) { + double sum = 0; + for (int i = 0; i < tpsList.length; i++) { + sum += tpsList[i]; + } + tps = sum / tpsList.length; + } + + /** + * JVM 状态 + * + * @author 夜雨 + * @since 2022-11-11 14:52 + */ + @Data + public static class JVM { + + /** CPU 已使用 */ + private double cpuUsed; + + /** 内存状态 */ + private Memory memory = new Memory(); + + /** + * 内存状态 + * + * @author 夜雨 + * @since 2022-11-15 09:54 + */ + @Data + public static class Memory { + + /** 已使用 */ + private long used; + + /** 已申请 */ + private long committed; + + /** 最大内存 */ + private long max; + + /** JVM 名称 */ + private String name; + } + } + } +} diff --git a/src/main/java/cn/forevermc/server/spigot/command/DebugCommand.java b/src/main/java/cn/forevermc/server/spigot/command/DebugCommand.java new file mode 100644 index 0000000..c4ea849 --- /dev/null +++ b/src/main/java/cn/forevermc/server/spigot/command/DebugCommand.java @@ -0,0 +1,174 @@ +package cn.forevermc.server.spigot.command; + +import cn.forevermc.server.spigot.FMCServer; +import org.apache.commons.lang.ObjectUtils; +import org.bukkit.World; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +/** + * 调试指令 + * + * @author 夜雨 + * @since 2024-08-15 10:51 + */ +public class DebugCommand implements CommandExecutor { + + private final Logger log; + private BukkitRunnable freezeTimeRunnable; + + private FMCServer fmcServer; + private FileConfiguration config; + + public DebugCommand() { + fmcServer = FMCServer.getInstance(); + config = fmcServer.getConfig(); + log = fmcServer.getLogger(); + + List worldList = fmcServer.getServer().getWorlds(); + Map worldsTickMap = new HashMap<>(); + ConfigurationSection worldsTickSection = config.getConfigurationSection("debug.worldsTick"); + if (worldsTickSection != null) { + for (String key : worldsTickSection.getKeys(false)) { + worldsTickMap.put(key, (Long) ObjectUtils.defaultIfNull(worldsTickSection.getLong(key), 0L)); + } + } + for (int i = 0; i < worldList.size(); i++) { + World world = worldList.get(i); + if (!worldsTickMap.containsKey(world.getName())) { + worldsTickMap.put(world.getName(), 0L); + } + } + config.set("debug.worldsTick", worldsTickMap); + fmcServer.saveConfig(); + } + + @Override + public boolean onCommand(CommandSender commandSender, Command command, String s, String[] strings) { + log.info(Arrays.toString(strings)); + return false; + } + +// public DebugCommand() { +// appendChildren("true", new ConsolePlayerCommand() { +// +// @Override +// protected void run(String[] strings) { +// enable(); +// success("已启用调试模式"); +// fmcServer.saveConfig(); +// fmcServer.reloadConfig(); +// } +// }); +// appendChildren("false", new ConsolePlayerCommand() { +// +// @Override +// protected void run(String[] strings) { +// disable(); +// success("已禁用调试模式"); +// fmcServer.saveConfig(); +// fmcServer.reloadConfig(); +// } +// }); +// appendChildren("status", new ConsolePlayerCommand() { +// +// @Override +// protected void run(String[] strings) { +// if (config.getDebug().isEnable()) { +// success("当前维护模式:启用"); +// } else { +// success("当前维护模式:禁用"); +// } +// } +// }); +// +// if (config.getDebug().isEnable()) { +// enable(); +// } else { +// disable(); +// } +// } + +// @Override +// public void run(String[] args) { +// boolean isEnable = !config.getReport().isEnable(); +// config.getDebug().setEnable(isEnable); +// +// success(String.format("已%s调式模式", isEnable ? "启用" : "禁用")); +// if (isEnable) { +// enable(); +// } else { +// disable(); +// } +// fmcServer.saveConfig(); +// fmcServer.reloadConfig(); +// } + + /** 启用 */ + private void enable() { +// config.getDebug().setEnable(true); + + List worldList = fmcServer.getServer().getWorlds(); + for (int i = 0; i < worldList.size(); i++) { + World world = worldList.get(i); + // 记录当前时刻 +// config.getDebug().getWorldsTick().put(world.getName(), world.getFullTime()); + // 正午 + world.setFullTime(6000); + // 禁用天气 + world.setStorm(false); + world.setThundering(false); + } + // 暂停时间 + if (freezeTimeRunnable != null) { + freezeTimeRunnable.cancel(); + } + freezeTimeRunnable = new BukkitRunnable() { + + @Override + public void run() { +// if (config.getDebug().isEnable()) { +// fmcServer.getServer().getWorlds().forEach(world -> world.setFullTime(6000)); +// } + } + }; + freezeTimeRunnable.runTaskTimer(fmcServer, 0L, 200L); + + // 踢出非维护人员 + Collection onlinePlayerList = fmcServer.getServer().getOnlinePlayers(); + for (Player player : onlinePlayerList) { + if (!player.isOp()) { + player.kickPlayer("服务器正在维护,请稍后重试"); + } + } + } + + /** 禁用 */ + private void disable() { + fmcServer.getConfig().set("debug.enable", false); + List worldList = fmcServer.getServer().getWorlds(); + for (int i = 0; i < worldList.size(); i++) { + World world = worldList.get(i); + // 启用天气 + world.setStorm(false); + world.setThundering(false); + } + // 还原时间 + if (freezeTimeRunnable != null) { + freezeTimeRunnable.cancel(); + } +// config.getDebug().getWorldsTick().forEach((worldName, fullTick) -> fmcServer.getServer().getWorld(worldName).setFullTime(fullTick.longValue())); + } +} diff --git a/src/main/java/cn/forevermc/server/spigot/command/StaffCommand.java b/src/main/java/cn/forevermc/server/spigot/command/StaffCommand.java new file mode 100644 index 0000000..8de2990 --- /dev/null +++ b/src/main/java/cn/forevermc/server/spigot/command/StaffCommand.java @@ -0,0 +1,99 @@ +package cn.forevermc.server.spigot.command; + +import cn.forevermc.server.spigot.FMCServer; +import cn.forevermc.server.spigot.util.Util; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; + +/** + * 维护人员操作 + * + * @author 夜雨 + * @since 2024-08-07 12:44 + */ +public class StaffCommand implements CommandExecutor { + + /** + * 指令动作 + * + * @author 夜雨 + * @since 2024-08-07 14:29 + */ + private enum Action { + + /** 添加 */ + ADD, + + /** 移除 */ + REMOVE, + + /** 列表 */ + LIST + } + + private final JavaPlugin plugin; + private final List actionList; + + public StaffCommand(JavaPlugin plugin) { + this.plugin = plugin; + + // 提示 + actionList = new ArrayList<>(); + for (Action action : Action.values()) { + actionList.add(action.toString().toLowerCase()); + } + plugin.getCommand("staff").setTabCompleter((commandSender, command, s, args) -> { + if (args.length == 1) { + return actionList; + } + return new ArrayList<>(); + }); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + FileConfiguration config = FMCServer.getConfiguration(); + Action action = Util.enumValueOf(Action.class, args[0]); + if (action == null) { + Util.returnMsg(sender, Level.WARNING, "使用方式: /staff [add|remove|list] "); + return true; + } + Set staffSet = new HashSet<>(config.getStringList("debug.staff")); + if (action == Action.LIST) { + Util.returnMsg(sender, Level.INFO, "维护人员列表: " + staffSet); + } else { + if (args.length < 2) { + Util.returnMsg(sender, Level.WARNING, "使用方式: /staff [add|remove|list] "); + return true; + } + String value = args[1]; + if (value.isEmpty()) { + Util.returnMsg(sender, Level.WARNING, "请输入维护人员 ID"); + return true; + } + switch (action) { + case ADD: + staffSet.add(value); + Util.returnMsg(sender, Level.INFO, String.format("已添加 %s 维护人员", value)); + break; + case REMOVE: + staffSet.remove(value); + Util.returnMsg(sender, Level.INFO, String.format("已移除 %s 维护人员", value)); + break; + } + config.set("debug.staff", staffSet.toArray(new String[0])); + plugin.saveConfig(); + plugin.reloadConfig(); + } + return true; + } +} diff --git a/src/main/java/cn/forevermc/server/spigot/event/DebugEvent.java b/src/main/java/cn/forevermc/server/spigot/event/DebugEvent.java new file mode 100644 index 0000000..84c2666 --- /dev/null +++ b/src/main/java/cn/forevermc/server/spigot/event/DebugEvent.java @@ -0,0 +1,53 @@ +package cn.forevermc.server.spigot.event; + +import cn.forevermc.server.spigot.FMCServer; +import org.bukkit.World; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.weather.WeatherChangeEvent; + +/** + * 调试事件 + * + * @author 夜雨 + * @since 2024-10-23 19:22 + */ +public class DebugEvent implements Listener { + + private final FileConfiguration config; + + public DebugEvent() { + FMCServer fmcServer = FMCServer.getInstance(); + config = fmcServer.getConfig(); + if (config.getBoolean("debug.enable")) { + for (World world : fmcServer.getServer().getWorlds()) { + world.setStorm(false); + world.setThundering(false); + } + } + } + + /** 保持晴天 */ + @EventHandler + public void onWeatherChange(WeatherChangeEvent event) { + if (!config.getBoolean("debug.enable")) { + return; + } + if (event.toWeatherState()) { + event.setCancelled(true); + } + } + + /** 踢出非维护人员 */ + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + if (!config.getBoolean("debug.enable")) { + return; + } + if (!event.getPlayer().isOp()) { + event.getPlayer().kickPlayer("服务器正在维护,请稍后重试"); + } + } +} diff --git a/src/main/java/cn/forevermc/server/spigot/service/ReportService.java b/src/main/java/cn/forevermc/server/spigot/service/ReportService.java new file mode 100644 index 0000000..7bc0747 --- /dev/null +++ b/src/main/java/cn/forevermc/server/spigot/service/ReportService.java @@ -0,0 +1,201 @@ +package cn.forevermc.server.spigot.service; + +import cn.forevermc.server.spigot.FMCServer; +import cn.forevermc.server.spigot.bean.ReportRequest; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.sun.management.OperatingSystemMXBean; +import lombok.Getter; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.fluent.Request; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.util.Timeout; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +import java.io.File; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 状态报告服务 + * + * @author 夜雨 + * @since 2022-11-11 14:38 + */ +public class ReportService extends BukkitRunnable { + + private final Gson gson; + private final Logger log; + private final FMCServer fmcServer; + private final FileConfiguration config; + + @Getter + private boolean isWorking = false; + + private final long bootAt; + private final Server server = Bukkit.getServer(); + private final ReportRequest reportReq = new ReportRequest(); + private final MemoryMXBean jvmMemory = ManagementFactory.getMemoryMXBean(); + private final double[] tpsList = new double[9]; + + private boolean requiredBaseInfo = false; + private final OperatingSystemMXBean operatingSystemMXBean; + + /** 失败时间,用于失败时任务执行周期将会更长 */ + private long failAt = -1; + + public ReportService() { + gson = new Gson(); + bootAt = System.currentTimeMillis(); + log = FMCServer.getInstance().getLogger(); + config = FMCServer.getInstance().getConfig(); + fmcServer = FMCServer.getInstance(); + operatingSystemMXBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class); + + try { + String api = config.getString("report.api"); + if (StringUtils.isBlank(api)) { + throw new RuntimeException("not found report.api in config"); + } + String token = config.getString("report.token"); + if (StringUtils.isBlank(token)) { + throw new RuntimeException("not found report.token in config"); + } + boolean isEnable = config.getBoolean("report.enable"); + String reportId = config.getString("report.id"); + + log.info("report: " + isEnable); + log.info("report.id: " + reportId); + log.info("worlds: " + server.getWorlds()); + if (!isEnable) { + return; + } + reportReq.setId(reportId); + // TPS 计算 + server.getScheduler().scheduleSyncRepeatingTask(fmcServer, new Runnable() { + + private int ticks; + private int tpsI; + private long cs; + private double tps; + + @Override + public void run() { + long now = System.nanoTime(); + + if (cs == 0) { + cs = now; + return; + } + + long diff = now - cs; + double currentTps = (double) TimeUnit.SECONDS.toNanos(1) / diff * ticks; + tpsI++; + tpsList[tpsI % tpsList.length] = tps = (tps == 0) ? currentTps : (tps * 0.9 + currentTps * 0.1); + + cs = now; + ticks = 1; + } + }, 0, 1); + + runTaskTimerAsynchronously(fmcServer, 0, 60); + isWorking = true; + } catch (Exception e) { + log.log(Level.WARNING, e.getMessage()); + } + } + + /** 报告状态 */ + @Override + public void run() { + // 失败时延长周期,(15+-3) 秒 + if (System.currentTimeMillis() - failAt < 1E3 * 15) { + return; + } + + try { + reportReq.getOnlineList().clear(); + Collection players = server.getOnlinePlayers(); + for (Player player : players) { + reportReq.getOnlineList().add(player.getName()); + } + + reportReq.getWorldStatusList().clear(); + List worldList = config.getStringList("report.worlds"); + if (worldList != null) { + for (int i = 0; i < worldList.size(); i++) { + World world = fmcServer.getServer().getWorld(worldList.get(i)); + ReportRequest.WorldStatus worldStatus = new ReportRequest.WorldStatus(); + worldStatus.setName(world.getName()); + worldStatus.setTicks(world.getFullTime()); + worldStatus.setRaining(world.hasStorm()); + worldStatus.setRaining(world.isThundering()); + reportReq.getWorldStatusList().add(worldStatus); + } + } + reportReq.setTps(tpsList); + reportReq.setDebugging(config.getBoolean("debug.enable")); + reportReq.setReportAt(System.currentTimeMillis()); + reportReq.getJvm().setCpuUsed(operatingSystemMXBean.getProcessCpuLoad()); + reportReq.getJvm().setName(System.getProperty("java.vm.name")); + reportReq.getJvm().getMemory().setUsed(jvmMemory.getHeapMemoryUsage().getUsed()); + reportReq.getJvm().getMemory().setCommitted(jvmMemory.getHeapMemoryUsage().getCommitted()); + reportReq.getJvm().getMemory().setMax(jvmMemory.getHeapMemoryUsage().getMax()); + + if (requiredBaseInfo) { + ReportRequest.BaseInfo baseInfo = new ReportRequest.BaseInfo(); + int end = server.getVersion().indexOf("MC:"); + if (end == -1) { + baseInfo.setCore(server.getVersion()); + } else { + baseInfo.setCore(server.getVersion().substring(0, end - 2)); + } + baseInfo.setBootAt(bootAt); + baseInfo.setMaxOnline(server.getMaxPlayers()); + File iconFile = new File(new File(fmcServer.getDataFolder().getParent()).getParent(), "server-icon.png"); + if (iconFile.exists()) { + baseInfo.setIcon(Base64.encodeBase64String(FileUtils.readFileToByteArray(iconFile))); + } + baseInfo.setVersion(server.getBukkitVersion().split("-")[0]); // 1.9.4-R0.1-SNAPSHOT + reportReq.setBaseInfo(baseInfo); + requiredBaseInfo = false; + } else { + reportReq.setBaseInfo(null); + } + + String respText = Request.post(config.getString("report.api")) + .addHeader("Token", config.getString("report.token")) + .bodyString(gson.toJson(reportReq), ContentType.APPLICATION_JSON) + .connectTimeout(Timeout.ofSeconds(60)) + .execute() + .returnContent() + .asString(); + + JsonObject resp = new JsonParser().parse(respText).getAsJsonObject(); + + int code = resp.get("code").getAsInt(); + if (code < 40000) { + // 被服务端忽略时视为需要基本信息 + requiredBaseInfo = code == 20001; + } else { + log.log(Level.WARNING, String.format("report api response error:%s", resp.get("msg").getAsString())); + } + } catch (Exception e) { + failAt = System.currentTimeMillis(); + log.log(Level.WARNING, String.format("report error:%s", e.getMessage())); + } + } +} diff --git a/src/main/java/cn/forevermc/server/spigot/util/HTTP.java b/src/main/java/cn/forevermc/server/spigot/util/HTTP.java new file mode 100644 index 0000000..06a8a49 --- /dev/null +++ b/src/main/java/cn/forevermc/server/spigot/util/HTTP.java @@ -0,0 +1,252 @@ +package cn.forevermc.server.spigot.util; + +import cn.forevermc.server.spigot.FMCServer; +import lombok.Data; +import lombok.Getter; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; + +/** + * HTTP 操作 + * + * @author 夜雨 + * @since 2024-08-06 14:35 + */ +public class HTTP { + + /** + * 请求类型 + * + * @author 夜雨 + * @version 2022-01-02 12:16 + */ + @Getter + public enum ContentType { + + /** 默认 URL 请求 */ + DEFAULT(null), + + /** FORM 表单模拟 */ + FORM("application/x-www-form-urlencoded"), + + /** JSON */ + JSON("application/json; charset=utf-8"), + + /** 数据流 */ + STREAM("multipart/form-data; charset=utf-8; boundary=B7018kpqFPpgrWAKIR0lHNAanJEqJEyz"); + + final String value; + + ContentType(String value) { + this.value = value; + } + + } + + /** + * 发送 POST 请求 + * + * @param url 请求地址 + * @param type 数据体类型 + * @param data 数据 + * @return 返回结果 + * @throws Exception 请求异常 + */ + public static String post(String url, ContentType type, String data) throws Exception { + HttpURLConnection connection = getConnection(url, type); + connection.setRequestMethod("POST"); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setUseCaches(false); + connection.setInstanceFollowRedirects(true); + if (data != null && !data.isEmpty()) { + BufferedOutputStream bos = new BufferedOutputStream(connection.getOutputStream()); + bos.write(data.getBytes(StandardCharsets.UTF_8)); + bos.flush(); + bos.close(); + } + + String result = ""; + if (HttpURLConnection.HTTP_OK == connection.getResponseCode()) { + result = toString(connection.getInputStream()); + } + connection.disconnect(); + return result; + } + + public static String post(String url, Map params, List postFileList) throws Exception { + final String newLine = "\r\n"; + final String paramHeader = "Content-Disposition: form-data; name=\"%s\""; + final String fileHeader = "Content-Disposition: form-data; name=\"%s\"; filename=\"%s\""; + final String split = ContentType.STREAM.getValue().split("boundary=")[1]; + + HttpURLConnection connection = getConnection(url, ContentType.STREAM); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setUseCaches(false); + connection.setRequestMethod("POST"); + connection.setChunkedStreamingMode(4096); + + DataOutputStream dos = new DataOutputStream(connection.getOutputStream()); + { + // 参数 + for (Map.Entry param : params.entrySet()) { + dos.writeBytes("--" + split); + dos.writeBytes(newLine); + // 参数协议 + dos.writeBytes(String.format(paramHeader, param.getKey())); + dos.writeBytes(newLine); + dos.writeBytes("Content-Type: text/plain; charset=UTF-8"); + dos.writeBytes(newLine); + dos.writeBytes("Content-Transfer-Encoding: 8bit"); + dos.writeBytes(newLine); + dos.writeBytes(newLine); + Object value = param.getValue(); + if (value != null) { + dos.write(value.toString().getBytes(StandardCharsets.UTF_8)); + } + dos.writeBytes(newLine); + } + // 文件 + for (PostFile postFile : postFileList) { + ByteArrayInputStream bais = new ByteArrayInputStream(postFile.getBytes()); + dos.writeBytes("--" + split); + dos.writeBytes(newLine); + dos.writeBytes(String.format(fileHeader, postFile.getField(), URLEncoder.encode(postFile.getFileName(), "UTF-8"))); + dos.writeBytes(newLine); + dos.writeBytes("Content-Type: application/octet-stream"); + dos.writeBytes(newLine); + dos.writeBytes(newLine); + + byte[] buffer = new byte[4096]; + int l; + while ((l = bais.read(buffer)) != -1) { + dos.write(buffer, 0, l); + } + // 协议结尾 + dos.writeBytes(newLine); + dos.writeBytes("--" + split + "--"); + dos.writeBytes(newLine); + } + // 协议结尾 + dos.writeBytes(newLine); + dos.writeBytes("--" + split + "--"); + dos.writeBytes(newLine); + dos.flush(); + } + // 返回 + if (connection.getResponseCode() >= 300) { + throw new Exception("HTTP Request error, Response code: " + connection.getResponseCode()); + } + String result = toString(connection.getInputStream()); + connection.disconnect(); + return result; + } + + /** + * 获取连接 + * + * @param url URL 地址 + * @param contentType Content-ContentType 数据类型 + * @return HTTP 连接 + */ + private static HttpURLConnection getConnection(String url, ContentType contentType) throws IOException, NoSuchAlgorithmException, KeyManagementException, URISyntaxException { + // 转 URL 对象 + url = url.startsWith("http") ? url : "http://" + url; + URL connectURL = new URI(url).toURL(); + + HttpURLConnection connection = (HttpURLConnection) connectURL.openConnection(); + // SSL 连接 + if (url.trim().startsWith("https")) { + SSLContext sslcontext = SSLContext.getInstance("TLS"); + sslcontext.init(null, new TrustManager[] {new X509TrustManager() { + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) { + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) { + } + }}, null); + if (connection instanceof HttpsURLConnection) { + ((HttpsURLConnection) connection).setSSLSocketFactory(sslcontext.getSocketFactory()); + } + } + // 数据类型 + if (contentType != null && contentType.getValue() != null) { + connection.setRequestProperty("Content-Type", contentType.getValue()); + } + connection.setRequestProperty("Token", FMCServer.getConfiguration().getString("report.token")); + // 超时 + connection.setReadTimeout(3000); + connection.setConnectTimeout(3000); + return connection; + } + + /** + * 读取数据流为字符串 + * + * @param is 输入流 + * @return 字符串 + * @throws IOException IO 异常 + */ + private static String toString(InputStream is) throws IOException { + StringBuilder sb = new StringBuilder(); + BufferedInputStream bis = new BufferedInputStream(is); + + InputStreamReader isr = new InputStreamReader(bis, StandardCharsets.UTF_8); + + char[] buffer = new char[4096]; + int l; + while ((l = isr.read(buffer)) != -1) { + sb.append(buffer, 0, l); + } + isr.close(); + bis.close(); + is.close(); + return sb.toString(); + } + + /** + * 发送文件 + * + * @author 夜雨 + * @since 2024-08-07 23:50 + */ + @Data + public static class PostFile { + + /** 字段名 */ + private String field; + + /** 文件名 */ + private String fileName; + + /** 字节数据 */ + private byte[] bytes; + } +} diff --git a/src/main/java/cn/forevermc/server/spigot/util/Util.java b/src/main/java/cn/forevermc/server/spigot/util/Util.java new file mode 100644 index 0000000..43fa1d5 --- /dev/null +++ b/src/main/java/cn/forevermc/server/spigot/util/Util.java @@ -0,0 +1,75 @@ +package cn.forevermc.server.spigot.util; + +import cn.forevermc.server.spigot.FMCServer; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.security.SecureRandom; +import java.util.logging.Level; + +/** + * 工具 + * + * @author 夜雨 + * @since 2024-08-06 20:40 + */ +public class Util { + + /** + * 空安全的字符串转枚举 + * + * @param enumType 枚举类 + * @param name 枚举字段字符串 + * @return 枚举 + * @param 枚举类型 + */ + public static > T enumValueOf(Class enumType, String name) { + if (name == null) { + return null; + } + T[] ts = enumType.getEnumConstants(); + if (ts == null) { + throw new IllegalArgumentException(enumType.getName() + " is not an enum type"); + } + for (int i = 0; i < ts.length; i++) { + if (ts[i].name().equalsIgnoreCase(name)) { + return ts[i]; + } + } + return null; + } + + /** + * 输出指令结果,如果是控制台则输出控制台,如果是玩家则输出到玩家 + * + * @param sender 发送者 + * @param level 等级 + * @param msg 消息 + */ + public static void returnMsg(CommandSender sender, Level level, String msg) { + if (sender instanceof Player) { + Player player = (Player) sender; + ChatColor color = level == Level.WARNING ? ChatColor.GREEN : ChatColor.RED; + player.sendMessage(color + msg); + } else { + FMCServer.log(level, msg); + } + } + + /** + * 随机字符串 + * + * @param length 长度 + * @return 随机字符串 + */ + public static String randomString(int length) { + String pool = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + SecureRandom r = new SecureRandom(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + sb.append(pool.charAt(r.nextInt(pool.length() - 1))); + } + return sb.toString(); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..22e21b4 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,10 @@ +report: + enable: false + id: "" + api: http://localhost:8091/fmc/server/report + token: qweqwe123 + worlds: + - world +debug: + enable: false + worldsTick: diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..414d9af --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,12 @@ +name: FMCServer +main: cn.forevermc.server.spigot.FMCServer +version: '0.0.2' + +author: Timi +website: forevermc.cn +description: This plugin is ForeverMC Server status reporter + +commands: + debug: + description: Debug Command + usage: "/ [true|false|status]"