From ec37e36a622614ac233d986911889bac1e59fc46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karel=20Ko=C4=8D=C3=AD?= Date: Mon, 7 Mar 2016 10:35:24 +0100 Subject: Cleaner output, input and output thread runs as daemons Output printing is now more standardized across script and allows defined Minecraft server like output. Input and output thread for server is now implemented more simple as daemons and wrapper exit is handled by waiting for server exit. Example configuration is little bit changed. It adds type information for all options. Vim configuration line added for both files (mcwrapper and example.conf). --- README.md | 4 +- example.conf | 45 +++++++++------ mcwrapper | 184 +++++++++++++++++++++++++++++------------------------------ 3 files changed, 120 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index a9c7599..f09895b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/mcwrapper b/mcwrapper index fda83fa..1a2d2f0 100755 --- a/mcwrapper +++ b/mcwrapper @@ -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() -- cgit v1.2.3