diff --git a/src/main/java/de/strifel/VTools/ServerCloser.java b/src/main/java/de/strifel/VTools/ServerCloser.java new file mode 100644 index 0000000..cbc2be0 --- /dev/null +++ b/src/main/java/de/strifel/VTools/ServerCloser.java @@ -0,0 +1,154 @@ +package de.strifel.VTools; + +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.player.ServerConnectedEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; +import de.strifel.VTools.listeners.TGBridge; +import okhttp3.*; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.Collection; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +public class ServerCloser { + private static final long INACTIVITY_TIMEOUT_MILLISECOND = 15L * 60 * 1000; // 15 minutes + private static final OkHttpClient CLIENT = new OkHttpClient(); + private String apiUrl; + + private static final String ACTION_STOP_JSON = "{\"action\":\"stop\"}"; + @SuppressWarnings("java:S1068") + private static final String ACTION_START_JSON = "{\"action\":\"start\"}"; + private final VTools plugin; + private final ProxyServer server; + + private Timer closeServerTimer = new Timer(); + + private final Object lock = new Object(); + + public ServerCloser(VTools plugin) { + this.plugin = plugin; + this.server = plugin.getServer(); + } + + private boolean executeAzure() { + return executeAzure(ACTION_STOP_JSON); + } + + private boolean executeAzure(String action) { + RequestBody requestBody = RequestBody.create(action, MediaType.parse("application/json")); + Request request = new Request.Builder() + .url(apiUrl) + .post(requestBody) + .addHeader("Content-Type", "application/json") + .build(); + + try { + Response response = CLIENT.newCall(request).execute(); + plugin.logger.info("ServerCloser: http request response: {} ({}).", response.body().string(), response.code()); + return (response.code() >= 200 && response.code() < 300); + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + + private void close() { + boolean httpSuccess = false; + for (int i = 0; i < 3; i++) { + httpSuccess = executeAzure(); + if (httpSuccess) break; + } + if (!httpSuccess) { + TGBridge.error("服务器关机 http 请求失效,服务器可能没有正常关闭。"); + } else { + try { + Runtime.getRuntime().exec("pkill pymcd"); + } catch (IOException ignored) {} +// plugin.getServer().shutdown(); + } + } + + private boolean firstInit; + + public void register() { + server.getEventManager().register(plugin, this); + firstInit = true; + update(); + loadConfig(); + } + + + private void loadConfig() { + try { + File configDir = plugin.dataDirectory.toFile(); + if (!configDir.exists()) { + configDir.mkdir(); + } + File configFile = new File(configDir, "config.yaml"); + if (!configFile.exists()) { + Files.write(Path.of(configFile.toURI()), "chat_id: \"0\"\ntoken: \"\"\n".getBytes(StandardCharsets.UTF_8)); + } + String configStr = Files.readString(Path.of(configFile.toURI()), StandardCharsets.UTF_8); + Yaml yaml = new Yaml(); + Map config = yaml.load(configStr); + apiUrl = config.getOrDefault("azure_api_url", "https://example.com/"); + } catch (Exception e) { + plugin.logger.error("parsing config", e); + } + } + + @SuppressWarnings("java:S106") + @Subscribe + public void onDisconnect(DisconnectEvent event) { + update(); + } + + @Subscribe + public void onServerConnected(ServerConnectedEvent event) { + update(); + } + + private void update() { + Collection players = server.getAllPlayers(); + + if (!players.isEmpty()) { + plugin.logger.info("ServerCloser: 有玩家在线,干掉任何可能的关机计时器。"); + closeServerTimer.cancel(); + return; + } + Timer timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + if (server.getAllPlayers().isEmpty()) { + plugin.logger.info("ServerCloser: 即将关机。"); + close(); + } else { + plugin.logger.error("ServerCloser: 定时器到点时发现服务器有人。这不应发生,因为定时器本应该被打断。"); + TGBridge.error("ServerCloser: #bug @NaAlOH4 定时器到点时发现服务器有人。这不应发生,因为定时器本应该被打断。"); + } + closeServerTimer.cancel(); + } + }, firstInit ? (4 * INACTIVITY_TIMEOUT_MILLISECOND) : INACTIVITY_TIMEOUT_MILLISECOND); + plugin.logger.info("ServerCloser: 将在 {} 后自动关机。", firstInit ? "1h" : "15min"); + + firstInit = false; + + synchronized (lock) { + closeServerTimer.cancel(); + closeServerTimer = timer; + } + } + +} diff --git a/src/main/java/de/strifel/VTools/VTools.java b/src/main/java/de/strifel/VTools/VTools.java index 5b84099..eaf1296 100644 --- a/src/main/java/de/strifel/VTools/VTools.java +++ b/src/main/java/de/strifel/VTools/VTools.java @@ -45,6 +45,7 @@ public class VTools { new TGBridge(this).register(); new PlayerStatus(this).register(); new GlobalChat(this).register(); + new ServerCloser(this).register(); } public ProxyServer getServer() { diff --git a/src/main/java/de/strifel/VTools/listeners/TGBridge.java b/src/main/java/de/strifel/VTools/listeners/TGBridge.java index 67bde90..263a9ca 100644 --- a/src/main/java/de/strifel/VTools/listeners/TGBridge.java +++ b/src/main/java/de/strifel/VTools/listeners/TGBridge.java @@ -273,6 +273,10 @@ public class TGBridge { }, parseMode); } + public static void error(String context) { + INSTANCE.outbound("*" + MarkdownString.escapeStr(context) + "*", ParseMode.MarkdownV2); + } + protected void outbound(String content) {outbound(content, (ParseMode) null);}