diff options
-rw-r--r-- | README.md | 99 | ||||
-rw-r--r-- | example.conf | 33 | ||||
-rwxr-xr-x | mcwrapper | 434 | ||||
-rwxr-xr-x | tests/prepare.sh | 3 | ||||
-rwxr-xr-x | tests/t_codingstandard.sh | 2 |
5 files changed, 180 insertions, 391 deletions
@@ -1,8 +1,8 @@ -MINECRAFT-WRAPPER -================= +MCSERVER-WRAPPER +================ [![Build Status](https://travis-ci.org/Cynerd/minecraft-wrapper.svg?branch=master)](https://travis-ci.org/Cynerd/minecraft-wrapper) -Python server wrapper for extracting informations about server status and list of -online players. +Minecraft server wrapper written in Python3 that extracts server status and list +of online players. Requires: ----------------- @@ -10,72 +10,63 @@ Requires: * Python3 * Dependencies for Minecraft server (Java) -Usage ------------------ +MCWRAPPER +--------- +### Usage ``` -mcwrapper [arguments...] IDENTIFIER - This script is executing Minecraft server and reads its output. From output is - extracted server status and list of online players. +mcwrapper [-h] [--verbose] [--quiet] [--status-file] [--players-file] + ... + +This script is executing Minecraft server and reads its output. From output is +extracted server status and list of online players. And standard input can be +accessed by fifo file. - arguments - -h, --help - Prints this help text. - -v, --verbose - Increase verbose level of output. - -q, --quiet - Decrease verbose level of output. - --config CONFIG_FILE - Specify configuration file to be used. - --configfile - prints used configuration file and exits. +positional arguments: + command Command to be executed to start Minecraft server. - IDENTIFIER - Identifier for new server. This allows multiple servers running with this - wrapper. Identifier is word without spaces and preferably without special - characters. +optional arguments: + -h, --help show this help message and exit + --verbose, -v Increase verbose level of output + --quiet, -q Decrease verbose level of output + --status-file, -s Outputs server status to file "status" + --players-file, -p Outputs list of online players to file "players" ``` -How it works ------------------ +### How it works Script is reading Minercraft server standard and error output. It's looking for -known lines that signals change of server output and players joining and leaving. +known lines that signals change of server status and players joining and leaving. Minecraft server output is well designed for information parsing. Informations are -in exported to directory specified in configuration as `status`. +exported to directory working directory or websocket server. + +#### Input pipe +This is unix pipe. This file is located in working directory and named as +`input_pipe`. This pipe is input to Minercraft server standard input. If you have +write access rights (default 640), then you can send any command to Minecraft +server by writing to this pipe. + +#### Server.pid file +This file contains PID of Minecraft server process. This is used to detect if +server is running when status files exists. It has probably no usage for user, but +shouldn't be tempered with. -###Status file -This file is in status directory named as `status`. If it exists, it specifies in -what status is server in the moment. +#### Status file +This file is in working directory and is named as `status`. If it exists, it +specifies in what status is server in the moment. Status can be: + * Starting * Running * Stopping If file not exists, then server is not running at all. -###Players file +#### Players file 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 -This is unix pipe. This file is located in status directory and named as -`input_pipe`. This pipe is input to Minercraft server standard input. If you have -write access rights (default 640), then you can send any command to Minecraft -server by writing to this pipe. - -###Server.pid file -This file contains PID of Minecraft server process. This is used to detect if -server is running when status files exists. It has probably no usage for user, but -shouldn't be tempered with. - -Configuration ------------------ -You can use `example.conf` as base configuration. Configuration file is in fact -Python3 script that is executed and its variables are used as configuration. -Script is searching for configuration in these files (in order of precedence): - * mcwrapper.conf - * mcwrapper.conf - * ~/.mcwrapper.conf - * ~/.config/mcwrapper.conf - * /etc/mcwrapper.conf -Or you can use `--config` argument to specify any other file with valid content. +MCWRAPPER-TERMINAL +------------------ +This application is going to be used for interactive terminal access to minecraft +server console. It should use latest minecraft server log as input and output will +be pushed to input pipe of mcwrapper. This app is currently in development. diff --git a/example.conf b/example.conf deleted file mode 100644 index 42e4394..0000000 --- a/example.conf +++ /dev/null @@ -1,33 +0,0 @@ -# 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. -# 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 - # Type: string - directory = 'minecraft-server' - # 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 minecraft_server.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 @@ -5,18 +5,12 @@ import sys import subprocess import signal import time -import traceback import atexit +import argparse from threading import Thread -from threading import Timer -import importlib.machinery as imp ############################################################################### # Exit codes and prints helpers -_EC_OK = 0 -_EC_ARG_UNKNOWN = 1 -_EC_ARG_MULTIPLE_CONFIG = 2 -_EC_MISSING_CONFIGURATION = 10 -_EC_SERVER_RUNNING = 11 +verbose_level = 0 def __print_message__(message, file=sys.stdout, notime=False): @@ -28,115 +22,23 @@ def __print_message__(message, file=sys.stdout, notime=False): def info(message, minverbose=0, notime=False): "Prints message to stdout if minverbose >= verbose_level" - try: - if conf.verbose_level >= minverbose: - __print_message__(message, notime=notime) - except (NameError, TypeError): + if verbose_level >= minverbose: __print_message__(message, notime=notime) def warning(message, minverbose=-1, notime=False): "Prints message to stderr if minverbose >= verbose_level" - try: - if conf.verbose_level >= minverbose: - __print_message__(message, file=sys.stderr, notime=notime) - except (NameError, TypeError): + if verbose_level >= minverbose: __print_message__(message, file=sys.stderr, notime=notime) -def error(message, minverbose=-2, ec=-1, notime=False): +def error(message, minverbose=-2, errcode=-1, notime=False): "Prints message to stderr if minverbose >= verbose_level" - try: - if conf.verbose_level >= minverbose: - __print_message__(message, file=sys.stderr, notime=notime) - except (NameError, TypeError): + if verbose_level >= minverbose: __print_message__(message, file=sys.stderr, notime=notime) - sys.exit(ec) + sys.exit(errcode) ############################################################################### -# Load configuration - -__all_config_files__ = ( - 'mcwrapper.conf', - '~/.mcwrapper.conf', - '~/.config/mcwrapper.conf', - '/etc/mcwrapper.conf', - ) - - -def load_conf(config_file): - """Load config_file to conf variable. Or if it has value None, search on - default paths""" - global conf - - def __set_empty_config__(): - global conf - warning('User configuration not loaded. Using default.') - conf = type('default config', (object,), {}) - if config_file is None: - # Find configuration in predefined paths - for cf in __all_config_files__: - if os.path.isfile(os.path.expanduser(cf)): - config_file = os.path.expanduser(cf) - break - if config_file is None: # If no configuration find. Set empty config - __set_empty_config__() - else: # else load configuration - try: - conf = imp.SourceFileLoader("conf", config_file).load_module() - except Exception: - traceback.print_exc() - __set_empty_config__() - # Set additional runtime configuration variables - if 'verbose_level' not in vars(conf): - conf.verbose_level = 0 - - -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", ec=_EC_MISSING_CONFIGURATION) - if 'timeout' not in vars(srv): - srv.timeout = 0 - if isinstance(srv.timeout) != int: - __conf_check_bad_type__('timeout') - if 'directory' not in vars(srv): - __conf_check_missing__('directory') - if isinstance(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 'command' not in vars(srv): - __conf_check_missing__('command') - if isinstance(srv.command) != str: - __conf_check_bad_type__('command') - if 'statusdir' not in vars(srv): - srv.statusdir = '/dev/shm/mcwrapper-' + server - if isinstance(srv.statusdir) != str: - __conf_check_bad_type__('statusdir') - srv.statusdir = os.path.expanduser(srv.statusdir) - return srv - -############################################################################### -# Minecraft server __STATUSSTRINGS__ = { 0: "Not running", @@ -145,35 +47,25 @@ __STATUSSTRINGS__ = { 3: "Stopping", } +__INPUTPIPE__ = 'input_pipe' +__STATUSFILE__ = 'status' +__PLAYERSFILE__ = 'players' +__PIDFILE__ = 'server.pid' + class MCServer: - def __init__(self, identifier, conf): - self.identifier = identifier + "Minecraft server wrapper class" + def __init__(self, command, statusfile=False, playersfile=False): self.players = set() self.status = 0 - self.conf = conf - self.prc = None - self.shutdownTimeout = None - 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 isinstance(self.conf.command) != str: - self.conf.command = ' '.join(self.conf.command) + self.process = None + self.command = command + self.statusfile = statusfile + self.plaersfile = playersfile 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: - pass - try: - os.mkfifo(self.inputPipe, 0o640) - except FileExistsError: - pass - if os.path.isfile(self.pidfile): - with open(self.pidfile) as f: - lpid = int(f.readline()) + if os.path.isfile(__PIDFILE__): + with open(__PIDFILE__) as file: + lpid = int(file.readline()) try: os.kill(lpid, 0) except OSError: @@ -181,122 +73,113 @@ class MCServer: "wrapper instance.") else: 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: + -1, 1) + try: + os.mkfifo(__INPUTPIPE__, 0o640) + except FileExistsError: pass - self.inputThread = Thread(target=self.__input_thread__, + if statusfile: + with open(__STATUSFILE__, 'w') as file: + file.write(__STATUSSTRINGS__[0] + '\n') + if playersfile: + open(__PLAYERSFILE__, 'w') + self.inputthread = Thread(target=self.__input_thread__, daemon=True) - self.outpuThread = Thread(target=self.__output_thread__, + self.outputhread = Thread(target=self.__output_thread__, daemon=True) def clean(self): + "Cleans files generated by wrapper" info("Server wrapper clean.") try: - os.remove(self.inputPipe) + os.remove(__INPUTPIPE__) + except FileNotFoundError: + pass + try: + os.path.isfile(__PIDFILE__) except FileNotFoundError: pass try: - os.remove(self.statusFile) + os.remove(__STATUSFILE__) except FileNotFoundError: pass try: - os.remove(self.playersFile) + os.remove(__STATUSFILE__) except FileNotFoundError: pass - if os.path.isfile(self.pidfile): - os.remove(self.pidfile) def execstart(self): - "Start execution of server" + "Start execution of Minecraft server and hold until its exits" self.start() - self.prc.wait() + self.process.wait() def start(self): "Start Minecraft server" - self.prc = subprocess.Popen( - self.conf.command, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, - start_new_session=False, - cwd=os.path.expanduser(self.conf.directory)) - with open(self.pidfile, "w") as f: - f.write(str(self.prc.pid)) + self.process = subprocess.Popen( + self.command, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + start_new_session=False) + with open(__PIDFILE__, "w") as file: + file.write(str(self.process.pid)) self.status = 1 - with open(self.statusFile, 'w') as f: - f.write(__STATUSSTRINGS__[1] + '\n') - if not self.inputThread.is_alive(): - self.inputThread.start() - if not self.outpuThread.is_alive(): - self.outpuThread.start() + if self.statusfile: + with open(__STATUSFILE__, 'w') as file: + file.write(__STATUSSTRINGS__[1] + '\n') + if not self.inputthread.is_alive(): + self.inputthread.start() + if not self.outputhread.is_alive(): + self.outputhread.start() def stop(self): + "Sends /stop command to Minecraft server" if self.running(): - self.prc.stdin.write(bytes("/stop\n", sys.getdefaultencoding())) - self.prc.stdin.flush() - self.__autoshutdown_disable__() + self.process.stdin.write(bytes( + "/stop\n", sys.getdefaultencoding())) + self.process.stdin.flush() def running(self): "Returns True if mc server is running. Othervise False." - if self.status: - return True - else: - return False + return bool(self.status) def write_to_terminal(self, text): "Write to server terminal. If server not running it does nothing" if self.status == 2: info("Input: " + text, 1) - self.prc.stdin.write(bytes(text, sys.getdefaultencoding())) - self.prc.stdin.flush() + self.process.stdin.write(bytes(text, sys.getdefaultencoding())) + self.process.stdin.flush() return True else: return False - def __autoshutdown_enable__(self): - if self.conf.timeout > 0: - 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("Automatic shutdown disabled.") - except AttributeError: - pass - def __user_join__(self, username): info("User '" + username + "' joined server.") self.players.add(username) - with open(self.playersFile, 'a') as f: - f.write(username + '\n') - self.__autoshutdown_disable__() + if self.plaersfile: + with open(__PLAYERSFILE__, 'a') as file: + file.write(username + '\n') def __user_leave__(self, username): info("User '" + username + "' left server.") self.players.remove(username) - with open(self.playersFile, 'w') as f: - f.writelines(self.players) - if self.players: - f.write('\n') - if not self.players: - self.__autoshutdown_enable__() + if self.plaersfile: + with open(__PLAYERSFILE__, 'w') as file: + file.writelines(self.players) + if self.players: + file.write('\n') def __parse_line__(self, line): if ': Done' in line: info("Server start.") self.status = 2 - with open(self.statusFile, 'w') as f: - f.write(__STATUSSTRINGS__[2] + '\n') - self.__autoshutdown_enable__() + if self.statusfile: + with open(__STATUSFILE__, 'w') as file: + file.write(__STATUSSTRINGS__[2] + '\n') elif ': Stopping the server' in line: info("Server stop.") self.status = 3 - with open(self.statusFile, 'w') as f: - f.write(__STATUSSTRINGS__[3] + '\n') + if self.statusfile: + with open(__STATUSFILE__, 'w') as file: + file.write(__STATUSSTRINGS__[3] + '\n') elif 'logged in with entity id' in line: name = line[len('[00:00:00] [Server thread/INFO]: '):] name = name[:name.index('[')] @@ -307,132 +190,83 @@ class MCServer: self.__user_leave__(name) def __output_thread__(self): - for linen in self.prc.stdout: + for linen in self.process.stdout: line = linen.decode(sys.getdefaultencoding()) info(line.rstrip(), 2, notime=True) self.__parse_line__(line.rstrip()) - with open(self.statusFile, 'w') as f: - f.write(__STATUSSTRINGS__[0] + '\n') + if self.statusfile: + with open(__STATUSFILE__, 'w') as file: + file.write(__STATUSSTRINGS__[0] + '\n') def __input_thread__(self): - with open(self.inputPipe, 'r') as p: + with open(__INPUTPIPE__, 'r') as pipe: while True: - ln = p.readline().rstrip() - if ln: - self.write_to_terminal(ln + "\n") + line = pipe.readline().rstrip() + if line: + self.write_to_terminal(line + "\n") else: time.sleep(3) ############################################################################### +mcserver = None -def wrapper_atexit(): +def __wrapper_atexit__(): "This is called when wrapper is exiting" - _mcserver.clean() + mcserver.clean() -def wrapper_toexit(): +def __wrapper_toexit__(): "This function is called when system signalizes that mcwrapper should exit" - _mcserver.stop() + mcserver.stop() def __signal_term__(_signo, _stack_frame): - wrapper_toexit() - - -def print_help(): - print('mcwrapper [arguments...] IDENTIFIER') - print(' This script is executing Minecraft server and reads its output.') - print(' From output isextracted server status and list of online') - print(' players.') - print('') - print(' arguments') - print(' -h, --help') - print(' Prints this help text.') - print(' -v, --verbose') - print(' Increase verbose level of output.') - print(' -q, --quiet') - print(' Decrease verbose level of output.') - print(' --config CONFIG_FILE') - print(' Specify configuration file to be used.') - print(' --configfile') - print(' prints used configuration file and exits.') - print(' IDENTIFIER') - print(' Identifier for new server. This allows multiple servers') - print(' running with this wrapper. Identifier is word without') - print(' spaces and preferably without special characters.') - sys.exit(_EC_OK) - - -def print_conffile(): - if '__file__' in vars(conf): - print(conf.__file__) - else: - print("No configuration file used.") - sys.exit(_EC_OK) - - -if __name__ == '__main__': - identifier = None - use_config = None - verbose_level = 0 - print_config_file = False - i = 1 - while i < len(sys.argv): - arg = sys.argv[i] - i += 1 - if arg[0] == '-': - if len(arg) > 2 and arg[1] == '-': - if arg == '--help': - print_help() - elif arg == '--verbose': - verbose_level += 1 - elif arg == '--quiet': - verbose_level += 1 - elif arg == '--config': - if use_config is not None: - error('Config option is used multiple times', - ec=_EC_ARG_MULTIPLE_CONFIG) - else: - use_config = sys.argv[i] - i += 1 - elif arg == '--configfile': - print_config_file = True - continue - else: - for l in arg[1:]: - if l == 'h': - print_help() - elif l == 'v': - verbose_level += 1 - elif l == 'q': - verbose_level -= 1 - else: - error("Unknown short argument " + l, - ec=_EC_ARG_UNKNOWN) - continue - if identifier is None: - identifier = arg - continue - error("Unknown argument: " + arg, ec=_EC_ARG_UNKNOWN) - # Parsing args ends - - load_conf(use_config) - - if print_config_file: - print_conffile() - - conf.verbose_level += verbose_level - # Set identifier if provided - if identifier: - conf.identifier = identifier - elif "identifier" not in vars(conf): - print_help() - - server_conf = conf_checkserver(conf.identifier) - _mcserver = MCServer(conf.identifier, server_conf) + __wrapper_toexit__() + + +__HELP_DESC__ = """ + This script is executing Minecraft server and reads its output. From output + is extracted server status and list of online players. And standard input + can be accessed by fifo file. + """ + + +def main(): + "Main function" + global verbose_level + parser = argparse.ArgumentParser(description=__HELP_DESC__) + parser.add_argument('--verbose', '-v', action='count', default=0, + help="Increase verbose level of output") + parser.add_argument('--quiet', '-q', action='count', default=0, + help="Decrease verbose level of output") + parser.add_argument('--status-file', '-s', action='store_true', + help="Outputs server status to file \"status\"") + parser.add_argument('--players-file', '-p', action='store_true', + help="""Outputs list of online players to file + \"players\" """) + parser.add_argument('command', nargs=argparse.REMAINDER, + help="""Command to be executed to start Minecraft + server.""") + args = parser.parse_args() + + verbose_level = args.verbose - args.quiet + command = args.command + sfile = args.status_file + pfile = args.players_file + + if not command: + parser.print_help() + if 'nogui' not in command: + command.append('nogui') + + global mcserver + mcserver = MCServer(command, pfile, sfile) signal.signal(signal.SIGTERM, __signal_term__) signal.signal(signal.SIGINT, __signal_term__) - atexit.register(wrapper_atexit) + atexit.register(__wrapper_atexit__) + + mcserver.execstart() - _mcserver.execstart() +if __name__ == '__main__': + main() diff --git a/tests/prepare.sh b/tests/prepare.sh index 89982c3..17e466d 100755 --- a/tests/prepare.sh +++ b/tests/prepare.sh @@ -4,9 +4,6 @@ if [[ "$(basename -- "$0")" = "prepare.sh" ]]; then exit 1 fi -# Write basic configuration -cp ../example.conf mcwrapper.conf - if [[ $PREPARED != "y" ]]; then # Move to known directory cd "$( readlink -f "${BASH_SOURCE[0]}" )" diff --git a/tests/t_codingstandard.sh b/tests/t_codingstandard.sh index 4daba17..1b472b4 100755 --- a/tests/t_codingstandard.sh +++ b/tests/t_codingstandard.sh @@ -21,4 +21,4 @@ fi # This test is not part of standard check because of errors caused by dynamic variable # loading to configuration. But it should be run from time to time to found other mistakes #echo "Checking bugs and poor quality" -#pylint --msg-template="{line}: [{msg_id}({symbol}), {obj}] {msg}" --reports=n ../mcwrapper +#pylint --reports=n ../mcwrapper |