From b0ad3b14a12bf3725cfd571036dba7e5664d4f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karel=20Ko=C4=8D=C3=AD?= Date: Fri, 8 Jan 2016 15:58:27 +0100 Subject: Change conf format and add multiple servers support This commit changes format of configuration files. It should now contains classes that represents different Minecraft servers. Minecraft server handling functionality implemented across whole mcwrapper script is now in one class MCServer. Instancing this class initializes new server and such server can be started and stopped on demand during mcwrapper execution. This allows execution of multiple Minecraft server under single mcwrapper instance, but this is not supported by command line interface. This feature will be used by new feature, man in the middle, that have to be implemented yes. --- mcwrapper | 440 +++++++++++++++++++++++++++++++++++--------------------------- 1 file changed, 247 insertions(+), 193 deletions(-) (limited to 'mcwrapper') diff --git a/mcwrapper b/mcwrapper index e7802ae..8b0c253 100755 --- a/mcwrapper +++ b/mcwrapper @@ -11,9 +11,42 @@ import atexit 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() -- cgit v1.2.3