EpicKnarvik97 5e24d5daa8
All checks were successful
KnarCraft/Minecraft-Server-Launcher/pipeline/head This commit looks good
Improves checking for whether a server should be delayed before starting
2021-08-24 16:08:02 +02:00

582 lines
18 KiB
Java

package net.knarcraft.minecraftserverlauncher.server;
import net.knarcraft.minecraftserverlauncher.Main;
import net.knarcraft.minecraftserverlauncher.profile.ServerLauncherController;
import net.knarcraft.minecraftserverlauncher.server.servertypes.ServerType;
import net.knarcraft.minecraftserverlauncher.userinterface.ServerLauncherGUI;
import net.knarcraft.minecraftserverlauncher.utility.CommonFunctions;
import javax.naming.ConfigurationException;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Contains all necessary information to create, runServer and manage a Minecraft server.
*
* @author Kristian Knarvik <kristian.knarvik@knett.no>
* @version 1.0.0
* @since 1.0.0
*/
public class Server {
private static final String jarDirectory = Main.getApplicationWorkDirectory() + File.separator + "files" +
File.separator + "Jars" + File.separator;
private final String name;
private final ArrayList<String> playerList;
private String path;
private boolean enabled;
private ServerType type;
private String serverVersion;
private String maxRam;
private Process process;
private BufferedWriter writer;
private BufferedReader reader;
private boolean started;
private ScheduledExecutorService consoleOutputExecutor;
private final ServerLauncherGUI gui = Main.getController().getGUI();
/**
* Initializes a new server with default values
*
* @param name <p>The name of the server</p>
*/
public Server(String name) {
this.name = name;
this.path = "";
this.enabled = false;
this.playerList = new ArrayList<>();
this.type = null;
this.serverVersion = "";
this.maxRam = ServerHandler.getRamList()[0];
this.process = null;
this.writer = null;
this.reader = null;
}
/**
* Initializes a server with the given values
*
* @param name <p>The name of the server</p>
* @param path <p>The file path of the folder containing the server files</p>
* @param enabled <p>Whether the server is enabled to start the next time servers are started</p>
* @param typeName <p>The name of the server type currently in use on the server</p>
* @param serverVersion <p>The currently selected server version for the given server type</p>
* @param maxRam <p>The maximum amount of ram the server is allowed to use</p>
*/
public Server(String name, String path, boolean enabled, String typeName, String serverVersion, String maxRam) throws ConfigurationException {
this.name = name;
this.path = path;
this.enabled = enabled;
this.type = ServerTypeHandler.getByName(typeName);
this.serverVersion = serverVersion;
this.maxRam = maxRam;
this.playerList = new ArrayList<>();
}
/**
* Gets the buffered reader used to read from this server
*
* @return <p>The buffered reader used to read from this server</p>
*/
private BufferedReader getReader() {
return this.reader;
}
/**
* Gets the name of the server
*
* @return <p>The name of the server</p>
*/
public String getName() {
return this.name;
}
/**
* Whether the server has been started
*
* @return <p>True if the server has been started. False otherwise</p>
*/
public boolean isStarted() {
return started;
}
/**
* Gets the name of the server type used by this server
*
* @return <p>The name of the server type used by this server</p>
*/
public String getTypeName() {
return this.type.getName();
}
/**
* Gets the server type used by this server
*
* @return <p>The server type used by this server</p>
*/
public ServerType getType() {
return this.type;
}
/**
* Gets the version used given server type used
*
* @return <p>The server version given server type</p>
*/
public String getServerVersion() {
return this.serverVersion;
}
/**
* Sets the server's server version to a valid version, or ignores the request
*
* @param serverVersion <p>The new server version</p>
*/
public void setServerVersion(String serverVersion) throws IllegalArgumentException {
if (this.type.getName().equals("Custom")) {
this.serverVersion = serverVersion;
} else {
String[] versions = this.type.getVersions();
for (String version : versions) {
if (version.equals(serverVersion)) {
this.serverVersion = serverVersion;
return;
}
}
throw new IllegalArgumentException("Invalid server version.");
}
}
/**
* Gets whether this server is a proxy server
*
* <p>A proxy server is a server running BungeeCord, Waterfall or Travertine.</p>
*
* @return <p>True if this server is a proxy server</p>
*/
public boolean isProxy() {
return this.type.isProxy();
}
/**
* Marks this server as stopped
*/
public void setStopped() {
this.started = false;
}
/**
* Gets the writer used to write to this server
*
* @return <p>The writer used.</p>
*/
public BufferedWriter getWriter() {
return this.writer;
}
/**
* Sets the writer used to write to this server
*
* @param writer <p>The new writer to use.</p>
*/
public void setWriter(BufferedWriter writer) {
this.writer = writer;
}
/**
* Gets the path for this server's files
*
* @return <p>The path of this server's files</p>
*/
public String getPath() {
return this.path;
}
/**
* Sets the path of this server's files
*
* @param path <p>The new path of the server's files</p>
*/
public void setPath(String path) {
this.path = path;
}
/**
* Gets the process of the server
*
* @return <p>The server process</p>
*/
public Process getProcess() {
return this.process;
}
/**
* Gets the maximum amount of ram usable by this server
*
* @return <p>The maximum amount of ram this server can use</p>
*/
public String getMaxRam() {
return this.maxRam;
}
/**
* Sets the max ram to be used by the server
*
* @param ram <p>The new maximum ram amount</p>
*/
public void setMaxRam(String ram) {
this.maxRam = ram;
}
/**
* Gets a list of all players connected to this server
*
* @return <p>A list of all players connected to the server</p>
*/
public List<String> getPlayers() {
return this.playerList;
}
/**
* Removes all information about the server's process, writer and reader
*/
private void cleanStoppedServerValues() {
consoleOutputExecutor.shutdown();
process = null;
writer = null;
reader = null;
started = false;
}
/**
* Checks whether this server is currently enabled
*
* @return <p>True if the server is currently enabled</p>
*/
public boolean isEnabled() {
return this.enabled;
}
/**
* Sets whether this server is currently enabled
*
* @param value <p>Whether the server is currently enabled</p>
*/
public void setEnabled(boolean value) {
this.enabled = value;
}
/**
* Checks whether this server has a given player
*
* @param name <p>The name of the player to check</p>
* @return <p>True if the player is connected</p>
*/
private boolean hasPlayer(String name) {
for (String player : this.playerList) {
if (player.equals(name)) {
return true;
}
}
return false;
}
/**
* Adds a player to the GUI and this server's player list
*
* @param name <p>The name of the player to add</p>
*/
private void addPlayer(String name) {
this.playerList.add(name);
gui.getServerControlTab().addPlayer(name);
}
/**
* Removes a player with the selected name from the player list
*
* @param name <p>The name of the player to remove</p>
*/
private void removePlayer(String name) {
playerList.removeIf(player -> player.equals(name));
gui.getServerControlTab().removePlayer(name);
}
/**
* Sets the server type to be used by the server
*
* @param type <p>The new server type to be used by the server</p>
*/
public void setType(ServerType type) {
this.type = type;
}
/**
* Runs a Minecraft server
*
* @param skipDelay <p>Whether to skip the startup delay for this server</p>
* @return <p>True if nothing went wrong</p>
*/
public boolean runServer(boolean skipDelay) {
if (ServerHandler.stoppingServers()) {
gui.logMessage("Stopping servers. Cannot start yet.");
return false;
}
//Ignore a disabled server
if (!this.enabled) {
this.started = false;
return true;
}
//Tries to do necessary pre-start work
if (!initializeJarDownload() || (!skipDelay && !delayStartup())) {
gui.logError("Failed to perform startup tasks.");
this.started = false;
return false;
}
if (ServerHandler.stoppingServers()) {
gui.logMessage("Stopping servers. Cannot start yet.");
return false;
}
//Starts the server if possible
try {
startServerProcess();
gui.setStatus("Servers are running");
this.started = true;
return true;
} catch (IOException e) {
gui.setStatus("Could not start server");
this.started = false;
return false;
}
}
/**
* Gets the correct java command to use for the selected server version
*
* @return <p>The java version to run</p>
*/
private String getJavaCommand() {
ServerLauncherController controller = ServerLauncherController.getInstance();
if (serverVersion.toLowerCase().contains("latest")) {
return controller.getJavaCommand();
} else if (serverVersion.contains(".") && serverVersion.split("\\.").length >= 2) {
try {
if (Integer.parseInt(serverVersion.split("\\.")[1]) >= 17) {
return controller.getJavaCommand();
}
} catch (NumberFormatException ignored) {}
}
return controller.getOldJavaCommand();
}
/**
* Starts the process running this server
*
* @throws IOException <p>If the process cannot be started</p>
*/
private void startServerProcess() throws IOException {
ProcessBuilder builder;
String serverPath;
//Decide the path of the .jar file to be executed
if (type.getName().equals("Custom")) {
serverPath = this.path + File.separator + serverVersion;
} else {
serverPath = jarDirectory + this.type.getName() + serverVersion + ".jar";
}
builder = new ProcessBuilder(getJavaCommand(), "-Xmx" + this.maxRam, "-Xms512M",
"-Djline.terminal=jline.UnsupportedTerminal", "-Dcom.mojang.eula.agree=true", "-jar", serverPath,
"nogui");
builder.directory(new File(this.path));
builder.redirectErrorStream(true);
this.process = builder.start();
this.writer = new BufferedWriter(new OutputStreamWriter(this.process.getOutputStream()));
this.reader = new BufferedReader(new InputStreamReader(this.process.getInputStream()));
//Start the process for reading from the server's console
consoleOutputExecutor = Executors.newSingleThreadScheduledExecutor();
consoleOutputExecutor.scheduleAtFixedRate(() -> {
try {
updateConsole();
} catch (IOException e) {
e.printStackTrace();
}
}, 10, 500, TimeUnit.MILLISECONDS);
}
/**
* Updates a single console with process output
*
* @throws IOException <p>If unable to read from the server's buffered reader</p>
*/
private void updateConsole() throws IOException {
String readText = CommonFunctions.readBufferedReader(getReader());
if (!readText.equals("")) {
Main.getController().getCurrentProfile().getCollection(getName()).getServerConsole().output(readText);
updatePlayerList(readText);
}
if (!getProcess().isAlive()) {
cleanStoppedServerValues();
}
}
/**
* Looks for strings implying a player has joined or left, and updates the appropriate lists
*
* @param text <p>The text to search</p>
*/
private void updatePlayerList(String text) {
if (!getType().isProxy()) {
String joinedPlayer = getPlayer(text, true);
String leftPlayer = getPlayer(text, false);
if (!joinedPlayer.equals("")) {
if (!hasPlayer(joinedPlayer)) {
addPlayer(joinedPlayer);
}
} else if (!leftPlayer.equals("")) {
if (hasPlayer(leftPlayer)) {
removePlayer(leftPlayer);
}
}
}
}
/**
* Searches a string for players joining or leaving
*
* @param text <p>The text string to search through</p>
* @param joined <p>Whether to search for a joining player</p>
* @return <p>The name of a player, or an empty string</p>
*/
private String getPlayer(String text, boolean joined) {
String playerName;
String loginPattern1 = " ([A-Z0-9a-z_]+)\\[/[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+:[0-9]+] logged in";
String loginPattern2 = "UUID of player ([A-Z0-9a-z_]+) is";
String logoutPattern1 = "INFO]: ([A-Z0-9a-z_]+) lost connection";
String logoutPattern2 = " ([A-Z0-9a-z_]+) left the game";
if (joined) {
playerName = getFirstRegexCaptureGroup(loginPattern1, text);
if (playerName.equals("")) {
playerName = getFirstRegexCaptureGroup(loginPattern2, text);
}
} else {
playerName = getFirstRegexCaptureGroup(logoutPattern1, text);
if (playerName.equals("")) {
playerName = getFirstRegexCaptureGroup(logoutPattern2, text);
}
}
return playerName;
}
/**
* Returns the first regex capture group found in a pattern
*
* @param pattern <p>The regex pattern to use</p>
* @param text <p>The string to execute the pattern on</p>
* @return <p>The first capture group if a match is found. An empty string otherwise</p>
*/
private String getFirstRegexCaptureGroup(String pattern, String text) {
Pattern compiledPattern = Pattern.compile(pattern);
Matcher patternMatcher = compiledPattern.matcher(text);
if (patternMatcher.find()) {
return patternMatcher.group(1);
} else {
return "";
}
}
/**
* Delays the server's startup for the given amount of time
*
* @return <p>True if the delay was successful</p>
*/
private boolean delayStartup() {
try {
gui.setStatus("Delaying startup");
TimeUnit.SECONDS.sleep(Main.getController().getCurrentProfile().getDelayStartup());
return true;
} catch (InterruptedException e) {
e.printStackTrace();
this.started = false;
return false;
}
}
/**
* Starts downloading the necessary .jar file
*
* @return <p>True if nothing went wrong</p>
*/
private boolean initializeJarDownload() {
try {
gui.setStatus("Downloading jar...");
this.downloadJar();
gui.setStatus("File downloaded");
} catch (IOException e) {
gui.setStatus("Error: Jar file could not be found, downloaded or built.");
gui.logError("Unable to get required .jar file: " + e.getMessage());
this.started = false;
return false;
}
return true;
}
/**
* Downloads necessary .jar file for the server
*
* @throws FileNotFoundException <p>If the file was not found and could not be acquired</p>
*/
private void downloadJar() throws IOException {
String path;
if (this.type.getName().equals("Custom")) {
path = this.path + File.separator + this.serverVersion;
} else {
path = jarDirectory;
}
File file = new File(path);
if (!(file.isFile() || type.downloadJar(path, this.serverVersion))) {
throw new FileNotFoundException("Jar file could not be downloaded.");
}
}
/**
* Sends a command to this server through its writer
*
* @param command <p>Command to send to the server</p>
* @throws IOException <p>If write fails</p>
*/
public void sendCommand(String command) throws IOException {
if (this.process != null && this.writer != null) {
this.writer.write(command + "\n");
this.writer.flush();
}
}
@Override
public String toString() {
return String.format(
"%s;%s;%b;%s;%s;%s;!",
this.getName(),
this.getPath(),
this.isEnabled(),
this.getTypeName(),
this.getServerVersion(),
this.getMaxRam()
);
}
}