1
0
Fork 0
forked from mc/VTools

split package name and re-write server closer

This commit is contained in:
Sodium-Aluminate 2023-12-21 15:58:21 +08:00
parent 1a3c8872d1
commit cba3248484
6 changed files with 317 additions and 230 deletions

View file

@ -1,8 +1,9 @@
package de.strifel.VTools;
package com.alpt.vtools.listeners;
import com.google.gson.Gson;
import com.sun.net.httpserver.HttpServer;
import com.velocitypowered.api.proxy.Player;
import de.strifel.VTools.VTools;
import org.jetbrains.annotations.TestOnly;
import java.io.IOException;
@ -13,18 +14,26 @@ import java.util.Collection;
public class OnlinePlayerQueryService {
public static OnlinePlayerQueryService INSTANCE;
private final VTools plugin;
public OnlinePlayerQueryService(VTools plugin) {
private OnlinePlayerQueryService(VTools plugin) {
this.plugin = plugin;
}
private static final Gson gson = new Gson();
public void register() {
public static OnlinePlayerQueryService createInstance(VTools plugin) {
if (INSTANCE != null) return INSTANCE;
INSTANCE = new OnlinePlayerQueryService(plugin);
INSTANCE.register();
return INSTANCE;
}
private void register() {
int port = Integer.parseInt(plugin.getConfigOrDefault("http_service_port", "17611"));
try {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 17611), 0);
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 0);
server.createContext("/api/getOnlinePlayers", exchange -> {
Collection<Player> players = plugin.getServer().getAllPlayers();

View file

@ -0,0 +1,223 @@
package com.alpt.vtools.listeners;
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.ProxyServer;
import de.strifel.VTools.VTools;
import de.strifel.VTools.listeners.TGBridge;
import okhttp3.*;
import java.io.IOException;
import java.util.concurrent.LinkedBlockingQueue;
public class ServerCloser {
private static final long MINUTE = 60L * 1000;
private static final OkHttpClient CLIENT = new OkHttpClient();
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 final LinkedBlockingQueue<Counter> lock = new LinkedBlockingQueue<>();
public static ServerCloser INSTANCE;
private 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(plugin.getConfigOrDefault("azure_api_url", "https://example.com/"))
.post(requestBody)
.addHeader("Content-Type", "application/json")
.build();
try (Response response = CLIENT.newCall(request).execute()) {
String body = response.body() == null ? "null" : response.body().string();
plugin.logger.info("ServerCloser: http request response: {} ({}).", body, response.code());
return (response.code() >= 200 && response.code() < 300);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
private void close() {
if (!INSTANCE.server.getAllPlayers().isEmpty()) {
plugin.logger.error("ServerCloser: 定时器到点时发现服务器有人。这不应发生,因为定时器本应该被打断。");
TGBridge.error("ServerCloser: #bug @NaAlOH4 定时器到点时发现服务器有人。这不应发生,因为定时器本应该被打断。");
}
boolean httpSuccess = false;
for (int i = 0; i < 3; i++) {
httpSuccess = executeAzure();
if (httpSuccess) {
TGBridge.log("ServerCloser: 向azure发送关机命令成功正在关闭 pymcd.");
TGBridge.setShuttingDown(0);
break;
}
}
if (!httpSuccess) {
TGBridge.error("服务器关机 http 请求失效,服务器可能没有正常关闭。");
} else {
try {
Runtime.getRuntime().exec(new String[]{"pkill", "pymcd"});
} catch (IOException e) {
TGBridge.error("关闭 pymcd 时出现问题:" + e.getMessage());
}
}
}
private boolean firstInit;
public static ServerCloser createInstance(VTools plugin) {
if (INSTANCE != null) return INSTANCE;
INSTANCE = new ServerCloser(plugin);
INSTANCE.server.getEventManager().register(plugin, INSTANCE);
INSTANCE.firstInit = true;
INSTANCE.update();
return INSTANCE;
}
@SuppressWarnings("java:S106")
@Subscribe
public void onDisconnect(DisconnectEvent event) {
update();
}
@Subscribe
public void onServerConnected(ServerConnectedEvent event) {
update();
}
private void update() {
if (server.getAllPlayers().isEmpty()) {
initCountdown();
return;
}
boolean canceledSomething = false;
synchronized (lock) {
while (!lock.isEmpty()) {
lock.poll().cancel();
canceledSomething = true;
}
}
if (canceledSomething) {
plugin.logger.info("ServerCloser: 有玩家在线,干掉任何可能的关机计时器。");
}
TGBridge.setShuttingDown(-1);
}
private void initCountdown() {
startCountdown(firstInit);
firstInit = false;
}
private void startCountdown(boolean isFirstInit) {
synchronized (lock) {
while (!lock.isEmpty()) {
lock.poll().cancel();
}
lock.add(new Counter(INSTANCE, isFirstInit ? 60 : 15).start());
}
}
public void fastShutdown() {
synchronized (lock) {
while (!lock.isEmpty()) {
lock.poll().cancel();
}
lock.add(new Counter(INSTANCE, 1).start());
}
}
public void slowShutdown() {
synchronized (lock) {
while (!lock.isEmpty()) {
lock.poll().cancel();
}
lock.add(new Counter(INSTANCE, 60).start());
}
}
private static class Counter {
private final ServerCloser instance;
private final int totalMin;
private int minLeft;
private boolean canceled = false;
protected Counter(ServerCloser instance, int minute) {
this.instance = instance;
totalMin = minute;
minLeft = minute;
}
protected synchronized void cancel() {
canceled = true;
this.notifyAll();
}
private boolean started = false;
private final Object startLock = new Object();
protected Counter start() {
synchronized (startLock) {
if (started) return this;
started = true;
}
new Thread(this::run).start();
return this;
}
private synchronized void run() {
while (true) {
if (canceled) return;
TGBridge.setShuttingDown(minLeft);
try {
this.wait(MINUTE);
} catch (InterruptedException ignored) {}
if (canceled) return;
if (!instance.server.getAllPlayers().isEmpty()) {
instance.plugin.logger.error("ServerCloser: 定时器发现服务器有人。这不应发生,因为定时器本应该被直接打断。");
TGBridge.error("ServerCloser: #bug @NaAlOH4 定时器发现服务器有人。这不应发生,因为定时器本应该被直接打断。");
canceled = true;
}
if (canceled) return;
minLeft--;
switch (minLeft) {
case 1 -> {
String msg = "服务器即将在一分钟后关机";
instance.plugin.logger.info(msg);
TGBridge.log(msg);
}
case 0 -> {
String msg = "ServerCloser: 距离上一个玩家离开已经过了 %s 分钟,即将关机。".formatted(totalMin);
instance.plugin.logger.info(msg);
TGBridge.log(msg);
instance.close();
canceled = true;
}
case -1 -> {
instance.plugin.logger.error("ServerCloser: 定时器写炸了。");
TGBridge.error("ServerCloser: #bug @NaAlOH4 定时器写炸了。");
canceled = true;
}
default -> instance.plugin.logger.info("服务器即将在 {} 分钟后关机", minLeft);
}
}
}
}
}

View file

@ -1,4 +1,4 @@
package de.strifel.VTools.listeners;
package com.alpt.vtools.utils;
import com.pengrad.telegrambot.model.Message;
import com.pengrad.telegrambot.model.MessageEntity;

View file

@ -1,191 +0,0 @@
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.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.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 long MINUTE = 60L * 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) {
TGBridge.log("ServerCloser: 向azure发送关机命令成功正在关闭 pymcd.");
TGBridge.setShuttingDown(0);
break;
}
}
if (!httpSuccess) {
TGBridge.error("服务器关机 http 请求失效,服务器可能没有正常关闭。");
} else {
try {
Runtime.getRuntime().exec("pkill pymcd");
} catch (IOException e) {
TGBridge.error("关闭 pymcd 时出现问题:" + e.getMessage());
}
}
}
private boolean firstInit;
private int countDown = -1;
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<String, String> 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() {
var hasPlayer = !server.getAllPlayers().isEmpty();
if (hasPlayer) {
plugin.logger.info("ServerCloser: 有玩家在线,干掉任何可能的关机计时器。");
closeServerTimer.cancel();
countDown = -1;
TGBridge.setShuttingDown(-1);
return;
}
initCountdown();
}
private void initCountdown() {
countDown = firstInit ? 60 : 15;
firstInit = false;
startCountdown();
}
private void startCountdown() {
TGBridge.setShuttingDown(countDown);
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
@SuppressWarnings("java:S1199")
public void run() {
{
countDown--;
if (countDown < 0) {
TGBridge.error("ServerCloser: #bug @NaAlOH4 计时器写炸了");
closeServerTimer.cancel();
return;
}
if (server.getAllPlayers().isEmpty()) {
switch (countDown) {
case 1 -> {
TGBridge.log("服务器即将在一分钟后关机");
startCountdown();
}
case 0 -> {
plugin.logger.info("ServerCloser: 距离上一个玩家离开已经过了%s即将关机。".formatted(firstInit ? "一小时" : "15分钟"));
TGBridge.log("ServerCloser: 距离上一个玩家离开已经过了%s即将关机。".formatted(firstInit ? "一小时" : "15分钟"));
close();
closeServerTimer.cancel();
}
default -> startCountdown();
}
} else {
plugin.logger.error("ServerCloser: 定时器到点时发现服务器有人。这不应发生,因为定时器本应该被打断。");
TGBridge.error("ServerCloser: #bug @NaAlOH4 定时器到点时发现服务器有人。这不应发生,因为定时器本应该被打断。");
closeServerTimer.cancel();
}
}
}
}, MINUTE);
plugin.logger.info("ServerCloser: 将在 {} 分钟后自动关机。", countDown);
synchronized (lock) {
closeServerTimer.cancel();
closeServerTimer = timer;
}
}
}

