From a0f2ce2375addd60f45d16c48930a652ebd81a57 Mon Sep 17 00:00:00 2001 From: Lixfel Date: Tue, 16 Jan 2024 12:50:46 +0100 Subject: [PATCH] Checkpointing support --- build.gradle | 4 +- src/de/steamwar/bungeecore/Arenaserver.java | 14 +- src/de/steamwar/bungeecore/Bauserver.java | 35 ++- src/de/steamwar/bungeecore/Builderserver.java | 29 ++- src/de/steamwar/bungeecore/Subserver.java | 209 +++++++++--------- 5 files changed, 165 insertions(+), 126 deletions(-) diff --git a/build.gradle b/build.gradle index f4a478c..533077e 100644 --- a/build.gradle +++ b/build.gradle @@ -32,8 +32,8 @@ mainClassName = '' compileJava.options.encoding = 'UTF-8' -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +sourceCompatibility = 1.11 +targetCompatibility = 1.11 sourceSets { main { diff --git a/src/de/steamwar/bungeecore/Arenaserver.java b/src/de/steamwar/bungeecore/Arenaserver.java index c1b826b..cd2a5fd 100644 --- a/src/de/steamwar/bungeecore/Arenaserver.java +++ b/src/de/steamwar/bungeecore/Arenaserver.java @@ -1,5 +1,8 @@ package de.steamwar.bungeecore; +import lombok.Getter; + +@Getter public class Arenaserver extends Subserver { private final String mode; @@ -13,15 +16,4 @@ public class Arenaserver extends Subserver { this.allowMerge = allowMerge; } - public String getMode() { - return mode; - } - - public String getMap() { - return map; - } - - public boolean isAllowMerge() { - return allowMerge; - } } diff --git a/src/de/steamwar/bungeecore/Bauserver.java b/src/de/steamwar/bungeecore/Bauserver.java index 7d98474..bd00a7f 100644 --- a/src/de/steamwar/bungeecore/Bauserver.java +++ b/src/de/steamwar/bungeecore/Bauserver.java @@ -19,26 +19,41 @@ package de.steamwar.bungeecore; +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; import java.util.UUID; +@Getter public class Bauserver extends Subserver { + private static final Map servers = new HashMap<>(); + public static Bauserver get(UUID owner) { + synchronized (servers) { + return servers.get(owner); + } + } private final UUID owner; - public Bauserver(String serverName, UUID owner, int port, String... command) { - this(serverName, owner, port, () -> {}, command); - } - - public Bauserver(String serverName, UUID owner, int port, Runnable shutdownCallback, String... command) { - this(serverName, owner, port, new ProcessBuilder(command), shutdownCallback); - } - public Bauserver(String serverName, UUID owner, int port, ProcessBuilder processBuilder, Runnable shutdownCallback){ super(Servertype.BAUSERVER, serverName, port, processBuilder, shutdownCallback); this.owner = owner; } - public UUID getOwner(){ - return owner; + @Override + protected void register() { + super.register(); + synchronized (servers) { + servers.put(owner, this); + } + } + + @Override + protected void unregister() { + synchronized (servers) { + servers.remove(owner); + } + super.unregister(); } } diff --git a/src/de/steamwar/bungeecore/Builderserver.java b/src/de/steamwar/bungeecore/Builderserver.java index 5b9baf0..a9a152f 100644 --- a/src/de/steamwar/bungeecore/Builderserver.java +++ b/src/de/steamwar/bungeecore/Builderserver.java @@ -19,8 +19,21 @@ package de.steamwar.bungeecore; +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +@Getter public class Builderserver extends Subserver { + private static final Map servers = new HashMap<>(); + public static Builderserver get(String map) { + synchronized (servers) { + return servers.get(map); + } + } + private final String map; public Builderserver(String serverName, String map, int port, ProcessBuilder processBuilder, Runnable shutdownCallback){ @@ -28,7 +41,19 @@ public class Builderserver extends Subserver { this.map = map; } - public String getMap() { - return map; + @Override + protected void register() { + super.register(); + synchronized (servers) { + servers.put(map, this); + } + } + + @Override + protected void unregister() { + synchronized (servers) { + servers.remove(map); + } + super.unregister(); } } diff --git a/src/de/steamwar/bungeecore/Subserver.java b/src/de/steamwar/bungeecore/Subserver.java index 3eabf41..2cdfcbc 100644 --- a/src/de/steamwar/bungeecore/Subserver.java +++ b/src/de/steamwar/bungeecore/Subserver.java @@ -19,6 +19,7 @@ package de.steamwar.bungeecore; +import lombok.Getter; import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.chat.BaseComponent; @@ -26,26 +27,29 @@ import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.connection.ProxiedPlayer; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PrintWriter; +import java.io.*; import java.net.InetSocketAddress; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; -@SuppressWarnings("deprecation") -public class Subserver implements Runnable { +public class Subserver { - private static final List serverList = new LinkedList<>(); + @SuppressWarnings("deprecation") + private static final Map servers = ProxyServer.getInstance().getServers(); private static final Logger logger = ProxyServer.getInstance().getLogger(); - public static Subserver getSubserver(ProxiedPlayer p){ + @Getter + private static final List serverList = new LinkedList<>(); + private static final Map infoToServer = new HashMap<>(); + + public static Subserver getSubserver(ProxiedPlayer p) { synchronized (serverList) { for(int i = serverList.size()-1; i >= 0; i--){ if(serverList.get(i).onServer(p)) @@ -54,43 +58,49 @@ public class Subserver implements Runnable { } return null; } - public static Subserver getSubserver(ServerInfo server){ + + public static Subserver getSubserver(ServerInfo server) { synchronized (serverList) { - for(Subserver s : serverList) - if(s.server == server) - return s; + return infoToServer.get(server); } - return null; } - public static List getServerList(){ - return serverList; + + static void shutdown(){ + while (!serverList.isEmpty()) { + Subserver server = serverList.get(0); + server.stop(); + } } private final String serverName; + private final boolean checkpoint; private final Runnable shutdownCallback; + private final Consumer failureCallback; private final Process process; private final PrintWriter writer; + @Getter private final ServerInfo server; + @Getter private final Servertype type; private final Thread thread; + @Getter private boolean started; private final List cachedPlayers = new LinkedList<>(); + @Getter private final Map tablistNames = new HashMap<>(); - public Subserver(Servertype type, String serverName, int port, String... command){ - this(type, serverName, port, () -> {}, command); - } - - public Subserver(Servertype type, String serverName, int port, Runnable shutdownCallback, String... command){ - this(type, serverName, port, new ProcessBuilder(command), shutdownCallback); - } - public Subserver(Servertype type, String serverName, int port, ProcessBuilder processBuilder, Runnable shutdownCallback){ + this(type, serverName, port, processBuilder, shutdownCallback, null); + } + + public Subserver(Servertype type, String serverName, int port, ProcessBuilder processBuilder, Runnable shutdownCallback, Consumer failureCallback) { this.started = false; this.serverName = serverName; this.type = type; this.shutdownCallback = shutdownCallback; + this.failureCallback = failureCallback == null ? this::fatalError : failureCallback; + this.checkpoint = processBuilder.command().contains("criu"); try{ this.process = processBuilder.start(); @@ -103,27 +113,18 @@ public class Subserver implements Runnable { serverName, address, "SteamWar.de - Subserver", false); this.writer = new PrintWriter(process.getOutputStream(), true); - this.thread = new Thread(this, "Subserver " + serverName); + this.thread = new Thread(this::run, "Subserver " + serverName); this.thread.start(); } - public ServerInfo getServer(){ - return server; - } - public Servertype getType(){ - return type; - } - public Map getTablistNames(){ - return tablistNames; - } - + @Deprecated public boolean hasStarted(){ return started; } - public void sendPlayer(ProxiedPlayer p){ + public void sendPlayer(ProxiedPlayer p) { if(!started){ - p.sendMessage(Persistent.PREFIX + "§7Starting server, please wait..."); + p.sendMessage(ChatMessageType.ACTION_BAR, generateBar(0)); cachedPlayers.add(p); }else{ p.connect(server); @@ -134,11 +135,16 @@ public class Subserver implements Runnable { writer.println(command); } - public void stop(){ - execute("stop"); + public void stop() { + if(checkpoint) + process.children().forEach(ProcessHandle::destroy); + else + execute("stop"); + try { if(!process.waitFor(1, TimeUnit.MINUTES)) process.destroyForcibly(); + thread.join(); }catch(InterruptedException e){ logger.log(Level.SEVERE, "Subserver stop interrupted!", e); @@ -146,98 +152,99 @@ public class Subserver implements Runnable { } } - public void waitForTermination(){ - try { - if(!process.waitFor(5, TimeUnit.MINUTES) && server.getPlayers().isEmpty()){ - process.destroy(); - } - thread.join(); - }catch(InterruptedException e){ - logger.log(Level.SEVERE, "Subserver stop interrupted!", e); - Thread.currentThread().interrupt(); - } - } - - static void shutdown(){ - while (!serverList.isEmpty()) { - Subserver server = serverList.get(0); - server.stop(); - } - } - private boolean onServer(ProxiedPlayer p){ return cachedPlayers.contains(p) || server.getPlayers().contains(p); } - private void fatalError(){ + private void fatalError(Exception e) { + logger.log(Level.SEVERE, e, () -> serverName + " did not run correctly!"); + for(ProxiedPlayer cached : cachedPlayers) - cached.sendMessage(Persistent.PREFIX + "§cUnexpected error during server startup."); + cached.sendMessage(TextComponent.fromLegacy(Persistent.PREFIX + "§cUnexpected error during server startup.")); for(ProxiedPlayer player : server.getPlayers()) - player.sendMessage(Persistent.PREFIX + "§cLost connection to server."); + player.sendMessage(TextComponent.fromLegacy(Persistent.PREFIX + "§cLost connection to server.")); } - private void sendProgress(int progress){ - StringBuilder sb = new StringBuilder("§e"); - for(int i = 0; i < progress; i++) - sb.append('⬛'); - sb.append("§8"); - for(int i = progress; i < 10; i++) - sb.append('⬛'); - - BaseComponent[] tc = TextComponent.fromLegacyText(sb.toString()); - for(ProxiedPlayer cached : cachedPlayers) - cached.sendMessage(ChatMessageType.ACTION_BAR, tc); - } - - public void run(){ - ProxyServer.getInstance().getServers().put(serverName, server); - synchronized (serverList) { - serverList.add(this); - } - - try(BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))){ + private void start(InputStream stream, Predicate test) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { String line = ""; while (!started && (line = reader.readLine()) != null) { - started = line.contains("ViaVersion detected server version"); - if(line.contains("Loading libraries, please wait")) - sendProgress(0); - else if(line.contains("Starting Minecraft server on")) - sendProgress(3); - else if(line.contains("Preparing start region")) - sendProgress(6); + started = test.test(line); } if(line == null){ - logger.log(Level.SEVERE, "Subserver {0} stopped to early!", serverName); - fatalError(); - return; + failureCallback.accept(new IOException(serverName + " did not start correctly!")); + started = false; + } + } + } + + protected void register() { + servers.put(serverName, server); + synchronized (serverList) { + serverList.add(this); + infoToServer.put(server, this); + } + } + + protected void unregister() { + synchronized (serverList){ + infoToServer.remove(server); + serverList.remove(this); + } + servers.remove(serverName); + } + + private void run() { + register(); + + try { + if(checkpoint) { + start(process.getErrorStream(), line -> line.contains("Restore finished successfully.")); + } else { + start(process.getInputStream(), line -> { + if(line.contains("Loading libraries, please wait")) + sendProgress(2); + else if(line.contains("Starting Minecraft server on")) + sendProgress(4); + else if(line.contains("Preparing start region")) + sendProgress(6); + return line.contains("Done ("); + }); } - sendProgress(9); + if(!started) + return; + + sendProgress(8); Thread.sleep(300); sendProgress(10); - for(ProxiedPlayer cachedPlayer : cachedPlayers){ + for(ProxiedPlayer cachedPlayer : cachedPlayers) { sendPlayer(cachedPlayer); } cachedPlayers.clear(); - reader.close(); // Prevent stdout clogging up process.waitFor(); } catch(IOException e) { - logger.log(Level.SEVERE, "Server " + serverName + " was interrupted!", e); - fatalError(); + failureCallback.accept(e); } catch(InterruptedException e) { - logger.log(Level.SEVERE, "Server " + serverName + " was interrupted!", e); - fatalError(); + failureCallback.accept(e); Thread.currentThread().interrupt(); } finally { - synchronized (serverList){ - serverList.remove(this); - } - ProxyServer.getInstance().getServers().remove(serverName); shutdownCallback.run(); + unregister(); } } + + private BaseComponent generateBar(int progress) { + return TextComponent.fromLegacy("§e" + "⬛".repeat(Math.max(0, progress)) + "§8" + "⬛".repeat(Math.max(0, 10 - progress))); + } + + private void sendProgress(int progress) { + BaseComponent tc = generateBar(progress); + for(ProxiedPlayer cached : cachedPlayers) + cached.sendMessage(ChatMessageType.ACTION_BAR, tc); + } }