#!/usr/bin/env python3 import os import sys import re import subprocess import signal import time import datetime import traceback from threading import Thread import importlib.machinery as imp ################################################################################# # Load configuration __all_config_files__ = ( 'mcwrapper.conf', '~/.mcwrapper.conf', '~/.config/mcwrapper.conf', '/etc/mcwrapper.conf', ) def __set_empty_config__(): global conf global conf_source print('Warning: User configuration not loaded. Using default.', file=sys.stderr) conf = type('default config', (object,), {}) __config_file__ = None try: __config_file__ = os.environ['CONFIG'] # get config file from environment except KeyError: # Find configuration in predefined paths for cf in __all_config_files__: if os.path.isfile(cf): __config_file__ = cf break if __config_file__ == 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 try: conf.verbose_level except AttributeError: conf.verbose_level = 0 try: conf.command except AttributeError: conf.command = [] try: conf.server except AttributeError: conf.server = dict() ################################################################################# __STATUSSTRINGS__ = { 0: "Not running", 1: "Starting", 2: "Running", 3: "Stopping", } def __user_join__(username): global playerCount playerCount += 1 if conf.verbose_level >= 0: print("User '" + username + "' joined server.") with open(conf.playersFile, 'a') as f: players.add(username) f.write(username + '\n') def __user_leave__(username): global playerCount playerCount -= 1 if conf.verbose_level >= 0: print("User '" + username + "' left server.") players.remove(username) with open(conf.playersFile, 'w') as f: f.writelines(players) if players: f.write('\n') def __server_start__(): if conf.verbose_level >= 0: print("Wrapper initializing with identifier: " + conf.identifier) try: os.mkdir(conf.status) except FileExistsError: pass if os.path.isfile(inputPipe): if conf.verbose_level >= -1: print("Error: Server input pipe already exists. Is another wrapper running?") sys.exit(4) os.mkfifo(inputPipe, 0o640) global statusFile statusFile = conf.status + '/status' with open(statusFile, 'w') as f: f.write(__STATUSSTRINGS__[1]) global playersFile playersFile = conf.status + '/status' with open(playersFile, 'w') as f: pass global playerCount playerCount = 0 def __server_clean__(): if conf.verbose_level >= 0: print("Wrapper clean.") try: os.remove(inputPipe) except FileNotFoundError: pass try: os.remove(statusFile) except FileNotFoundError: pass try: os.remove(playersFile) except FileNotFoundError: pass def __parse_line__(line): if ': Done' in line: print("Server start.") with open(statusFile, 'w') as f: f.write(__STATUSSTRINGS__[2] + '\n') elif ': Stopping the server' in line: print("Server stop.") with open(statusFile, 'w') as f: f.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('[')] __user_join__(name) elif 'left the game' in line: name = line[len('[00:00:00] [Server thread/INFO]: '):] name = name[:name.index(' ')] __user_leave__(name) ################################################################################# class __InputThread__(Thread): def __init__(self, pipeprocess): Thread.__init__(self, name='InputThread') self.pipeprocess = pipeprocess self.stopread = False def stopexec(self): self.stopread = True def wake(self): with open(inputPipe, 'w') as f: f.write("\n") f.flush() def run(self): with open(inputPipe, 'r') as p: while not self.stopread: ln = p.readline() if ln.rstrip(): if conf.verbose_level >= 1: print("Input: " + ln, end="") self.pipeprocess.write(bytes(ln, sys.getdefaultencoding())) self.pipeprocess.flush() else: time.sleep(1) def __server_send_stop__(): global prc prc.stdin.write(bytes("/stop\n", sys.getdefaultencoding())) prc.stdin.flush() def mcexec(): """Executes cmd and parses output for server status changes. """ global prc __server_start__() if type(conf.command) != str: conf.command = ' '.join(conf.command) if conf.verbose_level >= 1: print("Folder: " + conf.folder) print("Start command: " + conf.command) os.chdir(conf.folder) prc = subprocess.Popen(conf.command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True) inputThread = __InputThread__(prc.stdin) inputThread.start() inputThread.wake() # Input thread is stuck in waiting for first line for linen in prc.stdout: line = linen.decode(sys.getdefaultencoding()) if conf.verbose_level >= 2: print(line.rstrip()) __parse_line__(line.rstrip()) inputThread.stopexec() __server_clean__() ################################################################################# def __signal_term__(_signo, _stack_frame): __server_send_stop__() def print_help(): print('mcwrapper [arguments...] ACTION ...') print(' This script is executing Minecraft server and reads its output.') 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('') print(' Common action arguments') print(' IDENTIFIER') print(' Identifier for new server instance. This allows multiple server') print(' instances running with this wrapper.') print(' Identifier is word without spaces and preferably without special') print(' characters.') print('') print(' ACTION and it\'s arguments') print(' start INDETIFIER') print(' Start server under "IDENTIFIER"') print(' stop IDENTIFIER') print(' Sends stop command to server under "IDENTIFIER"') print(' say IDENTIFIER {message...}') print(' Sends message to server chat') sys.exit() if __name__ == '__main__': action = None message = [] for arg in sys.argv[1:]: if (action == 'start' or action == 'stop' or action == 'say') \ and conf.identifier == None: conf.identifier = arg continue if action == 'say': message.append(arg) continue if arg[0] == '-': if len(arg) > 2 and arg[1] == '-': if arg == '--help': print_help() if arg == '--verbose': conf.verbose_level += 1 if arg == '--quiet': conf.verbose_level += 1 continue else: for l in arg[1:]: if l == 'h': print_help() elif l == 'v': conf.verbose_level += 1 elif l == 'q': conf.verbose_level -= 1 else: sys.exit("Unknown short argument " + l) continue if action == None: if arg.lower() == 'start': action = 'start' continue if arg.lower() == 'stop': action = 'stop' continue if arg.lower() == 'say': action = 'say' continue sys.exit("Unknown argument: " + arg) # Parsing args ends # Expand configuration for specified identifier if action == 'start' or action == 'stop' or action == 'say': if not conf.identifier: print('Missing server identifier argument!') print('') print_help() try: conf.server[conf.identifier] vars(conf).update(conf.server[conf.identifier]) except KeyError: if conf.verbose_level >= -1: sys.exit('Error: No configuration associated with identifier: ' + conf.identifier) # Set configurations for server try: conf.folder except AttributeError: sys.exit('Missing "folder" config') try: conf.command except AttributeError: sys.exit('Missing server start command!') try: conf.status except AttributeError: conf.status = '/dev/shm/mcwrapper-' + conf.identifier # Set inputPipe global inputPipe inputPipe = conf.status + '/input_pipe' if action == 'start': signal.signal(signal.SIGTERM, __signal_term__) signal.signal(signal.SIGINT, __signal_term__) mcexec() elif action == 'stop': if not os.path.exists(inputPipe): sys.exit("Such server is not running") with open(inputPipe, 'w') as f: f.write("/stop\n") f.flush() while os.path.exists(inputPipe): # Block until server stops pass elif action == 'say': if not os.path.exists(inputPipe): sys.exit("Such server is not running") with open(inputPipe, 'w') as f: msg = ' '.join(message) msg = re.sub('^','say ', msg) print(msg) f.write(msg + '\n') f.flush() else: print_help()