Example #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)
Example #2
0
 def logger(self) -> MCDReforgedLogger:
     if self.__logger_for_plugin is None:
         try:
             self.__logger_for_plugin = MCDReforgedLogger(
                 self._mcdr_server, self.__plugin.get_id())
         except:
             return super().logger
     return self.__logger_for_plugin
Example #3
0
 def wrap(self: 'ServerInterface', *args, is_plugin_call=True, **kwargs):
     if is_plugin_call and MCDReforgedLogger.should_log_debug(
             option=DebugOption.PLUGIN):
         self.logger.debug('Plugin called {}(), args amount: {}'.format(
             func.__name__, len(args)),
                           no_check=True)
         for arg in args:
             self.logger.debug('  - type: {}, content: {}'.format(
                 type(arg).__name__, arg),
                               no_check=True)
     return func(self, *args, **kwargs)
Example #4
0
 def wrap(self, *args, **kwargs):
     is_plugin_call = 'is_plugin_call'
     if kwargs.get(is_plugin_call,
                   True) and MCDReforgedLogger.should_log_debug(
                       option=DebugOption.PLUGIN):
         self.logger.debug('Plugin called {}(), args amount: {}'.format(
             func.__name__, len(args)))
         for arg in args:
             self.logger.debug('  - type: {}, content: {}'.format(
                 type(arg).__name__, arg))
     if is_plugin_call in kwargs:
         kwargs.pop(is_plugin_call)
     return func(self, *args, **kwargs)