View file

@ -1,5 +1,7 @@
package de.strifel.VTools;
import com.alpt.vtools.listeners.OnlinePlayerQueryService;
import com.alpt.vtools.listeners.ServerCloser;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.plugin.Plugin;
@ -9,11 +11,17 @@ import de.strifel.VTools.commands.*;
import de.strifel.VTools.listeners.*;
import net.kyori.adventure.text.format.TextColor;
import org.slf4j.Logger;
import org.yaml.snakeyaml.Yaml;
import javax.inject.Inject;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
@Plugin(id = "vtools", name="VTools", version="1.0-SNAPSHOT", description="Some commands!", authors="unnamed")
@Plugin(id = "vtools", name = "VTools", version = "1.0-SNAPSHOT", description = "Some commands!", authors = "unnamed")
public class VTools {
private final ProxyServer server;
public final Logger logger;
@ -42,11 +50,46 @@ public class VTools {
server.getCommandManager().register("tps", new CommandTp(server), "jump");
server.getCommandManager().register("server", new CommandServer(server), "serverv");
server.getCommandManager().register("servers", new CommandServers(server), "allservers");
loadConfig();
new TGBridge(this).register();
new PlayerStatus(this).register();
new GlobalChat(this).register();
new ServerCloser(this).register();
new OnlinePlayerQueryService(this).register();
ServerCloser.createInstance(this);
OnlinePlayerQueryService.createInstance(this);
}
private Map<String, String> config = new HashMap<>();
public String getConfig(String k) {
return config.get(k);
}
public String getConfigOrDefault(String k, String v) {
return config.getOrDefault(k, v);
}
private void loadConfig() {
try {
File configDir = dataDirectory.toFile();
if (!configDir.exists()) {
configDir.mkdir();
}
File configFile = new File(configDir, "config.yaml");
if (!configFile.exists()) {
String defVal = """
chat_id: "0"
token: ""
azure_api_url: "https://example.com/"
http_service_port: 17611
""";
Files.write(Path.of(configFile.toURI()), defVal.getBytes(StandardCharsets.UTF_8));
}
String configStr = Files.readString(Path.of(configFile.toURI()), StandardCharsets.UTF_8);
Yaml yaml = new Yaml();
config = yaml.load(configStr);
} catch (Exception e) {
logger.error("parsing config", e);
}
}
public ProxyServer getServer() {

View file

@ -1,5 +1,7 @@
package de.strifel.VTools.listeners;
import com.alpt.vtools.listeners.ServerCloser;
import com.alpt.vtools.utils.MarkdownString;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.pengrad.telegrambot.Callback;
@ -27,13 +29,8 @@ import de.strifel.VTools.VTools;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.yaml.snakeyaml.Yaml;
import java.io.IOException;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.BiConsumer;
@ -77,24 +74,9 @@ public class TGBridge {
}
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<String, String> config = yaml.load(configStr);
synchronized (this) {
this.CHAT_ID = Long.parseLong(config.getOrDefault("chat_id", "0"));
this.TOKEN = config.getOrDefault("token", "");
}
} catch (Exception e) {
plugin.logger.error("parsing config", e);
CHAT_ID = Long.parseLong(plugin.getConfigOrDefault("chat_id", "0"));
TOKEN = plugin.getConfigOrDefault("token", "");
}
}
@ -138,9 +120,10 @@ public class TGBridge {
if (message.text() != null && !message.text().isEmpty()) {
String msgText = message.text();
if (msgText.startsWith("/")) {
String[] s = msgText.split("(@[A-Za-z0-9_]bot)?[\t \n]+", 2);
String[] s = msgText.split("((@\\w+bot)|(@\\w+bot)?[\t \n]+)", 2);
String command = s[0];
@Nullable String arg = s.length == 2 ? s[1] : null;
System.out.println(command);
switch (command) {
case "/list" -> outbound(genOnlineStatus(), ParseMode.MarkdownV2);
case "/setpin" -> {
@ -188,6 +171,22 @@ public class TGBridge {
}
}, ParseMode.MarkdownV2
);
case "/shutdown" -> {
if (server.getAllPlayers().isEmpty()) {
ServerCloser.INSTANCE.fastShutdown();
outbound("server will shutdown in 1 minute.\nuse /fuck to cancel.");
} else {
outbound("still player online, can't shutdown.");
}
}
case "/fuck" -> {
if (server.getAllPlayers().isEmpty()) {
ServerCloser.INSTANCE.slowShutdown();
outbound("shutdown timer has been set to 60 minutes.");
} else {
outbound("still player online, can't shutdown.");
}
}
default -> {}
}
}
@ -307,7 +306,7 @@ public class TGBridge {
}
String result = String.join("\n", out);
if (shutdownCountMinutes < 0) return result;
if (shutdownCountMinutes == 0) return "server is shutdown\\.%n%s".formatted(result);
if (shutdownCountMinutes == 0 || PROXY_SHUT_DOWN) return "server already shutdown\\.%n%s".formatted(result);
return "server will shutdown after %s minute\\.%n%s".formatted(shutdownCountMinutes, result);
}
@ -431,14 +430,14 @@ public class TGBridge {
} catch (InterruptedException ignored) {}
if (oldestRequest == null) {
plugin.logger.warn("updateRequests.take() return a null value, why?");
sleep(1000);
sleep(10000);
continue;
}
updateRequests.clear();
if (!updateOnlineStatus()) {
updateRequests.add(oldestRequest); // 更新失败 回去吧您内
sleep(1000);
sleep(10000);
}
}
}).start();
@ -479,7 +478,8 @@ public class TGBridge {
}
protected boolean setOnlineStatusNotAvailable() {
return editOnlineStatusMessage("proxy already shutdown");
return updateOnlineStatus();
// return editOnlineStatusMessage("proxy already shutdown");
}
private static final Gson prettyGson = new GsonBuilder().setPrettyPrinting().create();
@ -490,8 +490,11 @@ public class TGBridge {
}
BaseResponse response;
try {
response = bot.execute(new EditMessageText(CHAT_ID, ONLINE_STATUS_MESSAGE_ID, markdownText).parseMode(ParseMode.MarkdownV2));
response = bot.execute(new EditMessageText(CHAT_ID, ONLINE_STATUS_MESSAGE_ID, markdownText).parseMode(ParseMode.MarkdownV2).disableWebPagePreview(true));
if (!response.isOk()) {
if (response.description().equals("Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message")) {
return true;
}
String responseJSON = prettyGson.toJson(response);
plugin.logger.warn("update failed: {}", responseJSON);
}
@ -526,7 +529,7 @@ public class TGBridge {
protected JoinLeftAnnounceMessage(String firstMessage) {
text = new StringBuilder(firstMessage);
time = System.currentTimeMillis();
timeMinute = time/MINUTE;
timeMinute = time / MINUTE;
sendAnnounceMessage();
}