Source code for emsm.core.server

#!/usr/bin/env python3

# The MIT License (MIT)
#
# Copyright (c) 2014-2018 <see AUTHORS.txt>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


# Modules
# ------------------------------------------------

# std
import os
import shlex
import shutil
import urllib.request
import logging
import subprocess
import re
import tempfile
import glob

# third party
import blinker
import yaml


# Backward compatibility
# --------------------------------------------------

if not hasattr(shlex, "quote"):
    # From the Python3.3 library
    _find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.ASCII).search
    def _shlex_quote(s):
        """Return a shell-escaped version of the string *s*."""
        if not s:
            return "''"
        if _find_unsafe(s) is None:
            return s

        # use single quotes, and put single quotes into double quotes
        # the string $'b is then quoted as '$'"'"'b'
        return "'" + s.replace("'", "'\"'\"'") + "'"
    shlex.quote = _shlex_quote

try:
    FileNotFoundError
except:
    FileNotFoundError = OSError


# Data
# ------------------------------------------------

__all__ = ["ServerError",
           "ServerInstallationFailure",
           "ServerStatusError",
           "ServerIsOnlineError",
           "ServerIsOfflineError",
           "BaseServerWrapper",
           "ServerManager"
           ]

log = logging.getLogger(__file__)


# Exceptions
# --------------------------------------------------