Example #5
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'])
Example #6
0
	def __init__(self, *, generate_default_only: bool = False, initialize_environment: bool = False):
		"""
		:param generate_default_only: If set to true, MCDR will only generate the default configure and permission files
		"""
		self.mcdr_state = MCDReforgedState.INITIALIZING
		self.server_state = ServerState.STOPPED
		self.server_information = ServerInformation()
		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.config = Config(self.logger)
		self.permission_manager = PermissionManager(self)
		self.basic_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.translation_manager = TranslationManager(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.preference_manager = PreferenceManager(self)

		self.__check_environment()

		# --- Input arguments "generate_default_only" processing --- #
		if generate_default_only:
			self.config.save_default()
			self.permission_manager.save_default()
			return

		# --- Initialize fields instance --- #
		self.translation_manager.load_translations()  # translations are used for logging, so load them first
		if initialize_environment:
			# Prepare config / permission files if they're missing
			if not self.config.file_presents():
				self.config.save_default()
				default_config = self.config.get_default_yaml()
				file_util.touch_directory(default_config['working_directory'])  # create server/ folder
			if not self.permission_manager.file_presents():
				self.permission_manager.save_default()

		# Check if there's any file missing
		# If there's any, MCDR environment might not be probably setup
		file_missing = False

		def load(kind: str, func: Callable[[], Any]) -> bool:
			nonlocal file_missing
			try:
				func()
			except FileNotFoundError:
				self.logger.error('{} is missing'.format(kind.title()))
				file_missing = True
			except YAMLError as e:
				self.logger.error('Failed to load {}: {}'.format(kind, type(e).__name__))
				for line in str(e).splitlines():
					self.logger.error(line)
				return False
			else:
				return True

		# load_config: config, language, handlers, plugin directories, reactors, handlers
		# load_permission_file: permission
		# config change will lead to creating plugin folders
		loading_success = \
			load('configure', lambda: self.load_config(allowed_missing_file=False, echo=not initialize_environment)) and \
			load('permission', lambda: self.permission_manager.load_permission_file(allowed_missing_file=False))
		if file_missing:
			self.__on_file_missing()
			return
		if not loading_success:
			return

		# MCDR environment has been setup, so continue creating default folders and loading stuffs
		self.logger.set_file(core_constant.LOGGING_FILE)  # will create logs/ folder
		self.plugin_manager.touch_directory()  # will create config/ folder

		# --- Done --- #
		self.set_mcdr_state(MCDReforgedState.INITIALIZED)
Example #7
0
class MCDReforgedServer:
	process: Optional[Popen]

	def __init__(self, *, generate_default_only: bool = False, initialize_environment: bool = False):
		"""
		:param generate_default_only: If set to true, MCDR will only generate the default configure and permission files
		"""
		self.mcdr_state = MCDReforgedState.INITIALIZING
		self.server_state = ServerState.STOPPED
		self.server_information = ServerInformation()
		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.config = Config(self.logger)
		self.permission_manager = PermissionManager(self)
		self.basic_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.translation_manager = TranslationManager(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.preference_manager = PreferenceManager(self)

		self.__check_environment()

		# --- Input arguments "generate_default_only" processing --- #
		if generate_default_only:
			self.config.save_default()
			self.permission_manager.save_default()
			return

		# --- Initialize fields instance --- #
		self.translation_manager.load_translations()  # translations are used for logging, so load them first
		if initialize_environment:
			# Prepare config / permission files if they're missing
			if not self.config.file_presents():
				self.config.save_default()
				default_config = self.config.get_default_yaml()
				file_util.touch_directory(default_config['working_directory'])  # create server/ folder
			if not self.permission_manager.file_presents():
				self.permission_manager.save_default()

		# Check if there's any file missing
		# If there's any, MCDR environment might not be probably setup
		file_missing = False

		def load(kind: str, func: Callable[[], Any]) -> bool:
			nonlocal file_missing
			try:
				func()
			except FileNotFoundError:
				self.logger.error('{} is missing'.format(kind.title()))
				file_missing = True
			except YAMLError as e:
				self.logger.error('Failed to load {}: {}'.format(kind, type(e).__name__))
				for line in str(e).splitlines():
					self.logger.error(line)
				return False
			else:
				return True

		# load_config: config, language, handlers, plugin directories, reactors, handlers
		# load_permission_file: permission
		# config change will lead to creating plugin folders
		loading_success = \
			load('configure', lambda: self.load_config(allowed_missing_file=False, echo=not initialize_environment)) and \
			load('permission', lambda: self.permission_manager.load_permission_file(allowed_missing_file=False))
		if file_missing:
			self.__on_file_missing()
			return
		if not loading_success:
			return

		# MCDR environment has been setup, so continue creating default folders and loading stuffs
		self.logger.set_file(core_constant.LOGGING_FILE)  # will create logs/ folder
		self.plugin_manager.touch_directory()  # will create config/ folder

		# --- Done --- #
		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 __check_environment(self):
		"""
		Some checks at initialization
		In dev environment, you can use setup.py to create `mcdreforged.egg-info/` so the package check can pass
		"""
		mcdr_pkg = core_constant.PACKAGE_NAME  # should be "mcdreforged"
		try:
			pkg_resources.require(mcdr_pkg)
		except pkg_resources.ResolutionError:
			self.logger.warning('It looks like you\'re launching MCDR from source, since {} is not found in python packages'.format(mcdr_pkg))
			self.logger.warning('In this way, the plugin system might not work correctly')
			self.logger.warning('In a production environment, you should install {} from PyPI, see document ({}) for more information'.format(mcdr_pkg, core_constant.DOCUMENTION_URL))
			self.logger.warning('MCDR will launch after 20 seconds...')
			time.sleep(20)

	def __on_file_missing(self):
		self.logger.info('Looks like MCDR is not initialized at current directory {}'.format(os.getcwd()))
		self.logger.info('Use "python -m {} init" to initialize MCDR first'.format(core_constant.PACKAGE_NAME))

	# --------------------------
	#         Translate
	# --------------------------

	def get_language(self) -> str:
		return self.translation_manager.language

	def tr(self, translation_key: str, *args, language: Optional[str] = None, allow_failure=True, **kwargs) -> MessageText:
		"""
		Return a translated text corresponded to the translation key and format the text with given args
		If args contains RText element, then the result will be a RText, otherwise the result will be a regular str
		If the translation key is not recognized, the return value will be the translation key itself if allow_failure is True
		:param translation_key: The key of the translation
		:param args: The args to be formatted
		:param language: Specific language to be used in this translation, or the language that MCDR is using will be used
		:param allow_failure: If set to false, a KeyError will be risen if the translation key is not recognized
		:param kwargs: The kwargs to be formatted
		"""
		return self.translation_manager.translate(
			translation_key, args, kwargs,
			allow_failure=allow_failure,
			language=language,
			fallback_language=self.translation_manager.language,
			plugin_translations=self.plugin_manager.registry_storage.translations
		)

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

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

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

		self.translation_manager.set_language(self.config['language'])
		if echo:
			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()
		if echo:
			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'])
		if echo:
			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'])
		if echo:
			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_registry_changed(self):
		self.command_manager.clear_command()
		self.plugin_manager.registry_storage.export_commands(self.command_manager.register_command)

	# ---------------------------
	#      General Setters
	# ---------------------------
	# for field read-only access, simply use directly reference
	# but for field writing operation, use setters

	def set_task_executor(self, new_task_executor: TaskExecutor):
		self.task_executor = new_task_executor

	# ---------------------------
	#   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 should_exit_after_stop(self):
		return MCDReforgedFlag.EXIT_AFTER_STOP in self.flags

	def with_flag(self, flag: MCDReforgedFlag):
		self.flags |= flag
		self.logger.debug('Added MCDReforgedFlag {}'.format(flag), option=DebugOption.MCDR)

	def remove_flag(self, flag: MCDReforgedFlag):
		self.flags &= ~flag
		self.logger.debug('Removed MCDReforgedFlag {}'.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
			if self.should_exit_after_stop():  # natural server stop, or server_interface.exit() by plugin
				return False
			return True
		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) -> bool:
		"""
		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
		"""
		first_interrupt = not self.is_interrupt()
		self.logger.info('Interrupting, first strike = {}'.format(first_interrupt))
		if self.is_server_running():
			self.stop(forced=not first_interrupt)
		self.with_flag(MCDReforgedFlag.INTERRUPT)
		return first_interrupt

	def stop(self, forced: bool = 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.set_server_state(ServerState.RUNNING)
		self.with_flag(MCDReforgedFlag.EXIT_AFTER_STOP)  # Set after server state is set to RUNNING, or MCDR might have a chance to exit if the server is started by other thread
		self.logger.info(self.tr('mcdr_server.start_server.pid_info', self.process.pid))
		self.reactor_manager.on_server_start()
		self.plugin_manager.dispatch_event(MCDRPluginEvents.SERVER_START, ())

	def __on_server_stop(self):
		return_code = self.process.poll()
		self.logger.info(self.tr('mcdr_server.on_server_stop.show_stopcode', return_code))
		try:
			self.process.stdin.close()
		except Exception as e:
			self.logger.warning('Error when closing stdin: {}'.format(e))
		try:
			self.process.stdout.close()
		except Exception as e:
			self.logger.warning('Error when closing stdout: {}'.format(e))
		self.process = None
		self.set_server_state(ServerState.STOPPED)
		self.remove_flag(MCDReforgedFlag.SERVER_STARTUP | MCDReforgedFlag.SERVER_RCON_READY)  # removes this two
		self.reactor_manager.on_server_stop()
		self.plugin_manager.dispatch_event(MCDRPluginEvents.SERVER_STOP, (return_code,), block=True)

		if self.is_interrupt():
			self.logger.info(self.tr('mcdr_server.on_server_stop.user_interrupted'))
		else:
			self.logger.info(self.tr('mcdr_server.on_server_stop.server_stop'))

	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 isinstance(text, str):
			encoded_text = (text + ending).encode(encoding)
		elif isinstance(text, bytes):
			encoded_text = text
		else:
			raise TypeError()
		if self.is_server_running():
			self.process.stdin.write(encoded_text)
			self.process.stdin.flush()
		else:
			self.logger.warning(self.tr('mcdr_server.send.send_when_stopped'))
			self.logger.warning(self.tr('mcdr_server.send.send_when_stopped.text', text if len(text) <= 32 else text[:32] + '...'))

	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
		"""
		try:
			text = next(iter(self.process.stdout))  # type: bytes
		except StopIteration:  # server process has stopped
			for i in range(core_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:
				decoded_text = text.decode(self.decoding_method)  # type: str
			except:
				self.logger.error(self.tr('mcdr_server.receive.decode_fail', text))
				raise
			return decoded_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), no_check=True)
				for line in traceback.format_exc().splitlines():
					self.logger.debug('    {}'.format(line), no_check=True)
			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 stdout:', no_check=True)
				for line in parsed_result.format_text().splitlines():
					self.logger.debug('    {}'.format(line), no_check=True)
		self.server_handler_manager.detect_text(text)
		self.reactor_manager.put_info(parsed_result)

	def __on_mcdr_start(self):
		self.watch_dog.start()
		self.task_executor.start()
		self.preference_manager.load_preferences()
		self.plugin_manager.register_permanent_plugins()
		self.task_executor.execute_on_thread(self.load_plugins, block=True)
		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.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)

			self.logger.info(self.tr('mcdr_server.on_mcdr_stop.info'))

			self.plugin_manager.dispatch_event(MCDRPluginEvents.PLUGIN_UNLOADED, ())
			self.task_executor.wait_till_finish_all_task()
			with self.watch_dog.pausing():  # it's ok for plugins to take some time
				self.plugin_manager.dispatch_event(MCDRPluginEvents.MCDR_STOP, (), block=True)

			self.console_handler.stop()
			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'])
Example #8
0
 def __get_logger(self, plugin_name: str):
     return MCDReforgedLogger(self.__mcdr_server, plugin_name)
Example #9
0
    def __init__(self,
                 *,
                 generate_default_only: bool = False,
                 initialize_environment: bool = False):
        """
		:param generate_default_only: If set to true, MCDR will only generate the default configure and permission files
		"""
        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.config = Config(self.logger)
        self.permission_manager = PermissionManager(self)
        self.basic_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.translation_manager = TranslationManager(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)

        # --- Input arguments "generate_default_only" processing --- #
        if generate_default_only:
            self.config.save_default()
            self.permission_manager.save_default()
            return

        # --- Initialize fields instance --- #
        self.translation_manager.load_translations(
        )  # translations are used for logging, so load them first
        if initialize_environment:
            # Prepare config / permission files if they're missing
            if not self.config.file_presents():
                self.config.save_default()
                default_config = self.config.get_default_yaml()
                file_util.touch_directory(default_config['working_directory']
                                          )  # create server/ folder
            if not self.permission_manager.file_presents():
                self.permission_manager.save_default()

        # Check if there's any file missing
        # If there's any, MCDR environment might not be probably setup
        file_missing = False
        try:
            # loads config, language, handlers
            # config change will lead to creating plugin folders
            self.load_config(allowed_missing_file=False,
                             echo=not initialize_environment)
        except FileNotFoundError:
            self.logger.error('Configure is missing')
            file_missing = True
        try:
            self.permission_manager.load_permission_file(
                allowed_missing_file=False)
        except FileNotFoundError:
            self.logger.error('Permission file is missing')
            file_missing = True
        if file_missing:
            self.on_file_missing()
            return

        # MCDR environment has been setup, so continue creating default folders and loading stuffs
        self.logger.set_file(
            core_constant.LOGGING_FILE)  # will create logs/ folder
        self.plugin_manager.touch_directory()  # will create config/ folder

        # --- Done --- #
        self.set_mcdr_state(MCDReforgedState.INITIALIZED)