#!/usr/bin/env python3 # vim: expandtab ft=python ts=4 sw=4 sts=4: import os import sys import subprocess import signal import time import atexit import argparse from threading import Thread ############################################################################### # Exit codes and prints helpers verbose_level = 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" if verbose_level >= minverbose: __print_message__(message, notime=notime) def warning(message, minverbose=-1, notime=False): "Prints message to stderr if minverbose >= verbose_level" if verbose_level >= minverbose: __print_message__(message, file=sys.stderr, notime=notime) def error(message, minverbose=-2, errcode=-1, notime=False): "Prints message to stderr if minverbose >= verbose_level" if verbose_level >= minverbose: __print_message__(message, file=sys.stderr, notime=notime) sys.exit(errcode) ############################################################################### __STATUSSTRINGS__ = { 0: "Not running", 1: "Starting", 2: "Running", 3: "Stopping", } __INPUTPIPE__ = 'input_pipe' __STATUSFILE__ = 'status' __PLAYERSFILE__ = 'players' __PIDFILE__ = 'server.pid' class MCServer: "Minecraft server wrapper class" def __init__(self, command, statusfile=False, playersfile=False): self.players = set() self.status = 0 self.process = None self.command = command self.statusfile = statusfile self.plaersfile = playersfile info("Server wrapper initializing") if os.path.isfile(__PIDFILE__): with open(__PIDFILE__) as file: lpid = int(file.readline()) try: os.kill(lpid, 0) except OSError: warning("Detected forced termination of previous server " "wrapper instance.") else: error("Another wrapper is running with given identifier.", -1, 1) try: os.mkfifo(__INPUTPIPE__, 0o640) except FileExistsError: pass 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__, daemon=True) def clean(self): "Cleans files generated by wrapper" info("Server wrapper clean.") try: os.remove(__INPUTPIPE__) except FileNotFoundError: pass try: os.path.isfile(__PIDFILE__) except FileNotFoundError: pass try: os.remove(__STATUSFILE__) except FileNotFoundError: pass try: os.remove(__STATUSFILE__) except FileNotFoundError: pass def execstart(self): "Start execution of Minecraft server and hold until its exits" self.start() self.process.wait() def start(self): "Start Minecraft server" 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 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.process.stdin.write(bytes( "/stop\n", sys.getdefaultencoding())) self.process.stdin.flush() def running(self): "Returns True if mc server is running. Othervise 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.process.stdin.write(bytes(text, sys.getdefaultencoding())) self.process.stdin.flush() return True else: return False def __user_join__(self, username): info("User '" + username + "' joined server.") self.players.add(username) 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) 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 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 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('[')] self.__user_join__(name) elif 'left the game' in line: name = line[len('[00:00:00] [Server thread/INFO]: '):] name = name[:name.index(' ')] self.__user_leave__(name) def __output_thread__(self): for linen in self.process.stdout: line = linen.decode(sys.getdefaultencoding()) info(line.rstrip(), 2, notime=True) self.__parse_line__(line.rstrip()) if self.statusfile: with open(__STATUSFILE__, 'w') as file: file.write(__STATUSSTRINGS__[0] + '\n') def __input_thread__(self): with open(__INPUTPIPE__, 'r') as pipe: while True: line = pipe.readline().rstrip() if line: self.write_to_terminal(line + "\n") else: time.sleep(3) ############################################################################### mcserver = None def __wrapper_atexit__(): "This is called when wrapper is exiting" mcserver.clean() def __wrapper_toexit__(): "This function is called when system signalizes that mcwrapper should exit" mcserver.stop() def __signal_term__(_signo, _stack_frame): __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__) mcserver.execstart() if __name__ == '__main__': main()