Пример #1
0
    def __init__(self):
        # TODO console args
        self.mcdr_state = MCDReforgedState.INITIALIZING
        self.server_state = ServerState.STOPPED
        self.process = None  # type: Optional[PIPE]
        self.flags = MCDReforgedFlag.NONE
        self.starting_server_lock = Lock(
        )  # to prevent multiple start_server() call
        self.stop_lock = Lock()  # to prevent multiple stop() call

        # will be assigned in on_config_changed()
        self.encoding_method = None  # type: Optional[str]
        self.decoding_method = None  # type: Optional[str]

        # --- Constructing fields ---
        self.logger = MCDReforgedLogger(self)
        self.logger.set_file(constant.LOGGING_FILE)
        self.server_interface = ServerInterface(self)
        self.task_executor = TaskExecutor(self)
        self.console_handler = ConsoleHandler(self)
        self.watch_dog = WatchDog(self)
        self.update_helper = UpdateHelper(self)
        self.language_manager = LanguageManager(self.logger)
        self.config = Config(self.logger)
        self.rcon_manager = RconManager(self)
        self.server_handler_manager = ServerHandlerManager(self)
        self.reactor_manager = InfoReactorManager(self)
        self.command_manager = CommandManager(self)
        self.plugin_manager = PluginManager(self)
        self.permission_manager = PermissionManager(self)

        # --- Initialize fields instance ---
        file_missing = False
        try:
            self.load_config(
                allowed_missing_file=False)  # loads config, language, handlers
        except FileNotFoundError:
            self.logger.error('Config is missing, default config generated')
            self.config.save_default()
            file_missing = True
        try:
            self.permission_manager.load_permission_file(
                allowed_missing_file=False)
        except FileNotFoundError:
            self.logger.error(
                'Permission file is missing, default permission file generated'
            )
            self.permission_manager.save_default()
            file_missing = True
        if file_missing:
            self.on_first_start()
            return
        self.plugin_manager.register_permanent_plugins()

        self.set_mcdr_state(MCDReforgedState.INITIALIZED)
Пример #2
0
class MyTestCase(unittest.TestCase):
    def __init__(self, *args):
        super().__init__(*args)
        self.language_manager = LanguageManager(logging.getLogger())

    def test_0_same_key_order(self):
        language_list = ['en_us', 'zh_cn']
        language_key_dict = {}
        for lang in language_list:
            self.language_manager.set_language(lang)
            language_key_dict[lang] = list(
                self.language_manager.translations.keys())
        base_lang = language_list[0]
        base_keys = language_key_dict[base_lang]
        for test_lang, lang_keys in language_key_dict.items():
            for i, key in enumerate(base_keys):
                self.assertEqual(
                    key, lang_keys[i],
                    'key[{}] "{}" in base language {} is not the same as test language {}'
                    .format(i, key, base_lang, test_lang))
            self.assertEqual(len(base_keys), len(lang_keys))
