diff options
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | example.conf | 45 | ||||
-rwxr-xr-x | mcwrapper | 184 |
3 files changed, 120 insertions, 113 deletions
@@ -52,8 +52,8 @@ Status can be: If file not exists, then server is not running at all. ###Players file -This file in in status directory named as `players`. If server is running, it -contains online players. Player name per line. If server isn't running, it +This file in in status directory and is named as `players`. If server is running, +it contains online players. Player name per line. If server isn't running, it content don't have to be valid. ###Input pipe diff --git a/example.conf b/example.conf index a267735..9d656ea 100644 --- a/example.conf +++ b/example.conf @@ -1,29 +1,36 @@ -# This is exaple configuration for mcwrapper +# This is example configuration for mcwrapper # Use Python3 syntax to specify configuration. ############################################## +# vim: expandtab ft=python ts=4 sw=4 sts=4: # This is default identifier. It is used if no identifier is provided as argument # to mcwrapper. This is specially handy when you have only one identifier to be # run all the time. # Uncommenting this option if you want such feature. +# Type: string #identifier = 'exampleserver' +# Definition of Minecraft server. class exampleserver: - # Directory in which Minecraft server will be executed. It is where its files - # will be placed - directory = '~/minecraft', - # Command to start Minecraft server. It is executed in directory specified in - # option "directory". - # Suggested is to always append "nogui" no disable graphical interface. - command = "java -jar mcs.jar nogui", - # Directory where wrapper writes files signaling status of server and online - # players. - # In default it points to directory in /dev/shm. This means that files are in - # such case stored only in ram. - statusdir = '/dev/shm/mcwrapper-exampleserver', - # Automatic server shutdown when no player is online. This option defines time - # in minutes before that happens. It is measured from time of last player - # leaving server. - # Set this value to less or equal zero or comment it to disable automatic - # shutdown. - timeout = 15 + # Directory in which Minecraft server will be executed. It is where its files + # will be placed + # Type: string + directory = '~/minecraft' + # Command to start Minecraft server. It is executed in directory specified in + # option "directory". + # Suggested is to always append "nogui" no disable graphical interface. + # Type: string + command = "java -jar mcs.jar nogui" + # Directory where wrapper writes files signaling status of server and online + # players. + # In default it points to directory in /dev/shm. This means that files are in + # such case stored only in ram. + # Type: string + statusdir = '/dev/shm/mcwrapper-exampleserver' + # Automatic server shutdown when no player is online. This option defines time + # in minutes before that happens. It is measured from time of last player + # leaving server. + # Set this value to less or equal zero or comment it to disable automatic + # shutdown. + # Type: int + timeout = 15 @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# vim: expandtab ft=python ts=4 sw=4 sts=4: import os import sys import re @@ -19,30 +20,35 @@ _EC_ARG_MULTIPLE_CONFIG = 2 _EC_MISSING_CONFIGURATION = 10 _EC_SERVER_RUNNING = 11 -def info(message, minverbose = 0): +def __print_message__(message, file=sys.stdout, notime=False): + if notime: + print(message, file=file) + else: + print('[' + time.strftime('%H:%M:%S') + '] ' + message, file=file) + +def info(message, minverbose = 0, notime=False): "Prints message to stdout if minverbose >= verbose_level" try: if conf.verbose_level >= minverbose: - print(message) - except NameError: - print(message) + __print_message__(message, notime=notime) + except (NameError, TypeError): + __print_message__(message, notime=notime) -def warning(message, minverbose = -1): +def warning(message, minverbose = -1, notime=False): "Prints message to stderr if minverbose >= verbose_level" try: if conf.verbose_level >= minverbose: - print(message, file=sys.stderr) - except NameError: - print(message, file=sys.stderr) + __print_message__(message, file=sys.stderr, notime=notime) + except (NameError, TypeError): + __print_message__(message, file=sys.stderr, notime=notime) -def error(message, minverbose = -2, ec = -1): +def error(message, minverbose = -2, ec = -1, notime=False): "Prints message to stderr if minverbose >= verbose_level" try: if conf.verbose_level >= minverbose: - print(message, file=sys.stderr) - except NameError: - print(message, file=sys.stderr) - # TODO rather throw exception and handle it globally + __print_message__(message, file=sys.stderr, notime=notime) + except (NameError, TypeError): + __print_message__(message, file=sys.stderr, notime=notime) sys.exit(ec) ################################################################################# @@ -80,30 +86,47 @@ def load_conf(config_file): if not 'verbose_level' in vars(conf): conf.verbose_level = 0 -def conf_setserver(server): +def __conf_check_bad_type__(config): + error('Bad configuration type of configuration option: ' + config, + ec = _EC_MISSING_CONFIGURATION) +def __conf_check_missing__(config): + error('Missing configuration option: ' + config, + ec = _EC_MISSING_CONFIGURATION) +def __conf_check_no_dir__(directory): + error('No directory exists for configuration option: ' + directory, + ec = _EC_MISSING_CONFIGURATION) + +def conf_checkserver(server): "Check and set configuration for server specified as agument." try: srv = vars(conf)[server]; except KeyError: - error("No configuration class found for server: " + server, - ec = _EC_MISSING_CONFIGURATION) + error("No configuration class found", ec = _EC_MISSING_CONFIGURATION) if not 'timeout' in vars(srv): srv.timeout = 0 + if type(srv.timeout) != int: + __conf_check_bad_type__('timeout') if not 'directory' in vars(srv): - error('Missing "directory" config for server ' + server, - ec = _EC_MISSING_CONFIGURATION) + __conf_check_missing__('directory') + if type(srv.directory) != str: + __conf_check_bad_type__('directory') + srv.directory = os.path.expanduser(srv.directory) + if not os.path.isdir(srv.directory): + __conf_check_no_dir__('directory') if not 'command' in vars(srv): - error('Missing server start command for server ' + server, - ec = _EC_MISSING_CONFIGURATION) + __conf_check_missing__('command') + if type(srv.command) != str: + __conf_check_bad_type__('command') if not 'statusdir' in vars(srv): srv.statusdir = '/dev/shm/mcwrapper-' + server + if type(srv.statusdir) != str: + __conf_check_bad_type__('statusdir') + srv.statusdir = os.path.expanduser(srv.statusdir) return srv ################################################################################# # Minecraft server -_mcservers = [] - __STATUSSTRINGS__ = { 0: "Not running", 1: "Starting", @@ -112,21 +135,20 @@ __STATUSSTRINGS__ = { } class MCServer: - def __init__(self, identifier): - _mcservers.append(self) + def __init__(self, identifier, conf): self.identifier = identifier self.players = set() self.status = 0 - self.conf = conf_setserver(identifier) + self.conf = conf self.inputPipe = self.conf.statusdir + '/input_pipe' self.statusFile = self.conf.statusdir + '/status' self.playersFile = self.conf.statusdir + '/players' self.pidfile = self.conf.statusdir + '/server.pid' if type(self.conf.command) != str: self.conf.command = ' '.join(self.conf.command) - info(self.identifier + ": Server wrapper initializing") - info(self.identifier + ": Folder: " + self.conf.directory, 1) - info(self.identifier + ": Start command: " + self.conf.command, 1) + info("Server wrapper initializing") + info("Folder: " + self.conf.directory, 1) + info("Start command: " + self.conf.command, 1) try: os.mkdir(self.conf.statusdir) except FileExistsError: @@ -141,20 +163,21 @@ class MCServer: try: os.kill(lpid, 0) except OSError: - warning(self.identifier + ": Detected forced termination of " - "previous server wrapper instance.") + warning("Detected forced termination of previous server wrapper " + "instance.") else: - error(self.identifier + ": Another wrapper is running with given identifier.", + error("Another wrapper is running with given identifier.", -1, _EC_SERVER_RUNNING) with open(self.statusFile, 'w') as f: f.write(__STATUSSTRINGS__[0] + '\n') with open(self.playersFile, 'w') as f: pass - self.inputThread = __MCServerInputThread__(self) - self.outpuThread = __MCServerOutputThread__(self) - def __del__(self): - info(self.identifier + ": Server wrapper clean.") - _mcservers.remove(self) + self.inputThread = Thread(target=self.__input_thread__, + daemon = True) + self.outpuThread = Thread(target=self.__output_thread__, + daemon = True) + def clean(self): + info("Server wrapper clean.") try: os.remove(self.inputPipe) except FileNotFoundError: @@ -170,6 +193,10 @@ class MCServer: if os.path.isfile(self.pidfile): os.remove(self.pidfile) + def execstart(self): + "Start execution of server" + self.start() + self.prc.wait() def start(self): "Start Minecraft server" self.prc = subprocess.Popen(self.conf.command, stdin=subprocess.PIPE, @@ -180,12 +207,10 @@ class MCServer: self.status = 1 with open(self.statusFile, 'w') as f: f.write(__STATUSSTRINGS__[1] + '\n') - if self.inputThread.is_alive() or self.outpuThread.is_alive(): - # TODO throw exception - return - self.inputThread.start() - self.inputThread.wake() # Input thread is stuck in waiting for first line - self.outpuThread.start() + if not self.inputThread.is_alive(): + self.inputThread.start() + if not self.outpuThread.is_alive(): + self.outpuThread.start() def stop(self): if self.running(): self.prc.stdin.write(bytes("/stop\n", sys.getdefaultencoding())) @@ -200,37 +225,33 @@ class MCServer: def write_to_terminal(self, text): "Write to server terminal. If server not running it does nothing" if self.status == 2: - info(self.identifier + ": Input: " + text, 1) + info("Input: " + text, 1) self.prc.stdin.write(bytes(text, sys.getdefaultencoding())) self.prc.stdin.flush() return True else: return False - def join(self): - "Join execution untill server exits." - self.outpuThread.join() - self.inputThread.join() def __autoshutdown_enable__(self): if (self.conf.timeout > 0): - info(self.identifier + ": Automatic shutdown after " + str(self.conf.timeout) + " min.") + info("Automatic shutdown after " + str(self.conf.timeout) + " min.") self.shutdownTimeout = Timer(self.conf.timeout * 60.0, self.stop) self.shutdownTimeout.start(); def __autoshutdown_disable__(self): try: self.shutdownTimeout.cancel() del self.shutdownTimeout - info(self.identifier + ": Automatic shutdown disabled.") + info("Automatic shutdown disabled.") except AttributeError: pass def __user_join__(self, username): - info(self.identifier + ": User '" + username + "' joined server.") + info("User '" + username + "' joined server.") + self.players.add(username) with open(self.playersFile, 'a') as f: - self.players.add(username) f.write(username + '\n') self.__autoshutdown_disable__() def __user_leave__(self, username): - info(self.identifier + ": User '" + username + "' left server.") + info("User '" + username + "' left server.") self.players.remove(username) with open(self.playersFile, 'w') as f: f.writelines(self.players) @@ -241,13 +262,13 @@ class MCServer: def __parse_line__(self, line): if ': Done' in line: - info(self.identifier + ": Server start.") + info("Server start.") self.status = 2 with open(self.statusFile, 'w') as f: f.write(__STATUSSTRINGS__[2] + '\n') self.__autoshutdown_enable__() elif ': Stopping the server' in line: - info(self.identifier + ": Server stop.") + info("Server stop.") self.status = 3 with open(self.statusFile, 'w') as f: f.write(__STATUSSTRINGS__[3] + '\n') @@ -259,41 +280,21 @@ class MCServer: name = line[len('[00:00:00] [Server thread/INFO]: '):] name = name[:name.index(' ')] self.__user_leave__(name) - -class __MCServerOutputThread__(Thread): - def __init__(self, mcserver): - Thread.__init__(self, name='MCServerOutputThread:' + mcserver.identifier) - self.mcserver = mcserver - self.__stopread__ = False - def stop(self): - self.stopread = True - def run(self): - for linen in self.mcserver.prc.stdout: + def __output_thread__(self): + for linen in self.prc.stdout: line = linen.decode(sys.getdefaultencoding()) - info(self.mcserver.identifier + ": " + line.rstrip(), 2) - self.mcserver.__parse_line__(line.rstrip()) - self.mcserver.inputThread.stop() - self.mcserver.status = 0 - with open(self.mcserver.statusFile, 'w') as f: + info(line.rstrip(), 2, notime=True) + self.__parse_line__(line.rstrip()) + self.inputThread.stop() + self.status = 0 + with open(self.statusFile, 'w') as f: f.write(__STATUSSTRINGS__[0] + '\n') - -class __MCServerInputThread__(Thread): - def __init__(self, mcserver): - Thread.__init__(self, name='MCServerInputThread:' + mcserver.identifier) - self.mcserver = mcserver - self.stopread = False - def stop(self): - self.stopread = True - def wake(self): - with open(self.mcserver.inputPipe, 'w') as f: - f.write("\n") - f.flush() - def run(self): - with open(self.mcserver.inputPipe, 'r') as p: - while not self.stopread: + def __input_thread__(self): + with open(self.inputPipe, 'r') as p: + while True: ln = p.readline().rstrip() if ln: - self.mcserver.write_to_terminal(ln + "\n") + self.write_to_terminal(ln + "\n") else: time.sleep(3) @@ -303,13 +304,11 @@ class __MCServerInputThread__(Thread): def wrapper_atexit(): "This is called when wrapper is exiting" - for srv in _mcservers: - del srv + _mcserver.clean() def wrapper_toexit(): "This function is called when system signalizes that mcwrapper should exit" - for srv in _mcservers: - srv.stop() + _mcserver.stop() def __signal_term__(_signo, _stack_frame): wrapper_toexit() @@ -399,10 +398,11 @@ if __name__ == '__main__': elif not "identifier" in vars(conf): print_help() + server_conf = conf_checkserver(conf.identifier) + global _mcserver + _mcserver = MCServer(conf.identifier, server_conf) signal.signal(signal.SIGTERM, __signal_term__) signal.signal(signal.SIGINT, __signal_term__) atexit.register(wrapper_atexit) - mcs = MCServer(conf.identifier) - mcs.start() - mcs.join() + _mcserver.execstart() |