diff options
| -rw-r--r-- | example.conf | 33 | ||||
| -rwxr-xr-x | mcwrapper | 440 | 
2 files changed, 272 insertions, 201 deletions
| diff --git a/example.conf b/example.conf index 6b8cf8f..a267735 100644 --- a/example.conf +++ b/example.conf @@ -1,12 +1,29 @@  # This is exaple configuration for mcwrapper  # Use Python3 syntax to specify configuration. -# For full list of configuration options refer to documentation. +############################################## -identifier = 'exampleserver' +# 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. +#identifier = 'exampleserver' -server = dict() -server["exampleserver"] = { -		"directory": '~/minecraft', -		"command": "java -jar mcs.jar nogui", -		"status": '/dev/shm/mcwrapper-exampleserver', -		} +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 @@ -12,8 +12,41 @@ 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 + +def info(message, minverbose = 0): +    "Prints message to stdout if minverbose >= verbose_level" +    try: +        if conf.verbose_level >= minverbose: +            print(message) +    except NameError: +        print(message) + +def warning(message, minverbose = -1): +    "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) + +def error(message, minverbose = -2, ec = -1): +    "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 +    sys.exit(ec) + +#################################################################################  # Load configuration -# This segment is same as in mcmim  __all_config_files__ = (          'mcwrapper.conf', @@ -22,13 +55,13 @@ __all_config_files__ = (          '/etc/mcwrapper.conf',          ) -def __set_empty_config__(): -    global conf -    print('Warning: User configuration not loaded. Using default.', file=sys.stderr) -    conf = type('default config', (object,), {}) -  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 == None:          # Find configuration in predefined paths          for cf in __all_config_files__: @@ -46,34 +79,30 @@ def load_conf(config_file):      # Set additional runtime configuration variables      if not 'verbose_level' in vars(conf):          conf.verbose_level = 0 -    if not 'server' in vars(conf): -        conf.server = dict() -    if not 'timeout' in vars(conf): -        conf.timeout = 0 - -################################################################################# - -def autoshutdown_enable(): -    global shutdownTimeout -    if (conf.timeout > 0): -        if (conf.verbose_level >= 0): -            print("Automatic shutdown after " + str(conf.timeout) + -                " min.") -        shutdownTimeout = Timer(conf.timeout * 60.0, __server_send_stop__) -        shutdownTimeout.start(); -    pass -def autoshutdown_disable(): -    global shutdownTimeout +def conf_setserver(server): +    "Check and set configuration for server specified as agument."      try: -        shutdownTimeout.cancel() -        del shutdownTimeout -        if (conf.verbose_level >= 0): -            print("Automatic shutdown disabled.") -    except NameError: -        pass +        srv = vars(conf)[server]; +    except KeyError: +        error("No configuration class found for server: " + server, +                ec = _EC_MISSING_CONFIGURATION) +    if not 'timeout' in vars(srv): +        srv.timeout = 0 +    if not 'directory' in vars(srv): +        error('Missing "directory" config for server ' + server, +                ec = _EC_MISSING_CONFIGURATION) +    if not 'command' in vars(srv): +        error('Missing server start command for server ' + server, +                ec = _EC_MISSING_CONFIGURATION) +    if not 'statusdir' in vars(srv): +        srv.statusdir = '/dev/shm/mcwrapper-' + server +    return srv  ################################################################################# +# Minecraft server + +_mcservers = []  __STATUSSTRINGS__ = {      0: "Not running", @@ -82,153 +111,204 @@ __STATUSSTRINGS__ = {      3: "Stopping",      } -def __user_join__(username): -    global playersFile -    global players -    if conf.verbose_level >= 0: -        print("User '" + username + "' joined server.") -    with open(playersFile, 'a') as f: -        players.add(username) -        f.write(username + '\n') -    autoshutdown_disable() - -def __user_leave__(username): -    global playersFile -    global players -    if conf.verbose_level >= 0: -        print("User '" + username + "' left server.") -    players.remove(username) -    with open(playersFile, 'w') as f: -        f.writelines(players) -        if players: -            f.write('\n') -    if (not players): -        autoshutdown_enable() - -def __server_start__(): -    if conf.verbose_level >= 0: -        print("Wrapper initializing with identifier: " + conf.identifier) -    try: -        os.mkdir(conf.status) -    except FileExistsError: -        pass -    try: -        os.mkfifo(inputPipe, 0o640) -    except FileExistsError: -        pass -    if os.path.isfile(pidfile): -        with open(pidfile) as f: -            lpid = int(f.readline()) +class MCServer: +    def __init__(self, identifier): +        _mcservers.append(self) +        self.identifier = identifier +        self.players = set() +        self.status = 0 +        self.conf = conf_setserver(identifier) +        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) +        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()) +            try: +                os.kill(lpid, 0) +            except OSError: +                warning(self.identifier + ": Detected forced termination of " +                        "previous server wrapper instance.") +            else: +                error(self.identifier + ": 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)          try: -            os.kill(lpid, 0) -        except OSError: -            if conf.verbose_level >= 0: -                print("Warning: Detected forced termination of previous wrapper instance") +            os.remove(self.inputPipe) +        except FileNotFoundError: +            pass +        try: +            os.remove(self.statusFile) +        except FileNotFoundError: +            pass +        try: +            os.remove(self.playersFile) +        except FileNotFoundError: +            pass +        if os.path.isfile(self.pidfile): +            os.remove(self.pidfile) + +    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.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() +    def stop(self): +        if self.running(): +            self.prc.stdin.write(bytes("/stop\n", sys.getdefaultencoding())) +            self.prc.stdin.flush() +    def running(self): +        "Returns True if mc server is running. Othervise False." +        if self.status != 0: +            return True          else: -            if conf.verbose_level >= -1: -                print("Error: Another wrapper is running with given identifier.") -            sys.exit(4) -    with open(statusFile, 'w') as f: -        f.write(__STATUSSTRINGS__[1] + '\n') -    with open(playersFile, 'w') as f: -        pass +            return False +    def write_to_terminal(self, text): +        "Write to server terminal. If server not running it does nothing" +        if self.running(): +            info(self.identifier + ": Input: " + ln,  1) +            self.prc.write(bytes(line, sys.getdefaultencoding())) +            self.prc.flush() +    def join(self): +        "Join execution untill server exits." +        self.outpuThread.join() +        self.inputThread.join() -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 -    if os.path.isfile(pidfile): -        os.remove(pidfile) +    def __autoshutdown_enable__(self): +        if (self.conf.timeout > 0): +            info(self.identifier + ": 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.") +        except AttributeError: +            pass +    def __user_join__(self, username): +        info(self.identifier + ": User '" + username + "' joined server.") +        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.") +        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__() -def __parse_line__(line): -    if ': Done' in line: -        print("Server start.") -        with open(statusFile, 'w') as f: -            f.write(__STATUSSTRINGS__[2] + '\n') -        autoshutdown_enable() -    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) +    def __parse_line__(self, line): +        if ': Done' in line: +            info(self.identifier + ": 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.") +            self.status = 3 +            with open(self.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('[')] +            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) -################################################################################# +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: +            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: +            f.write(__STATUSSTRINGS__[0] + '\n') -class __InputThread__(Thread): -    def __init__(self, pipeprocess): -        Thread.__init__(self, name='InputThread') -        self.pipeprocess = pipeprocess +class __MCServerInputThread__(Thread): +    def __init__(self, mcserver): +        Thread.__init__(self, name='MCServerInputThread:' + mcserver.identifier) +        self.mcserver = mcserver          self.stopread = False -    def stopexec(self): +    def stop(self):          self.stopread = True      def wake(self): -        with open(inputPipe, 'w') as f: +        with open(self.mcserver.inputPipe, 'w') as f:              f.write("\n")              f.flush()      def run(self): -        with open(inputPipe, 'r') as p: +        with open(self.mcserver.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() +                ln = p.readline().rstrip() +                if ln: +                    self.mcserver.write_to_terminal(ln + "\n")                  else: -                    time.sleep(1) +                    time.sleep(3) -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 -    if type(conf.command) != str: -        conf.command = ' '.join(conf.command) -    if conf.verbose_level >= 1: -        print("Folder: " + conf.directory) -        print("Start command: " + conf.command) -    os.chdir(os.path.expanduser(conf.directory)) -    prc = subprocess.Popen(conf.command, stdin=subprocess.PIPE, -            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, -             start_new_session=False) -    with open(pidfile, "w") as f: -        f.write(str(prc.pid)) -    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()  ################################################################################# +def wrapper_atexit(): +    "This is called when wrapper is exiting" +    for srv in _mcservers: +        del srv + +def wrapper_toexit(): +    "This function is called when system signalizes that mcwrapper should exit" +    for srv in _mcservers: +        srv.stop() +  def __signal_term__(_signo, _stack_frame): -    __server_send_stop__() +    wrapper_toexit()  def print_help():      print('mcwrapper [arguments...] IDENTIFIER') @@ -248,13 +328,12 @@ def print_help():      print('   Identifier for new server. This allows multiple servers running with this')      print('   wrapper.  Identifier is word without spaces and preferably without special')      print('   characters.') -    sys.exit() +    sys.exit(_EC_OK)  if __name__ == '__main__':      identifier = None      use_config = None      verbose_level = 0 -    message = []      i = 1      while i < len(sys.argv):          arg = sys.argv[i] @@ -269,7 +348,8 @@ if __name__ == '__main__':                      verbose_level += 1                  elif arg == '--config':                      if use_config != None: -                        sys.exit('Config option is used multiple times') +                        error('Config option is used multiple times', +                                ec = _EC_ARG_MULTIPLE_CONFIG)                      else:                          use_config = sys.argv[i]                          i += 1 @@ -283,12 +363,12 @@ if __name__ == '__main__':                      elif l == 'q':                          verbose_level -= 1                      else: -                        sys.exit("Unknown short argument " + l) +                        error("Unknown short argument " + l, ec = _EC_ARG_UNKNOWN)                  continue          if identifier == None:              identifier = arg              continue -        sys.exit("Unknown argument: " + arg) +        error("Unknown argument: " + arg, ec = _EC_ARG_UNKNOWN)      # Parsing args ends      load_conf(use_config) @@ -296,37 +376,11 @@ if __name__ == '__main__':      # Set identifier if provided      if identifier:          conf.identifier = identifier -    # Expand configuration for specified identifier -    if 'identifier' not in vars(conf): -        print('Missing server identifier argument!') -        print('') -        print_help() -    try: -        conf.server[conf.identifier] -        vars(conf).update(conf.server[conf.identifier]) -    except KeyError: -        sys.exit('Error: No configuration associated with identifier: ' + conf.identifier) -    # Check configurations for server -    if not 'directory' in vars(conf): -        sys.exit('Missing "directory" config for server ' + conf.identifier) -    if not 'command' in vars(conf): -        sys.exit('Missing server start command for server ' + conf.identifier) -    if not 'status' in vars(conf): -        conf.status = '/dev/shm/mcwrapper-' + conf.identifier -    # Set inputPipe -    global inputPipe -    inputPipe = conf.status + '/input_pipe' -    global statusFile -    statusFile = conf.status + '/status' -    global playersFile -    playersFile = conf.status + '/players' -    global pidfile -    pidfile = conf.status + '/server.pid' -    global players -    players = set()      signal.signal(signal.SIGTERM, __signal_term__)      signal.signal(signal.SIGINT, __signal_term__) -    __server_start__() -    atexit.register(__server_clean__) -    mcexec() +    atexit.register(wrapper_atexit) + +    mcs = MCServer(conf.identifier) +    mcs.start() +    mcs.join() | 
