588 lines
25 KiB
Java
588 lines
25 KiB
Java
package com.example.playertime;
|
|
|
|
import com.google.gson.*;
|
|
import com.mojang.authlib.GameProfile;
|
|
import com.sun.net.httpserver.HttpServer;
|
|
import com.sun.net.httpserver.HttpHandler;
|
|
import com.sun.net.httpserver.HttpExchange;
|
|
import net.fabricmc.loader.api.FabricLoader;
|
|
import net.minecraft.server.MinecraftServer;
|
|
import net.minecraft.server.PlayerManager;
|
|
import net.minecraft.server.network.ServerPlayerEntity;
|
|
|
|
import java.io.*;
|
|
import java.lang.management.ManagementFactory;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.URL;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.file.*;
|
|
import java.util.*;
|
|
import java.util.concurrent.*;
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
public class WebServer {
|
|
private final HttpServer server;
|
|
private final PlayerTimeTracker timeTracker;
|
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
|
private final ExecutorService executor = Executors.newFixedThreadPool(4);
|
|
private final MinecraftServer minecraftServer;
|
|
private static final Map<String, String> MIME_TYPES = Map.ofEntries(
|
|
Map.entry("html", "text/html"),
|
|
Map.entry("css", "text/css"),
|
|
Map.entry("js", "application/javascript"),
|
|
Map.entry("json", "application/json"),
|
|
Map.entry("png", "image/png"), // Added common web asset types
|
|
Map.entry("jpg", "image/jpeg"),
|
|
Map.entry("jpeg", "image/jpeg"),
|
|
Map.entry("gif", "image/gif"),
|
|
Map.entry("svg", "image/svg+xml"),
|
|
Map.entry("woff", "application/font-woff"),
|
|
Map.entry("woff2", "application/font-woff2"),
|
|
Map.entry("ttf", "application/font-sfnt"),
|
|
Map.entry("eot", "application/vnd.ms-fontobject")
|
|
);
|
|
|
|
public WebServer(PlayerTimeTracker timeTracker, int port, MinecraftServer minecraftServer) throws IOException {
|
|
this.minecraftServer = minecraftServer;
|
|
if (port < 1 || port > 65535) {
|
|
throw new IllegalArgumentException("Invalid port number: " + port);
|
|
}
|
|
this.timeTracker = timeTracker;
|
|
this.server = HttpServer.create(new InetSocketAddress(port), 0);
|
|
setupContexts();
|
|
}
|
|
|
|
private void setupContexts() {
|
|
server.createContext("/api/stats", exchange -> {
|
|
handleCors(exchange);
|
|
if ("OPTIONS".equals(exchange.getRequestMethod())) {
|
|
exchange.sendResponseHeaders(204, -1);
|
|
return;
|
|
}
|
|
|
|
if (!"GET".equals(exchange.getRequestMethod())) {
|
|
sendResponse(exchange, 405, "Method Not Allowed");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 白名单
|
|
Map<String, String> stats = timeTracker.getWhitelistedPlayerStats();
|
|
String response = GSON.toJson(stats);
|
|
sendResponse(exchange, 200, response.getBytes(StandardCharsets.UTF_8), "application/json");
|
|
} catch (Exception e) {
|
|
PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to get stats data", e);
|
|
sendResponse(exchange, 500, "Internal Server Error");
|
|
}
|
|
});
|
|
|
|
// 语言文件内容
|
|
server.createContext("/api/lang", exchange -> {
|
|
handleCors(exchange);
|
|
if ("OPTIONS".equals(exchange.getRequestMethod())) {
|
|
exchange.sendResponseHeaders(204, -1);
|
|
return;
|
|
}
|
|
|
|
if (!"GET".equals(exchange.getRequestMethod())) {
|
|
sendResponse(exchange, 405, "Method Not Allowed");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
String langCode = PlayerTimeMod.getConfig().getLanguage();
|
|
String resourcePath = String.format("assets/playertime/lang/%s.json", langCode);
|
|
|
|
String finalResourcePath1 = resourcePath;
|
|
InputStream is = FabricLoader.getInstance().getModContainer("playertime")
|
|
.flatMap(container -> container.findPath(finalResourcePath1))
|
|
.map(path -> {
|
|
try {
|
|
return path.toUri().toURL().openStream();
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
})
|
|
.orElse(null);
|
|
|
|
if (is == null) {
|
|
// Fallback to default language if configured language file is not found
|
|
langCode = "en_us"; // Default fallback
|
|
resourcePath = String.format("assets/playertime/lang/%s.json", langCode);
|
|
String finalResourcePath = resourcePath;
|
|
is = FabricLoader.getInstance().getModContainer("playertime")
|
|
.flatMap(container -> container.findPath(finalResourcePath))
|
|
.map(path -> {
|
|
try {
|
|
return path.toUri().toURL().openStream();
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
})
|
|
.orElse(null);
|
|
|
|
if (is == null) {
|
|
PlayerTimeMod.LOGGER.error("[PlayerTime] Default language file (en_us.json) not found!");
|
|
sendResponse(exchange, 500, "Language file not found");
|
|
return;
|
|
}
|
|
PlayerTimeMod.LOGGER.warn("[PlayerTime] Configured language file ({}.json) not found, using default (en_us.json).", PlayerTimeMod.getConfig().getLanguage());
|
|
}
|
|
|
|
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
|
byte[] data = new byte[1024];
|
|
int nRead;
|
|
while ((nRead = is.read(data, 0, data.length)) != -1) {
|
|
buffer.write(data, 0, nRead);
|
|
}
|
|
buffer.flush();
|
|
is.close();
|
|
|
|
sendResponse(exchange, 200, buffer.toByteArray(), "application/json");
|
|
PlayerTimeMod.LOGGER.debug("[PlayerTime] Served language file: {}", resourcePath);
|
|
|
|
} catch (IOException e) {
|
|
PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to read language file", e);
|
|
sendResponse(exchange, 500, "Error reading language file");
|
|
} catch (Exception e) {
|
|
PlayerTimeMod.LOGGER.error("[PlayerTime] An unknown error occurred while processing language request", e);
|
|
sendResponse(exchange, 500, "Internal Server Error");
|
|
}
|
|
});
|
|
|
|
|
|
// 静态文件服务
|
|
server.createContext("/", exchange -> {
|
|
try {
|
|
String requestPath = exchange.getRequestURI().getPath();
|
|
String resourceFileName;
|
|
|
|
if (requestPath.equals("/")) {
|
|
resourceFileName = "index.html";
|
|
} else {
|
|
int lastSlash = requestPath.lastIndexOf('/');
|
|
resourceFileName = requestPath.substring(lastSlash + 1);
|
|
}
|
|
String resourcePath = "assets/playertime/web" + requestPath;
|
|
if (requestPath.equals("/")) {
|
|
resourcePath += resourceFileName;
|
|
}
|
|
|
|
|
|
String finalResourcePath = resourcePath;
|
|
InputStream is = FabricLoader.getInstance().getModContainer("playertime")
|
|
.flatMap(container -> container.findPath(finalResourcePath))
|
|
.map(p -> {
|
|
try {
|
|
return p.toUri().toURL().openStream();
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
})
|
|
.orElse(null);
|
|
|
|
|
|
if (is == null) {
|
|
PlayerTimeMod.LOGGER.warn("[PlayerTime] Static resource not found: {}", resourcePath);
|
|
sendResponse(exchange, 404, "Not Found");
|
|
return;
|
|
}
|
|
|
|
// 确定内容类型,一层保险
|
|
String extension = "";
|
|
int dotIndex = resourceFileName.lastIndexOf('.');
|
|
if (dotIndex > 0 && dotIndex < resourceFileName.length() - 1) {
|
|
extension = resourceFileName.substring(dotIndex + 1).toLowerCase();
|
|
}
|
|
String contentType = MIME_TYPES.getOrDefault(extension, "application/octet-stream");
|
|
|
|
// 读取文件内容
|
|
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
|
byte[] data = new byte[1024];
|
|
int nRead;
|
|
while ((nRead = is.read(data, 0, data.length)) != -1) {
|
|
buffer.write(data, 0, nRead);
|
|
}
|
|
buffer.flush();
|
|
is.close();
|
|
|
|
sendResponse(exchange, 200, buffer.toByteArray(), contentType);
|
|
PlayerTimeMod.LOGGER.debug("[PlayerTime] Served static file: {}", resourcePath);
|
|
|
|
} catch (Exception e) {
|
|
PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to serve static resource", e);
|
|
sendResponse(exchange, 500, "Internal Server Error");
|
|
}
|
|
});
|
|
|
|
|
|
// 没啥用了
|
|
server.createContext("/api/widget-data", exchange -> {
|
|
handleCors(exchange);
|
|
if ("OPTIONS".equals(exchange.getRequestMethod())) {
|
|
exchange.sendResponseHeaders(204, -1);
|
|
return;
|
|
}
|
|
|
|
|
|
if (!"GET".equals(exchange.getRequestMethod())) {
|
|
sendResponse(exchange, 405, "Method Not Allowed");
|
|
return;
|
|
}
|
|
|
|
|
|
try {
|
|
MinecraftServer server = minecraftServer;
|
|
PlayerManager playerManager = server.getPlayerManager();
|
|
|
|
JsonObject response = new JsonObject();
|
|
response.addProperty("onlineCount", playerManager.getCurrentPlayerCount());
|
|
|
|
JsonArray whitelistPlayers = new JsonArray();
|
|
|
|
Set<UUID> whitelistUuids = new HashSet<>();
|
|
for (String name : playerManager.getWhitelist().getNames()) {
|
|
server.getUserCache().findByName(name).ifPresent(profile -> {
|
|
whitelistUuids.add(profile.getId());
|
|
});
|
|
}
|
|
|
|
for (ServerPlayerEntity player : playerManager.getPlayerList()) {
|
|
UUID uuid = player.getUuid();
|
|
if (whitelistUuids.contains(uuid)) {
|
|
PlayerTimeTracker.PlayerTimeStats stats = timeTracker.getPlayerStats(uuid);
|
|
if (stats != null) {
|
|
JsonObject playerJson = new JsonObject();
|
|
playerJson.addProperty("name", player.getName().getString());
|
|
playerJson.addProperty("time", PlayerTimeTracker.formatTime(stats.totalTime));
|
|
whitelistPlayers.add(playerJson);
|
|
}
|
|
}
|
|
}
|
|
response.add("whitelistPlayers", whitelistPlayers);
|
|
|
|
JsonArray topPlayers = new JsonArray();
|
|
timeTracker.getPlayerData().entrySet().stream()
|
|
.filter(entry -> whitelistUuids.contains(entry.getKey())) // 只筛选白名单玩家
|
|
.sorted((a, b) -> Long.compare(b.getValue().totalTime, a.getValue().totalTime))
|
|
.limit(3)
|
|
.forEach(entry -> {
|
|
JsonObject playerJson = new JsonObject();
|
|
playerJson.addProperty("name", timeTracker.getPlayerName(entry.getKey()));
|
|
playerJson.addProperty("time", PlayerTimeTracker.formatTime(entry.getValue().totalTime)); // formatTime doesn't need localization
|
|
topPlayers.add(playerJson);
|
|
});
|
|
|
|
response.add("topPlayers", topPlayers);
|
|
response.addProperty("timestamp", System.currentTimeMillis());
|
|
|
|
sendResponse(exchange, 200, GSON.toJson(response).getBytes(StandardCharsets.UTF_8), "application/json");
|
|
} catch (Exception e) {
|
|
PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to get widget data", e);
|
|
sendResponse(exchange, 500, "Internal Server Error");
|
|
}
|
|
});
|
|
|
|
// 在线玩家列表
|
|
server.createContext("/api/online-players", exchange -> {
|
|
handleCors(exchange);
|
|
if ("OPTIONS".equals(exchange.getRequestMethod())) {
|
|
exchange.sendResponseHeaders(204, -1);
|
|
return;
|
|
}
|
|
|
|
if (!"GET".equals(exchange.getRequestMethod())) {
|
|
sendResponse(exchange, 405, "Method Not Allowed");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
PlayerManager playerManager = minecraftServer.getPlayerManager();
|
|
JsonObject response = new JsonObject();
|
|
|
|
// 获取白名单玩家UUID集合
|
|
Set<UUID> whitelistUuids = new HashSet<>();
|
|
for (String name : playerManager.getWhitelist().getNames()) {
|
|
minecraftServer.getUserCache().findByName(name).ifPresent(profile -> {
|
|
whitelistUuids.add(profile.getId());
|
|
});
|
|
}
|
|
|
|
// 分类玩家
|
|
JsonArray whitelistedPlayers = new JsonArray();
|
|
JsonArray nonWhitelistedPlayers = new JsonArray();
|
|
|
|
for (ServerPlayerEntity player : playerManager.getPlayerList()) {
|
|
UUID uuid = player.getUuid();
|
|
JsonObject playerJson = new JsonObject();
|
|
playerJson.addProperty("name", player.getName().getString());
|
|
playerJson.addProperty("uuid", uuid.toString());
|
|
|
|
if (whitelistUuids.contains(uuid)) {
|
|
whitelistedPlayers.add(playerJson);
|
|
} else {
|
|
nonWhitelistedPlayers.add(playerJson);
|
|
}
|
|
}
|
|
|
|
response.add("whitelisted", whitelistedPlayers);
|
|
response.add("non_whitelisted", nonWhitelistedPlayers);
|
|
|
|
sendResponse(exchange, 200, GSON.toJson(response).getBytes(StandardCharsets.UTF_8), "application/json");
|
|
} catch (Exception e) {
|
|
PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to get online players list", e);
|
|
sendResponse(exchange, 500, "Internal Server Error");
|
|
}
|
|
});
|
|
|
|
// 玩家计数
|
|
server.createContext("/api/player-count", exchange -> {
|
|
handleCors(exchange);
|
|
if ("OPTIONS".equals(exchange.getRequestMethod())) {
|
|
exchange.sendResponseHeaders(204, -1);
|
|
return;
|
|
}
|
|
|
|
if (!"GET".equals(exchange.getRequestMethod())) {
|
|
sendResponse(exchange, 405, "Method Not Allowed");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
PlayerManager playerManager = minecraftServer.getPlayerManager();
|
|
JsonObject response = new JsonObject();
|
|
|
|
// 获取白名单玩家UUID集合
|
|
Set<UUID> whitelistUuids = new HashSet<>();
|
|
for (String name : playerManager.getWhitelist().getNames()) {
|
|
minecraftServer.getUserCache().findByName(name).ifPresent(profile -> {
|
|
whitelistUuids.add(profile.getId());
|
|
});
|
|
}
|
|
|
|
// 分类计数
|
|
int whitelistedCount = 0;
|
|
int nonWhitelistedCount = 0;
|
|
|
|
for (ServerPlayerEntity player : playerManager.getPlayerList()) {
|
|
if (whitelistUuids.contains(player.getUuid())) {
|
|
whitelistedCount++;
|
|
} else {
|
|
nonWhitelistedCount++;
|
|
}
|
|
}
|
|
|
|
response.addProperty("total", playerManager.getCurrentPlayerCount());
|
|
response.addProperty("whitelisted", whitelistedCount);
|
|
response.addProperty("non_whitelisted", nonWhitelistedCount);
|
|
|
|
sendResponse(exchange, 200, GSON.toJson(response).getBytes(StandardCharsets.UTF_8), "application/json");
|
|
} catch (Exception e) {
|
|
PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to get player count", e);
|
|
sendResponse(exchange, 500, "Internal Server Error");
|
|
}
|
|
});
|
|
|
|
// 白名单玩家(还有用吗?)
|
|
server.createContext("/api/whitelist", exchange -> {
|
|
handleCors(exchange);
|
|
if ("OPTIONS".equals(exchange.getRequestMethod())) {
|
|
exchange.sendResponseHeaders(204, -1);
|
|
return;
|
|
}
|
|
|
|
if (!"GET".equals(exchange.getRequestMethod())) {
|
|
sendResponse(exchange, 405, "Method Not Allowed");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
PlayerManager playerManager = minecraftServer.getPlayerManager();
|
|
JsonArray whitelist = new JsonArray();
|
|
|
|
for (String name : playerManager.getWhitelist().getNames()) {
|
|
JsonObject player = new JsonObject();
|
|
player.addProperty("name", name);
|
|
|
|
// 尝试获取UUID
|
|
Optional<GameProfile> profile = minecraftServer.getUserCache().findByName(name);
|
|
if (profile.isPresent()) {
|
|
player.addProperty("uuid", profile.get().getId().toString());
|
|
|
|
// 检查是否在线
|
|
ServerPlayerEntity onlinePlayer = playerManager.getPlayer(profile.get().getId());
|
|
player.addProperty("online", onlinePlayer != null);
|
|
} else {
|
|
player.addProperty("online", false);
|
|
}
|
|
|
|
whitelist.add(player);
|
|
}
|
|
|
|
sendResponse(exchange, 200, GSON.toJson(whitelist).getBytes(StandardCharsets.UTF_8), "application/json");
|
|
} catch (Exception e) {
|
|
PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to get whitelist", e);
|
|
sendResponse(exchange, 500, "Internal Server Error");
|
|
}
|
|
});
|
|
|
|
// 服务器状态
|
|
server.createContext("/api/server-status", exchange -> {
|
|
handleCors(exchange);
|
|
if ("OPTIONS".equals(exchange.getRequestMethod())) {
|
|
exchange.sendResponseHeaders(204, -1);
|
|
return;
|
|
}
|
|
|
|
if (!"GET".equals(exchange.getRequestMethod())) {
|
|
sendResponse(exchange, 405, "Method Not Allowed");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
Runtime runtime = Runtime.getRuntime();
|
|
JsonObject status = new JsonObject();
|
|
|
|
long maxMemory = runtime.maxMemory();
|
|
long totalMemory = runtime.totalMemory();
|
|
long freeMemory = runtime.freeMemory();
|
|
long usedMemory = totalMemory - freeMemory;
|
|
|
|
JsonObject memory = new JsonObject();
|
|
memory.addProperty("max", maxMemory);
|
|
memory.addProperty("total", totalMemory);
|
|
memory.addProperty("used", usedMemory);
|
|
memory.addProperty("free", freeMemory);
|
|
memory.addProperty("usage_percentage", (double) usedMemory / maxMemory * 100);
|
|
status.add("memory", memory);
|
|
|
|
status.addProperty("available_processors", runtime.availableProcessors());
|
|
|
|
long uptime = ManagementFactory.getRuntimeMXBean().getUptime();
|
|
status.addProperty("uptime", uptime);
|
|
status.addProperty("uptime_formatted", formatUptime(uptime)); // formatUptime doesn't need localization
|
|
|
|
File diskPartition = new File(".");
|
|
long totalSpace = diskPartition.getTotalSpace();
|
|
long freeSpace = diskPartition.getFreeSpace();
|
|
long usableSpace = diskPartition.getUsableSpace();
|
|
|
|
JsonObject disk = new JsonObject();
|
|
disk.addProperty("total", totalSpace);
|
|
disk.addProperty("free", freeSpace);
|
|
disk.addProperty("usable", usableSpace);
|
|
disk.addProperty("usage_percentage", (double) (totalSpace - freeSpace) / totalSpace * 100);
|
|
status.add("disk", disk);
|
|
|
|
JsonObject serverInfo = new JsonObject();
|
|
serverInfo.addProperty("version", minecraftServer.getVersion());
|
|
serverInfo.addProperty("player_count", minecraftServer.getCurrentPlayerCount());
|
|
serverInfo.addProperty("max_players", minecraftServer.getMaxPlayerCount());
|
|
|
|
serverInfo.addProperty("average_tick_time_ms", minecraftServer.getAverageTickTime());
|
|
|
|
long[] tickTimes = minecraftServer.getTickTimes();
|
|
if (tickTimes != null && tickTimes.length > 0) {
|
|
double recentAvgTickTime = Arrays.stream(tickTimes).average().orElse(0) / 1000000.0;
|
|
serverInfo.addProperty("recent_avg_tick_time_ms", recentAvgTickTime);
|
|
|
|
JsonArray recentTicks = new JsonArray();
|
|
int sampleCount = Math.min(10, tickTimes.length);
|
|
for (int i = 0; i < sampleCount; i++) {
|
|
recentTicks.add(tickTimes[i] / 1000000.0);
|
|
}
|
|
serverInfo.add("recent_tick_samples_ms", recentTicks);
|
|
}
|
|
|
|
status.add("server", serverInfo);
|
|
|
|
sendResponse(exchange, 200, GSON.toJson(status).getBytes(StandardCharsets.UTF_8), "application/json");
|
|
} catch (Exception e) {
|
|
PlayerTimeMod.LOGGER.error("[PlayerTime] Failed to get server status", e);
|
|
sendResponse(exchange, 500, "Internal Server Error");
|
|
}
|
|
});
|
|
|
|
// 原始数据文件
|
|
server.createContext("/api/playerdata", exchange -> {
|
|
handleCors(exchange);
|
|
if ("OPTIONS".equals(exchange.getRequestMethod())) {
|
|
exchange.sendResponseHeaders(204, -1); // 204 No Content
|
|
return;
|
|
}
|
|
|
|
if (!"GET".equals(exchange.getRequestMethod())) {
|
|
sendResponse(exchange, 405, "Method Not Allowed");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
Path dataFile = timeTracker.getDataFile(); // 从PlayerTimeTracker获取文件路径
|
|
|
|
if (!Files.exists(dataFile)) {
|
|
PlayerTimeMod.LOGGER.warn("[PlayerTime] Player data file not found: {}", dataFile);
|
|
sendResponse(exchange, 404, "Data file not found");
|
|
return;
|
|
}
|
|
|
|
byte[] fileContent = Files.readAllBytes(dataFile);
|
|
|
|
sendResponse(exchange, 200, fileContent, "application/json");
|
|
PlayerTimeMod.LOGGER.debug("[PlayerTime] Successfully served player data file {}", dataFile);
|
|
|
|
} catch (IOException e) {
|
|
PlayerTimeMod.LOGGER.error("[PlayerTime] Error reading player data file", e);
|
|
sendResponse(exchange, 500, "Error reading data file");
|
|
} catch (Exception e) {
|
|
PlayerTimeMod.LOGGER.error("[PlayerTime] An unknown error occurred while processing player data file request", e);
|
|
sendResponse(exchange, 500, "Internal Server Error");
|
|
}
|
|
});
|
|
|
|
|
|
server.setExecutor(executor);
|
|
}
|
|
|
|
private void handleCors(HttpExchange exchange) {
|
|
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
|
|
exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type");
|
|
}
|
|
|
|
private void sendResponse(HttpExchange exchange, int code, String response) throws IOException {
|
|
sendResponse(exchange, code, response.getBytes(StandardCharsets.UTF_8), "text/plain");
|
|
}
|
|
|
|
private void sendResponse(HttpExchange exchange, int code, byte[] response, String contentType) throws IOException {
|
|
exchange.getResponseHeaders().set("Content-Type", contentType);
|
|
exchange.sendResponseHeaders(code, response.length);
|
|
try (OutputStream os = exchange.getResponseBody()) {
|
|
os.write(response);
|
|
}
|
|
}
|
|
|
|
public void start() {
|
|
server.start();
|
|
}
|
|
|
|
public void stop() {
|
|
server.stop(0);
|
|
executor.shutdown();
|
|
}
|
|
|
|
|
|
private String formatUptime(long millis) {
|
|
long seconds = millis / 1000;
|
|
long days = seconds / 86400;
|
|
seconds %= 86400;
|
|
long hours = seconds / 3600;
|
|
seconds %= 3600;
|
|
long minutes = seconds / 60;
|
|
seconds %= 60;
|
|
|
|
return String.format("%d天 %02d小时 %02d分钟 %02d秒", days, hours, minutes, seconds);
|
|
}
|
|
}
|