Пример #3
0
class MCDReforgedServer:
    process: Optional[Popen]

    def __init__(self):
        # TODO console args
        self.mcdr_state = MCDReforgedState.INITIALIZING
        self.server_state = ServerState.STOPPED
        self.process = None  # type: Optional[PIPE]
        self.flags = MCDReforgedFlag.NONE
        self.starting_server_lock = Lock(
        )  # to prevent multiple start_server() call
        self.stop_lock = Lock()  # to prevent multiple stop() call

        # will be assigned in on_config_changed()
        self.encoding_method = None  # type: Optional[str]
        self.decoding_method = None  # type: Optional[str]

        # --- Constructing fields ---
        self.logger = MCDReforgedLogger(self)
        self.logger.set_file(constant.LOGGING_FILE)
        self.server_interface = ServerInterface(self)
        self.task_executor = TaskExecutor(self)
        self.console_handler = ConsoleHandler(self)
        self.watch_dog = WatchDog(self)
        self.update_helper = UpdateHelper(self)
        self.language_manager = LanguageManager(self.logger)
        self.config = Config(self.logger)
        self.rcon_manager = RconManager(self)
        self.server_handler_manager = ServerHandlerManager(self)
        self.reactor_manager = InfoReactorManager(self)
        self.command_manager = CommandManager(self)
        self.plugin_manager = PluginManager(self)
        self.permission_manager = PermissionManager(self)

        # --- Initialize fields instance ---
        file_missing = False
        try:
            self.load_config(
                allowed_missing_file=False)  # loads config, language, handlers
        except FileNotFoundError:
            self.logger.error('Config is missing, default config generated')
            self.config.save_default()
            file_missing = True
        try:
            self.permission_manager.load_permission_file(
                allowed_missing_file=False)
        except FileNotFoundError:
            self.logger.error(
                'Permission file is missing, default permission file generated'
            )
            self.permission_manager.save_default()
            file_missing = True
        if file_missing:
            self.on_first_start()
            return
        self.plugin_manager.register_permanent_plugins()

        self.set_mcdr_state(MCDReforgedState.INITIALIZED)

    def __del__(self):
        try:
            if self.process and self.process.poll() is None:
                self.kill_server()
        except:
            pass

    def on_first_start(self):
        self.logger.info(
            'Some of the user files are missing, check them before launch MCDR again'
        )
        default_config = self.config.get_default()
        file_util.touch_directory(default_config['working_directory'])
        self.plugin_manager.set_plugin_directories(
            default_config['plugin_directories'])  # to touch the directory

    # --------------------------
    #   Translate info strings
    # --------------------------

    def tr(self, text, *args, allow_failure=True):
        result = self.language_manager.translate(text,
                                                 allow_failure).strip('\r\n')
        if len(args) > 0:
            result = result.format(*args)
        return result

    # --------------------------
    #          Loaders
    # --------------------------

    def load_config(self, *, allowed_missing_file=True):
        has_missing = self.config.read_config(allowed_missing_file)
        # load the language first to make sure tr() is available
        self.on_config_changed()
        if has_missing:
            for line in self.tr('config.missing_config').splitlines():
                self.logger.warning(line)

    def on_config_changed(self):
        logger.console_color_disabled = self.config['disable_console_color']
        self.logger.set_debug_options(self.config['debug'])
        if self.config.is_debug_on():
            self.logger.info(
                self.tr('mcdr_server.on_config_changed.debug_mode_on'))

        self.language_manager.set_language(self.config['language'])
        self.logger.info(
            self.tr('mcdr_server.on_config_changed.language_set',
                    self.config['language']))

        self.encoding_method = self.config['encoding'] if self.config[
            'encoding'] is not None else sys.getdefaultencoding()
        self.decoding_method = self.config['decoding'] if self.config[
            'decoding'] is not None else locale.getpreferredencoding()
        self.logger.info(
            self.tr('mcdr_server.on_config_changed.encoding_decoding_set',
                    self.encoding_method, self.decoding_method))

        self.plugin_manager.set_plugin_directories(
            self.config['plugin_directories'])
        self.logger.info(
            self.tr('mcdr_server.on_config_changed.plugin_directories_set',
                    self.encoding_method, self.decoding_method))
        for directory in self.plugin_manager.plugin_directories:
            self.logger.info('- {}'.format(directory))

        self.reactor_manager.register_reactors(
            self.config['custom_info_reactors'])

        self.server_handler_manager.register_handlers(
            self.config['custom_handlers'])
        self.server_handler_manager.set_handler(self.config['handler'])
        self.logger.info(
            self.tr('mcdr_server.on_config_changed.handler_set',
                    self.config['handler']))

        self.connect_rcon()

    def load_plugins(self):
        self.plugin_manager.refresh_all_plugins()
        self.logger.info(
            self.plugin_manager.last_operation_result.to_rtext(show_path=True))

    def on_plugin_changed(self):
        self.command_manager.clear_command()
        self.plugin_manager.registry_storage.export_commands(
            self.command_manager.register_command)

    # ---------------------------
    #   State Getters / Setters
    # ---------------------------

    def is_server_running(self):
        return self.server_state.in_state(
            {ServerState.RUNNING, ServerState.STOPPING})

    # Flags

    def is_server_startup(self):
        return MCDReforgedFlag.SERVER_STARTUP in self.flags

    def is_server_rcon_ready(self):
        return MCDReforgedFlag.SERVER_RCON_READY in self.flags

    def is_interrupt(self):
        return MCDReforgedFlag.INTERRUPT in self.flags

    def is_mcdr_exit(self):
        return self.mcdr_in_state(MCDReforgedState.STOPPED)

    def is_mcdr_about_to_exit(self):
        return self.mcdr_in_state(
            {MCDReforgedState.PRE_STOPPED, MCDReforgedState.STOPPED})

    def is_exit_naturally(self):
        return MCDReforgedFlag.EXIT_NATURALLY in self.flags

    def with_flag(self, flag: MCDReforgedFlag):
        self.flags |= flag

    def remove_flag(self, flag: MCDReforgedFlag):
        self.flags &= ~flag

    def set_exit_naturally(self, flag):
        if flag:
            self.with_flag(MCDReforgedFlag.EXIT_NATURALLY)
        else:
            self.remove_flag(MCDReforgedFlag.EXIT_NATURALLY)
        self.logger.debug('flag_exit_naturally has set to "{}"'.format(flag),
                          option=DebugOption.MCDR)

    # State

    def server_in_state(self, states):
        return self.server_state.in_state(states)

    def mcdr_in_state(self, states):
        return self.mcdr_state.in_state(states)

    def is_initialized(self):
        return self.mcdr_in_state(MCDReforgedState.INITIALIZED)

    def set_server_state(self, state):
        self.server_state = state
        self.logger.debug('Server state has set to "{}"'.format(state),
                          option=DebugOption.MCDR)

    def set_mcdr_state(self, state):
        self.mcdr_state = state
        self.logger.debug('MCDR state has set to "{}"'.format(state),
                          option=DebugOption.MCDR)

    def should_keep_looping(self):
        """
		A criterion for sub threads to determine if it should keep looping
		:rtype: bool
		"""
        if self.server_in_state(ServerState.STOPPED):
            if self.is_interrupt():  # if interrupted and stopped
                return False
            return not self.is_exit_naturally(
            )  # if the sever exited naturally, exit MCDR
        return not self.is_mcdr_exit()

    # --------------------------
    #      Server Controls
    # --------------------------

    def start_server(self):
        """
		try to start the server process
		return True if the server process has started successfully
		return False if the server is not able to start

		:return: a bool as above
		:rtype: bool
		"""
        with self.starting_server_lock:
            if self.is_interrupt():
                self.logger.warning(
                    self.tr('mcdr_server.start_server.already_interrupted'))
                return False
            if self.is_server_running():
                self.logger.warning(
                    self.tr('mcdr_server.start_server.start_twice'))
                return False
            if self.is_mcdr_about_to_exit():
                self.logger.warning(
                    self.tr('mcdr_server.start_server.about_to_exit'))
                return False
            cwd = self.config['working_directory']
            if not os.path.isdir(cwd):
                self.logger.error(
                    self.tr('mcdr_server.start_server.cwd_not_existed', cwd))
                return False
            try:
                start_command = self.config['start_command']
                self.logger.info(
                    self.tr('mcdr_server.start_server.starting',
                            start_command))
                self.process = Popen(start_command,
                                     cwd=self.config['working_directory'],
                                     stdin=PIPE,
                                     stdout=PIPE,
                                     stderr=STDOUT,
                                     shell=True)
            except:
                self.logger.exception(
                    self.tr('mcdr_server.start_server.start_fail'))
                return False
            else:
                self.on_server_start()
                return True

    def kill_server(self):
        """
		Kill the server process group
		"""
        if self.process and self.process.poll() is None:
            self.logger.info(self.tr('mcdr_server.kill_server.killing'))
            try:
                for child in psutil.Process(
                        self.process.pid).children(recursive=True):
                    child.kill()
                    self.logger.info(
                        self.tr('mcdr_server.kill_server.process_killed',
                                child.pid))
            except psutil.NoSuchProcess:
                pass
            self.process.kill()
            self.logger.info(
                self.tr('mcdr_server.kill_server.process_killed',
                        self.process.pid))
        else:
            raise IllegalCallError(
                "Server process has already been terminated")

    def interrupt(self):
        """
		Interrupt MCDR
		The first call will softly stop the server and the later calls will kill the server
		Return if it's the first try
		:rtype: bool
		"""
        self.logger.info(
            'Interrupting, first strike = {}'.format(not self.is_interrupt()))
        self.stop(forced=self.is_interrupt())
        ret = self.is_interrupt()
        self.with_flag(MCDReforgedFlag.INTERRUPT)
        return ret

    def stop(self, forced=False):
        """
		Stop the server

		:param forced: an optional bool. If it's False (default) MCDR will stop the server by sending the STOP_COMMAND from the
		current handler. If it's True MCDR will just kill the server process group
		"""
        with self.stop_lock:
            if not self.is_server_running():
                self.logger.warning(
                    self.tr('mcdr_server.stop.stop_when_stopped'))
                return
            self.set_server_state(ServerState.STOPPING)
            if not forced:
                try:
                    self.send(self.server_handler_manager.get_current_handler(
                    ).get_stop_command())
                except:
                    self.logger.error(self.tr('mcdr_server.stop.stop_fail'))
                    forced = True
            if forced:
                try:
                    self.kill_server()
                except IllegalCallError:
                    pass

    # --------------------------
    #      Server Logics
    # --------------------------

    def on_server_start(self):
        self.logger.info(
            self.tr('mcdr_server.start_server.pid_info', self.process.pid))
        self.plugin_manager.dispatch_event(MCDRPluginEvents.SERVER_START, ())
        self.set_server_state(ServerState.RUNNING)
        self.set_exit_naturally(
            True
        )  # Set after server state is set to RUNNING, or MCDR might have a chance to exit if the server is started by other thread

    def on_server_stop(self):
        return_code = self.process.poll()
        self.logger.info(
            self.tr('mcdr_server.on_server_stop.show_stopcode', return_code))
        self.process.stdin.close()
        self.process.stdout.close()
        self.process = None
        self.set_server_state(ServerState.STOPPED)
        self.remove_flag(
            MCDReforgedFlag.SERVER_STARTUP
            | MCDReforgedFlag.SERVER_RCON_READY)  # removes this two
        self.plugin_manager.dispatch_event(MCDRPluginEvents.SERVER_STOP,
                                           (return_code, ),
                                           wait=True)

    def send(self, text, ending='\n', encoding=None):
        """
		Send a text to server's stdin if the server is running

		:param text: A str or a bytes you want to send. if text is a str then it will attach the ending parameter to its
		back
		:param str ending: The suffix of a command with a default value \n
		:param str encoding: The encoding method for the text. If it's not given used the method in config
		"""
        if encoding is None:
            encoding = self.encoding_method
        if type(text) is str:
            text = (text + ending).encode(encoding)
        if self.is_server_running():
            self.process.stdin.write(text)
            self.process.stdin.flush()
        else:
            self.logger.warning(self.tr('mcdr_server.send.send_when_stopped'))

    def receive(self):
        """
		Try to receive a str from server's stdout. This will block the current thread
		If server has stopped it will wait up to 10s for the server process to exit, then raise a ServerStopped exception

		:rtype: str
		:raise: ServerStopped
		"""
        while True:
            try:
                text = next(iter(self.process.stdout))
            except StopIteration:  # server process has stopped
                for i in range(constant.WAIT_TIME_AFTER_SERVER_STDOUT_END_SEC *
                               10):
                    if self.process.poll() is not None:
                        break
                    time.sleep(0.1)
                    if i % 10 == 0:
                        self.logger.info(
                            self.tr('mcdr_server.receive.wait_stop'))
                raise ServerStopped()
            else:
                try:
                    text = text.decode(self.decoding_method)
                except:
                    self.logger.error(
                        self.tr('mcdr_server.receive.decode_fail', text))
                    raise
                return text.rstrip('\n\r').lstrip('\n\r')

    def tick(self):
        """
		ticking MCDR:
		try to receive a new line from server's stdout and parse / display / process the text
		"""
        try:
            text = self.receive()
        except ServerStopped:
            self.on_server_stop()
            return
        try:
            text = self.server_handler_manager.get_current_handler(
            ).pre_parse_server_stdout(text)
        except:
            self.logger.warning(self.tr('mcdr_server.tick.pre_parse_fail'))

        parsed_result: Info
        try:
            parsed_result = self.server_handler_manager.get_current_handler(
            ).parse_server_stdout(text)
        except:
            if self.logger.should_log_debug(
                    option=DebugOption.HANDLER
            ):  # traceback.format_exc() is costly
                self.logger.debug(
                    'Fail to parse text "{}" from stdout of the server, using raw handler'
                    .format(text))
                for line in traceback.format_exc().splitlines():
                    self.logger.debug('    {}'.format(line))
            parsed_result = self.server_handler_manager.get_basic_handler(
            ).parse_server_stdout(text)
        else:
            if self.logger.should_log_debug(option=DebugOption.HANDLER):
                self.logger.debug('Parsed text from server stdin:')
                for line in parsed_result.format_text().splitlines():
                    self.logger.debug('    {}'.format(line))
        self.server_handler_manager.detect_text(text)
        self.reactor_manager.put_info(parsed_result)

    def on_mcdr_start(self):
        self.task_executor.start()
        self.task_executor.enqueue_regular_task(self.load_plugins)
        self.task_executor.wait_till_finish_all_task()
        self.plugin_manager.dispatch_event(MCDRPluginEvents.MCDR_START, ())
        if not self.config['disable_console_thread']:
            self.console_handler.start()
        else:
            self.logger.info(
                self.tr('mcdr_server.on_mcdr_start.console_disabled'))
        if not self.start_server():
            raise ServerStartError()
        self.update_helper.start()
        self.watch_dog.start()
        self.server_handler_manager.start_handler_detection()
        self.set_mcdr_state(MCDReforgedState.RUNNING)

    def on_mcdr_stop(self):
        try:
            self.set_mcdr_state(MCDReforgedState.PRE_STOPPED)
            if self.is_interrupt():
                self.logger.info(
                    self.tr('mcdr_server.on_mcdr_stop.user_interrupted'))
            else:
                self.logger.info(
                    self.tr('mcdr_server.on_mcdr_stop.server_stop'))

            self.watch_dog.stop()  # it's ok for plugins to take some time
            self.watch_dog.join()
            self.plugin_manager.dispatch_event(MCDRPluginEvents.MCDR_STOP, (),
                                               wait=True)

            self.logger.info(self.tr('mcdr_server.on_mcdr_stop.bye'))
        except KeyboardInterrupt:  # I don't know why there sometimes will be a KeyboardInterrupt if MCDR is stopped by ctrl-c
            pass
        except:
            self.logger.exception(
                self.tr('mcdr_server.on_mcdr_stop.stop_error'))
        finally:
            self.set_mcdr_state(MCDReforgedState.STOPPED)

    def start(self):
        """
		The entry method to start MCDR
		Try to start the server. if succeeded the console thread will start and MCDR will start ticking

		:raise: IllegalStateError if MCDR is in wrong state
		:raise: ServerStartError if the server is already running or start_server has been called by other
		"""
        if not self.mcdr_in_state(MCDReforgedState.INITIALIZED):
            if self.mcdr_in_state(MCDReforgedState.INITIALIZING):
                raise IllegalStateError(
                    'This instance is not fully initialized')
            else:
                raise IllegalStateError('MCDR can only start once')
        self.main_loop()
        return self.process

    def main_loop(self):
        """
		The main loop of MCDR
		"""
        self.on_mcdr_start()
        while self.should_keep_looping():
            try:
                if self.is_server_running():
                    self.tick()
                else:
                    time.sleep(0.01)
            except KeyboardInterrupt:
                self.interrupt()
            except:
                if self.is_interrupt():
                    break
                else:
                    self.logger.critical(self.tr('mcdr_server.run.error'),
                                         exc_info=True)
        self.on_mcdr_stop()

    def connect_rcon(self):
        self.rcon_manager.disconnect()
        if self.config['rcon']['enable'] and self.is_server_rcon_ready():
            self.rcon_manager.connect(self.config['rcon']['address'],
                                      self.config['rcon']['port'],
                                      self.config['rcon']['password'])
Пример #4
0
 def __init__(self, *args):
     super().__init__(*args)
     self.language_manager = LanguageManager(logging.getLogger())