[docs]class ServerError(Exception): """ Base class for all exceptions in this module. """ pass
[docs]class ServerInstallationFailure(ServerError): """ Raised if a server installation failed. """ def __init__(self, server, msg=None): self.server = server self.msg = str(msg) return None def __str__(self): temp = "The installation of the server '{}' failed."\ .format(self.server.name()) if self.msg is not None: temp += " " + self.msg return temp
[docs]class ServerStatusError(ServerError): """ Raised if the server should be online/offline for an action but is offline/online. """ def __init__(self, server, status, msg = str()): self.server = server self.status = status self.msg = str(msg) return None def __str__(self): if self.status: temp = "The server '{}' is online. {}" else: temp = "The server '{}' is offline. {}" temp = temp.format(self.server.name(), self.msg) return temp
[docs]class ServerIsOnlineError(ServerStatusError): """ Raised if the server is online but should be offline. """ def __init__(self, server, msg = str()): ServerStatusError.__init__(self, server, True, msg) return None
[docs]class ServerIsOfflineError(ServerStatusError): """ Raised if the server is offline but should be online. """ def __init__(self, server, msg = str()): ServerStatusError.__init__(self, server, False, msg) return None
# Classes # ------------------------------------------------
[docs]class BaseServerWrapper(object): """ Wraps a minecraft server (executable), NOT a world. The BaseServerWrapper is initialized using the options in the :file:`server.conf` configuration file. :param emsm.core.application.Application app: The parent EMSM application """
[docs] @classmethod def name(cls): """ **ABSTRACT** The unique name of the server. **Example**: ``"vanilla 1.8"`` """ raise NotImplementedError()
def __init__(self, app): """ """ log.info("initialising server '{}' ...".format(self.name())) self.__app = app # Absolute path to the directory which should contain all server # software. # For the vanilla server, this is only a single file # *minecraft_server.jar*. The forge server comes with some # subdirectories and libraries. self.__directory = app.paths().server_(self.name()) # The configuration section in the *server.conf* configuration file. if not app.conf().server().has_section(self.name()): app.conf().server().add_section(self.name()) self.__conf = app.conf().server()[self.name()] return None
[docs] def directory(self): """ Absolute path to the directory which contains all server software. """ if not os.path.exists(self.__directory): os.makedirs(self.__directory) return self.__directory
[docs] def exe_path(self): """ **ABSTRACT** Absolute path to the server executable. This file is usually located in :meth:`directory`. """ raise NotImplementedError()
[docs] def conf(self): """ Returns the configuration section in the *server.conf* configuration file. """ return self.__conf
[docs] def default_url(self): """ **ABSTRACT** The URL where the server executable can be downloaded from. """ raise NotImplementedError()
[docs] def url(self): """ Returns the url in :meth:`conf`, if available. Otherwise the value of :meth:`default_url`. """ if "url" in self.conf(): return self.conf().get("url") return self.default_url()
[docs] def is_installed(self): """ ``True`` if the executable has been downloaded and exists, otherwise ``False``. Per default, this method only checks if the :meth:`directory` is empty or not. It can be *overridden* for a more detailed check. """ return bool(os.listdir(self.directory()))
[docs] def is_online(self): """ Returns ``True`` if at least one world is currently running with this server. """ worlds = self.__app.worlds().get_by_pred( lambda w: w.server() is self and w.is_online() ) return bool(worlds)
[docs] def install(self): """ **ABSTRACT** Installs the server by downloading it to :meth:`server`. If the server is already installed, nothing should happen. This method is called during the EMSM start phase if :meth:`is_installed` returns ``False``. :raises ServerInstallationFailure: * when the installation failed. """ raise NotImplementedError()
[docs] def reinstall(self): """ Tries to reinstall the server. If the reinstallation fails, the old :meth:`server()` is restored and everything is like before. :raises ServerInstallationFailure: * when the installation failed. :raises ServerIsOnlineError: * when a world powered by this server software is online. """ if self.is_online(): raise ServerIsOnlineError(self) # Save the old directory in a temporary folder, so that we can restore # it if something fails. with tempfile.TemporaryDirectory() as tmp_dir: tmp_server_path = shutil.move(self.directory(), tmp_dir) # is_installed() returns now False. assert not self.is_installed() # So we can call install() again. try: self.install() except: # Clean up the installation target directory self.directory() # and move the old server path back. if os.path.exists(self.directory()): if os.path.isdir(self.directory()): shutil.rmtree(self.directory()) else: # This is a relict of version 3, when not all server # created a directory in ``server/``. os.remove(self.directory()) shutil.move(tmp_server_path, self.directory()) # Reraise the original exception. raise return None
[docs] def default_start_cmd(self): """ **ABSTRACT** Returns the bash command string, that must be executed, to start the server. If there are paths in the returned command, they must be absolute. """ raise NotImplementedError()
[docs] def start_cmd(self, world=None): """ Returns the value for *start_command* in :meth:`conf` if available and the :meth:`default_start_cmd` otherwise. :arg str world: The name of an EMSM world. The start command can be overridden for each world. We will look for a custom start command in the worlds configuration file first. """ cmd = "" # 1.) morpheus.world.conf if world: world_conf = self.__app.conf().world(world) if world_conf.has_option("server:" + self.name(), "start_command"): cmd = world_conf["server:" + self.name()]["start_command"] # 2.) server.conf if not cmd: cmd = self.conf().get("start_command") # 3.) fallback if not cmd: cmd = self.default_start_cmd() cmd = cmd.format(server_exe = shlex.quote(self.exe_path())) return cmd
[docs] def translate_command(self, cmd): """ **ABSTRACT** Translates the vanilla server command *cmd* to a command with the same meaning, but which can be understood by the server. **Example:** .. code-block:: python >>> # A BungeeCoord wrapper would do this: >>> bungeecord.translate_command("stop") "end" >>> bungeecord.translate_command("say Hello World!") "alert Hello World!" """ return NotImplementedError()
[docs] def log_path(cls, self): """ **ABSTRACT** Returns the path of the server log file of a world. If a relative path is returned, the base path is the world directory. """ raise NotImplementedError()
[docs] def log_start_re(self): """ **ABSTRACT** Returns a regex, that matches the first line in the log file, after a server restart. """ raise NotImplementedError()
[docs] def log_error_re(self): """ **ABSTRACT** Returns a regex, that matches every line with a *severe* (critical) error. A severe error means, that the server does not run correct and needs to be restarted. """ raise NotImplementedError()
[docs] def world_address(self, world): """ **ABSTRACT** Returns the address (ip, port) which is binded by the world. (None, None) should be returned, if the binding can not be retrieved. If the server is binded to all ip addresses, return the emtpy string ``""`` for the ip address. The port should be returned as integer. If it can not be retrieved, return None. """ raise NotImplementedError()
# Vanilla # ''''''' class VanillaBase(BaseServerWrapper): """ Base class for all vanilla server versions. """ def exe_path(self): return os.path.join(self.directory(), self.name()) def default_start_cmd(self): return "java -jar {} nogui".format(shlex.quote(self.exe_path())) def translate_command(self, cmd): return cmd def install(self): """ """ if self.is_installed(): return None # Simply download the minecraft jar from mojang and copy the .jar in # the EMSM_ROOT/server directory. try: tmp_path, http_resp = urllib.request.urlretrieve(self.url()) except Exception as err: raise ServerInstallationFailure(self, err) else: shutil.move(tmp_path, self.exe_path()) return None def world_address(self, world): """ """ # Read the server.properties file in the world's directory. conf_path = os.path.join(world.directory(), "server.properties") try: with open(conf_path, "r") as file: conf = file.read() except (OSError, IOError) as err: port = None ip = None else: # Retrieve the values for *server-port* and *server-ip*. # Note, that we use the '^' and '$' chars, so that we only match # valid lines. If there is a syntax error in the configuration, # we will ignore the value and return None instead. port_re = "^server-port\s*=\s*([\d]{1,5})\s*$" port = re.findall(port_re, conf, re.MULTILINE) port = int(port[0]) if port else None ip_re = "^server-ip\s*=\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s*$" ip = re.findall(ip_re, conf, re.MULTILINE) ip = ip[0] if ip else "localhost" return (ip, port) class Vanilla_1_2(VanillaBase): @classmethod def name(self): return "vanilla 1.2" def default_url(self): return "http://s3.amazonaws.com/Minecraft.Download/versions/1.2.5/minecraft_server.1.2.5.jar" def log_path(self): return "./server.log" def log_start_re(self): return re.compile("^.*Starting minecraft server version 1\.2.*") def log_error_re(self): return re.compile(".* \[SEVERE\] .*", re.MULTILINE) class Vanilla_1_3(VanillaBase): @classmethod def name(self): return "vanilla 1.3" def default_url(self): return "http://s3.amazonaws.com/Minecraft.Download/versions/1.3.2/minecraft_server.1.3.2.jar" def log_path(self): return "./server.log" def log_start_re(self): return re.compile("^.*Starting minecraft server version 1\.3.*") def log_error_re(self): return re.compile(".* \[SEVERE\] .*", re.MULTILINE) class Vanilla_1_4(VanillaBase): @classmethod def name(self): return "vanilla 1.4" def default_url(self): return "http://s3.amazonaws.com/Minecraft.Download/versions/1.4.7/minecraft_server.1.4.7.jar" def log_path(self): return "./server.log" def log_start_re(self): return re.compile("^.*Starting minecraft server version 1\.4.*") def log_error_re(self): return re.compile(".* \[SEVERE\] .*", re.MULTILINE) class Vanilla_1_5(VanillaBase): @classmethod def name(self): return "vanilla 1.5" def default_url(self): return "http://s3.amazonaws.com/Minecraft.Download/versions/1.5.2/minecraft_server.1.5.2.jar" def log_path(self): return "./server.log" def log_start_re(self): return re.compile("^.*Starting minecraft server version 1\.5.*") def log_error_re(self): return re.compile(".* \[SEVERE\] .*", re.MULTILINE) class Vanilla_1_6(VanillaBase): @classmethod def name(self): return "vanilla 1.6" def default_url(self): return "https://s3.amazonaws.com/Minecraft.Download/versions/1.6.4/minecraft_server.1.6.4.jar" def log_path(self): return "./server.log" def log_start_re(self): return re.compile("^.*Starting minecraft server version 1\.6.*") def log_error_re(self): return re.compile(".* \[SEVERE\] .*", re.MULTILINE) class Vanilla_1_7(VanillaBase): @classmethod def name(self): return "vanilla 1.7" def default_url(self): return "https://s3.amazonaws.com/Minecraft.Download/versions/1.7.10/minecraft_server.1.7.10.jar" def log_path(self): return "./logs/latest.log" def log_start_re(self): return re.compile("^.*Starting minecraft server version 1\.7.*") def log_error_re(self): return re.compile(".* \[SEVERE\] .*", re.MULTILINE) class Vanilla_1_8(VanillaBase): @classmethod def name(self): return "vanilla 1.8" def default_url(self): return "https://s3.amazonaws.com/Minecraft.Download/versions/1.8.9/minecraft_server.1.8.9.jar" def log_path(self): return "./logs/latest.log" def log_start_re(self): return re.compile("^.*Starting minecraft server version 1\.8.*") def log_error_re(self): return re.compile(".* \[SEVERE\] .*", re.MULTILINE) class Vanilla_1_9(VanillaBase): @classmethod def name(self): return "vanilla 1.9" def default_url(self): return "https://s3.amazonaws.com/Minecraft.Download/versions/1.9/minecraft_server.1.9.jar" def log_path(self): return "./logs/latest.log" def log_start_re(self): return re.compile("^.*Starting minecraft server version 1\.9.*") def log_error_re(self): return re.compile(".* \[SEVERE\] .*", re.MULTILINE) class Vanilla_1_10(VanillaBase): @classmethod def name(self): return "vanilla 1.10" def default_url(self): return "https://s3.amazonaws.com/Minecraft.Download/versions/1.10.2/minecraft_server.1.10.2.jar" def log_path(self): return "./logs/latest.log" def log_start_re(self): return re.compile("^.*Starting minecraft server version 1\.10.*") def log_error_re(self): return re.compile(".* \[SEVERE\] .*", re.MULTILINE) class Vanilla_1_11(VanillaBase): @classmethod def name(self): return "vanilla 1.11" def default_url(self): return "https://s3.amazonaws.com/Minecraft.Download/versions/1.11.2/minecraft_server.1.11.2.jar" def log_path(self): return "./logs/latest.log" def log_start_re(self): return re.compile("^.*Starting minecraft server version 1\.11.*") def log_error_re(self): return re.compile(".* \[SEVERE\] .*", re.MULTILINE) class Vanilla_1_12(VanillaBase): @classmethod def name(self): return "vanilla 1.12" def default_url(self): return "https://s3.amazonaws.com/Minecraft.Download/versions/1.12.1/minecraft_server.1.12.1.jar" def log_path(self): return "./logs/latest.log" def log_start_re(self): return re.compile("^.*Starting minecraft server version 1\.12.*") def log_error_re(self): return re.compile(".* \[SEVERE\] .*", re.MULTILINE) class Vanilla_1_13(VanillaBase): @classmethod def name(self): return "vanilla 1.13" def default_url(self): return "https://launcher.mojang.com/mc/game/1.13.1/server/fe123682e9cb30031eae351764f653500b7396c9/server.jar" def log_path(self): return "./logs/latest.log" def log_start_re(self): return re.compile("^.*Starting minecraft server version 1\.13.*") def log_error_re(self): return re.compile(".* \[SEVERE\] .*", re.MULTILINE) # MinecraftForge # '''''''''''''' class MinecraftForgeBase(BaseServerWrapper): """ Base class for the minecraft forge server. Todo: * check if :meth:`log_error_re` is correct implemented. """ def translate_command(self, cmd): return cmd def install(self): """ """ if self.is_installed(): return None try: # We need to download the *installer* first. try: tmp_path, http_resp = urllib.request.urlretrieve(self.url()) except Exception as err: raise ServerInstallationFailure(self, err) from err else: # Now, we have to run the installer. # Clear the server directory. shutil.rmtree(self.directory()) if not os.path.exists(self.directory()): os.makedirs(self.directory()) os.chdir(self.directory()) if not os.path.samefile(self.directory(), os.curdir): msg = "Could not chdir to {}".format(self.directory()) raise ServerInstallationFailure(self, msg) sys_install_cmd = "java -jar {} --installServer"\ .format(tmp_path) sys_install_cmd = shlex.split(sys_install_cmd) try: p = subprocess.Popen( sys_install_cmd, stdout = subprocess.PIPE, stderr = subprocess.PIPE ) # Store the output of the installer in the logfiles. out, err = p.communicate() out = out.decode() err = err.decode() log.info(out) log.warning(err) # Check, if the installer exited with return code 0 and # throw an exception if not. if p.returncode: msg = "Installer returned with '{}'."\ .format(p.returncode) raise ServerInstallationFailure(self, msg) except Exception as err: raise ServerInstallationFailure(self, err) from err except: # Try to undo the installation. if os.path.exists(self.directory()): shutil.rmtree(self.directory()) raise return None def default_start_cmd(self): start_cmd = "java -jar {} nogui".format(shlex.quote(self.exe_path())) return start_cmd class MinecraftForge_1_6(MinecraftForgeBase, Vanilla_1_6): @classmethod def name(self): return "minecraft forge 1.6" def default_url(self): return "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.6.4-9.11.1.1345/forge-1.6.4-9.11.1.1345-installer.jar" def exe_path(self): filenames = [filename \ for filename in os.listdir(self.directory()) \ if re.match("^minecraftforge-universal-1\.6.*.jar$", filename)] filename = filenames[0] return os.path.join(self.directory(), filename) class MinecraftForge_1_7(MinecraftForgeBase, Vanilla_1_7): @classmethod def name(self): return "minecraft forge 1.7" def default_url(self): return "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.7.10-10.13.4.1614-1.7.10/forge-1.7.10-10.13.4.1614-1.7.10-installer.jar" def exe_path(self): filenames = [filename \ for filename in os.listdir(self.directory()) \ if re.match("^forge-1\.7.*.jar$", filename)] filename = filenames[0] return os.path.join(self.directory(), filename) class MinecraftForge_1_8(MinecraftForgeBase, Vanilla_1_8): @classmethod def name(self): return "minecraft forge 1.8" def default_url(self): return "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.8.9-11.15.1.1722/forge-1.8.9-11.15.1.1722-installer.jar" def exe_path(self): filenames = [filename \ for filename in os.listdir(self.directory()) \ if re.match("^forge-1\.8.*.jar$", filename)] filename = filenames[0] return os.path.join(self.directory(), filename) class MinecraftForge_1_10(MinecraftForgeBase, Vanilla_1_10): @classmethod def name(self): return "minecraft forge 1.10" def default_url(self): return "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.10.2-12.18.0.2008/forge-1.10.2-12.18.0.2008-installer.jar" def exe_path(self): filenames = [filename \ for filename in os.listdir(self.directory()) \ if re.match("^forge-1\.10.*.jar$", filename)] filename = filenames[0] return os.path.join(self.directory(), filename) class MinecraftForge_1_11(MinecraftForgeBase, Vanilla_1_11): @classmethod def name(self): return "minecraft forge 1.11" def default_url(self): return "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.11.2-13.20.0.2228/forge-1.11.2-13.20.0.2228-installer.jar" def exe_path(self): filenames = [filename \ for filename in os.listdir(self.directory()) \ if re.match("^forge-1\.11.*.jar$", filename)] filename = filenames[0] return os.path.join(self.directory(), filename) class MinecraftForge_1_12(MinecraftForgeBase, Vanilla_1_12): @classmethod def name(self): return "minecraft forge 1.12" def default_url(self): return "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.12-14.21.1.2405/forge-1.12-14.21.1.2405-installer.jar" def exe_path(self): filenames = [filename \ for filename in os.listdir(self.directory()) \ if re.match("^forge-1\.12.*.jar$", filename)] filename = filenames[0] return os.path.join(self.directory(), filename) # Bungeecord # '''''''''' class BungeeCordServerWrapper(BaseServerWrapper): """ Wraps only the **latest** BungeeCord version. Unfortunetly, the BungeeCord server uses the git commit hash value as its version number. So it would be too much work to keep track of the versions. """ @classmethod def name(self): return "bungeecord" def default_url(self): return "http://ci.md-5.net/job/BungeeCord/lastSuccessfulBuild/artifact/bootstrap/target/BungeeCord.jar" def exe_path(self): return os.path.join(self.directory(), self.name()) def install(self): """ """ if self.is_installed(): return None # Simply download the latest build and save it in the EMSM_ROOT/server # directory. try: tmp_path, http_resp = urllib.request.urlretrieve(self.url()) except Exception as err: raise ServerInstallationFailure(err) from err else: shutil.move(tmp_path, self.exe_path()) return None def default_start_cmd(self): return "java -jar {}".format(shlex.quote(self.exe_path())) def translate_command(self, cmd): cmd = cmd.strip() if cmd.startswith("say "): cmd = "alert " + cmd[len("say "):] elif cmd == "stop": cmd = "end" return cmd def log_path(self): return "./proxy.log.0" def log_start_re(self): return re.compile("^.*Enabled BungeeCord version git:.*") def log_error_re(self): return re.compile(".* \[SEVERE\] .*", re.MULTILINE) def world_address(self, world): """ """ # Try to read and parse the configuration. # Note that it may not exist, not readable or parseable. conf_path = os.path.join(world.directory(), "config.yml") try: with open(conf_path) as file: conf = yaml.load(file) except (OSError, IOError) as err: ip, port = (None, None) else: # Try to extract the ip and port. try: adr = conf["listeners"][0]["host"] ip, port = adr.split(":") except: ip, port = (None, None) else: # Check if the ip address is valid. ip = ip.strip() if not re.match("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", ip): ip = None # Check if the server is bounded to all available ip addresses. if ip == "0.0.0.0": ip = "localhost" # Check if the port is valid and convert it to an int. port = port.strip() port = int(port) if re.match("^\d{1,5}$", port) else None return (ip, port) # Spigot (1.8+) # ''''''''''''' class SpigotBase(BaseServerWrapper): """ Wrapper for the Spigot build tool. """ @classmethod def revision(cls): """The revision number, e.g. ``latest`` or ``1.13.1``. This value is passed as **--rev** parameter to the build tool. """ raise NotImplementedError() @classmethod def name(cls): return "spigot {}".format(cls.revision()) def default_url(self): return "https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar" def build_dir(self): """ You can specify a build directory in the :file:`server.conf`. If not specified, we will use a temporary directory: .. code-block:: ini [spigot latest] build_dir = /path/to/my/build/dir """ if "build_dir" in self.conf(): tmp = self.conf().get("build_dir") tmp = os.path.expanduser(tmp) tmp = os.path.abspath(tmp) return tmp return tempfile.mkdtemp(prefix='spigotmc') def install(self): if self.is_installed(): return None # Download and build in /tmp build_dir = self.build_dir() log.info("Installing spigot ...") log.info("- Building in '{}' ...".format(build_dir)) try: # Download the build tools. try: buildtools, http_resp = urllib.request.urlretrieve( self.url(), os.path.join(build_dir, "BuildTools.jar") ) except Exception as err: raise ServerInstallationFailure(self, err) from err log.info("- BuildTools: '{}' ...".format(buildtools)) # Run the BuildTools.jar file. with subprocess.Popen( ["java", "-jar", buildtools, "--rev", self.revision()], cwd = build_dir, stdout = subprocess.PIPE, stderr = subprocess.PIPE ) as proc: # Store the output of the installer in the logfiles. out, err = proc.communicate() out = out.decode() err = err.decode() log.info(out) log.error(err) # Check, if the installer exited with return code 0 and # throw an exception if not. if proc.returncode: msg = "Installer returned with '{}'."\ .format(proc.returncode) raise ServerInstallationFailure(self, msg) # Move the built files to *exe_path()*. jarfiles = glob.glob( os.path.join(build_dir, "Spigot/Spigot-Server/target/spigot-*.jar") ) if len(jarfiles) > 0: jarfile = jarfiles[0] shutil.move(jarfile, self.exe_path()) else: msg = "Could not find built spigot-*.jar file." raise ServerInstallationFailure(self, msg) finally: # Remove the build directory, if it's only a temporary dir. if "build_dir" not in self.conf(): log.info("- Removing build directory {}".format(build_dir)) shutil.rmtree(build_dir) return None def exe_path(self): return os.path.join(self.directory(), self.name()) def default_start_cmd(self): return "java -jar {}".format(shlex.quote(self.exe_path())) def log_path(self): return "./logs/latest.log" def log_start_re(self): return re.compile("^.*Starting minecraft server version .*") def log_error_re(self): """ Todo: Check if this regex is correct and matches an error line. """ return re.compile(".*/SEVERE\].*", re.MULTILINE) def translate_command(self, cmd): return cmd def world_address(self, world): """ """ # Read the server.properties file in the world's directory. conf_path = os.path.join(world.directory(), "server.properties") try: with open(conf_path, "r") as file: conf = file.read() except (OSError, IOError) as err: port = None ip = None else: # Retrieve the values for *server-port* and *server-ip*. # Note, that we use the '^' and '$' chars, so that we only match # valid lines. If there is a syntax error in the configuration, # we will ignore the value and return None instead. port_re = "^server-port\s*=\s*([\d]{1,5})\s*$" port = re.findall(port_re, conf, re.MULTILINE) port = int(port[0]) if port else None ip_re = "^server-ip\s*=\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s*$" ip = re.findall(ip_re, conf, re.MULTILINE) ip = ip[0] if ip else "localhost" return (ip, port) class Spigot(SpigotBase): @classmethod def revision(cls): return "latest" class Spigot_1_8(SpigotBase): @classmethod def revision(cls): return "1.8" class Spigot_1_9(SpigotBase): @classmethod def revision(cls): return "1.9" class Spigot_1_10(SpigotBase): @classmethod def revision(cls): return "1.10" class Spigot_1_11(SpigotBase): @classmethod def revision(cls): return "1.11" class Spigot_1_12(SpigotBase): @classmethod def revision(cls): return "1.12" class Spigot_1_13(SpigotBase): @classmethod def revision(cls): return "1.13" # MC-Server # ''''''''' class MCServer(BaseServerWrapper): pass # Server DB # ------------------------------------------------
[docs]class ServerManager(object): """ Manages all server wrappers, owned by an EMSM application. The ServerManager helps to avoid double instances of the same server wrapper. """ def __init__(self, app): """ """ self._app = app # Maps *server.name()* to *server* self._server = dict() self.__add_emsm_wrapper() return None def __add_emsm_wrapper(self): """ Loads all default EMSM server wrappers. These are all server wrappers defined in this module. """ # We only add the complete wrappers. These wrappers have all # virtual (abstract) methods implemented. wrappers = [ Vanilla_1_2, Vanilla_1_3, Vanilla_1_4, Vanilla_1_5, Vanilla_1_6, Vanilla_1_7, Vanilla_1_8, Vanilla_1_9, Vanilla_1_10, Vanilla_1_11, Vanilla_1_12, Vanilla_1_13, MinecraftForge_1_6, MinecraftForge_1_7, MinecraftForge_1_8, MinecraftForge_1_10, MinecraftForge_1_11, MinecraftForge_1_12, BungeeCordServerWrapper, Spigot, Spigot_1_8, Spigot_1_9, Spigot_1_10, Spigot_1_11, Spigot_1_12, Spigot_1_13 ] for wrapper in wrappers: self.add(wrapper) return None
[docs] def add(self, server_class): """ Makes the *server_class* visible to this manager. The class must implement all abstract methods of :class:`BaseServerWrapper` or unexpected errors may occure. :raises TypeError: if *server_class* does not inherit :class:`BaseServerWrapper` :raises ValueError: if another wrapper with the :meth:`~BaseServerWrapper.name()` of *server_class* has already been registered. """ if not issubclass(server_class, BaseServerWrapper): raise TypeError("server_class has to inherit from BaseServerWrapper") if server_class.name() in self._server: raise ValueError("another server with the name '{}' has already "\ "been registered.".format(server_class.name())) # Create a new instance of the server wrapper. self._server[server_class.name()] = server_class(self._app) return None
[docs] def get(self, servername): """ Returns the :class:`ServerWrapper` with the name *servername* and ``None``, if there is not such a server. """ return self._server.get(servername)
[docs] def get_all(self): """ Returns a list with all loaded :class:`ServerWrapper`. """ return list(self._server.values())
[docs] def get_by_pred(self, pred=None): """ Almost equal to: .. code-block:: python >>> filter(pred, ServerManager.get_all()) ... """ return list(filter(pred, self._server.values()))
[docs] def get_selected(self): """ Returns all server that have been selected per command line argument. """ args = self._app.argparser().args() selected_server = args.server all_server = args.all_server if all_server: return list(self._server.values()) else: return [self._server[server] for server in selected_server]
[docs] def get_names(self): """ Returns a list with the names of all server. """ return list(self._server.keys())