All checks were successful
KnarCraft/Minecraft-Server-Launcher/pipeline/head This commit looks good
582 lines
18 KiB
Java
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()
|
|
);
|
|
}
|
|
} |